Solito Cross-Platform Development Skill
A specialized skill for building universal applications using Solito, combining Expo (React Native) and Next.js with shared navigation and component libraries.
When to Use This Skill
Invoke this skill when:
-
Building universal apps (web + mobile) with shared code
-
Implementing cross-platform navigation
-
Creating shared UI components that work on web and native
-
Setting up monorepo structure for Solito projects
-
Handling platform-specific code (web-only or native-only features)
-
Implementing authentication across platforms
-
Debugging navigation or routing issues
-
Optimizing bundle sizes for web and native
What is Solito?
Solito enables developers to share navigation code between React Native (Expo) and Next.js. Instead of maintaining separate routing logic for web and mobile, Solito provides a unified navigation layer that respects platform-specific patterns while sharing as much code as possible.
Key Benefits:
-
Single source of truth for navigation
-
File-system routing for native apps (via Expo Router)
-
Shared UI components with platform-specific optimizations
-
Unified API for links and navigation
-
Type-safe routing with TypeScript
Before You Start: Essential Solito Tips
Project Structure First
Solito projects use monorepo architecture. Always structure your project correctly from the start:
my-app/ apps/ expo/ # React Native mobile app next/ # Next.js web app packages/ app/ # Shared UI components & navigation api/ # Shared API client (optional) package.json # Workspace configuration
See references/monorepo-setup.md for detailed workspace configuration.
Use Solito Starter Template
The official Solito starter saves hours of configuration:
npx create-solito-app@latest my-app
Choose your preferred package manager (pnpm recommended for monorepos).
Navigation is Different
Don't use Next.js Link or React Navigation Link directly. Use Solito's unified components:
import { Link } from 'solito/link'
// ✅ CORRECT: Works on both web and native <Link href="/profile"> <Text>View Profile</Text> </Link>
// ❌ WRONG: Next.js-only import Link from 'next/link'
// ❌ WRONG: React Navigation-only import { Link } from '@react-navigation/native'
See references/navigation-patterns.md for detailed routing patterns.
Core Development Principles
- Shared by Default, Platform-Specific When Needed
80% of your code should live in packages/app . Only create platform-specific code when absolutely necessary.
// packages/app/features/home/screen.tsx // ✅ GOOD: Shared screen component export function HomeScreen() { return ( <View> <Text>Welcome!</Text> <Link href="/profile"> <Text>Go to Profile</Text> </Link> </View> ) }
Platform-specific code should be minimal:
// packages/app/components/map.tsx import { Platform } from 'react-native'
export function Map() { // ✅ GOOD: Platform detection for necessary differences if (Platform.OS === 'web') { return <WebMapComponent /> } return <NativeMapComponent /> }
- Think in Features, Not Platforms
Organize by feature domain, not by platform:
packages/app/ features/ home/ screen.tsx # Home screen components/ # Home-specific components profile/ screen.tsx # Profile screen components/ auth/ login-screen.tsx register-screen.tsx components/ # Shared UI components provider/ # Navigation provider navigation/ # Navigation configuration
Not organized by platform:
❌ BAD: packages/app/ web/ screens/ native/ screens/
- Navigation is Central
Design your navigation structure early. Solito uses file-system routing on native and Next.js App Router on web.
Navigation mapping:
// apps/next/app/profile/page.tsx (web route) import { ProfileScreen } from 'app/features/profile/screen'
export default ProfileScreen
// apps/expo/app/profile.tsx (native route) import { ProfileScreen } from 'app/features/profile/screen'
export default ProfileScreen
Both platforms render the same ProfileScreen component from packages/app .
See references/navigation-patterns.md for routing patterns, params, and navigation hooks.
Development Workflow
Local Development
Run both platforms simultaneously:
Terminal 1 - Web:
cd apps/next pnpm dev
Terminal 2 - Mobile:
cd apps/expo pnpm start
Press i for iOS simulator, a for Android emulator, or scan QR code for physical device.
Making Changes
-
Create shared component in packages/app/components/
-
Create screen in packages/app/features/[feature]/screen.tsx
-
Add routes in both apps/next/app/ and apps/expo/app/
-
Test on both platforms to verify cross-platform behavior
Common Commands
Install dependencies (from root)
pnpm install
Run web dev server
cd apps/next && pnpm dev
Run Expo dev server
cd apps/expo && pnpm start
Type checking
pnpm typecheck
Lint all packages
pnpm lint
Build for production
pnpm build
Styling with NativeWind
NativeWind brings Tailwind CSS to React Native, enabling shared styling across platforms.
import { View, Text } from 'react-native'
// ✅ GOOD: Tailwind classes work on both platforms export function Card() { return ( <View className="bg-white rounded-lg p-4 shadow-md"> <Text className="text-xl font-bold">Title</Text> <Text className="text-gray-600">Description</Text> </View> ) }
Platform-specific styles when needed:
<View className="p-4 web:p-6 native:p-2"> {/* Different padding on web vs native */} </View>
Design Tokens
Use shared design tokens in packages/app/design/tokens.ts :
export const colors = { primary: '#3B82F6', secondary: '#10B981', background: '#FFFFFF', text: '#1F2937', }
export const spacing = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, }
Authentication Patterns
Share auth logic across platforms:
// packages/app/lib/auth.ts import { createContext, useContext } from 'react'
interface AuthContextType { user: User | null signIn: (email: string, password: string) => Promise<void> signOut: () => Promise<void> }
export const AuthContext = createContext<AuthContextType>(null!)
export function useAuth() { return useContext(AuthContext) }
Use in components:
import { useAuth } from 'app/lib/auth'
export function ProfileScreen() { const { user, signOut } = useAuth()
return ( <View> <Text>Welcome, {user?.name}</Text> <Button onPress={signOut}>Sign Out</Button> </View> ) }
Popular auth solutions:
-
Clerk - Excellent Solito integration, works on both platforms
-
Supabase - Self-hosted option with good RN support
-
Custom JWT - Full control, requires more setup
API Integration
Share API client in packages/api/ :
// packages/api/client.ts const API_URL = process.env.NEXT_PUBLIC_API_URL
export async function fetchUser(id: string) {
const response = await fetch(${API_URL}/users/${id})
return response.json()
}
Use with tRPC for type-safety:
// packages/api/trpc.ts import { initTRPC } from '@trpc/server'
const t = initTRPC.create()
export const appRouter = t.router({ getUser: t.procedure .input(z.string()) .query(async ({ input }) => { return db.user.findUnique({ where: { id: input } }) }), })
export type AppRouter = typeof appRouter
tRPC works seamlessly across web and native with full type-safety.
State Management
Recommended: Zustand (lightweight, works great with Solito)
// packages/app/store/user.ts import { create } from 'zustand'
interface UserState { user: User | null setUser: (user: User | null) => void }
export const useUserStore = create<UserState>((set) => ({ user: null, setUser: (user) => set({ user }), }))
Use in components:
import { useUserStore } from 'app/store/user'
export function ProfileScreen() { const user = useUserStore((state) => state.user)
return <Text>Welcome, {user?.name}</Text> }
Other options:
-
Jotai - Atomic state management
-
Redux Toolkit - For complex state needs
-
React Context - For simple global state
Platform-Specific Code
Use platform detection sparingly:
import { Platform } from 'react-native'
export function VideoPlayer() { if (Platform.OS === 'web') { return <video src={url} controls /> }
// Use native video component return <Video source={{ uri: url }} /> }
Better: Use platform-specific files:
packages/app/components/ video-player.tsx # Shared interface video-player.web.tsx # Web implementation video-player.native.tsx # Native implementation
// video-player.tsx export { VideoPlayer } from './video-player.native'
Metro bundler and Next.js will automatically resolve the correct file.
See references/platform-specific.md for detailed patterns.
Common Pitfalls to Avoid
Critical mistakes and their solutions are documented in references/common-pitfalls.md . Key pitfalls include:
-
Using Next.js Link or React Navigation Link directly
-
Hardcoding absolute URLs (use relative paths)
-
Not handling navigation params correctly
-
Importing from wrong packages (next/link vs solito/link)
-
Forgetting to configure navigation provider
-
Not testing on both platforms regularly
-
Overusing platform-specific code
Review references/common-pitfalls.md before implementing navigation or shared components.
Performance Optimization
Bundle Size
Keep packages/app lean:
-
Avoid large dependencies in shared code
-
Use dynamic imports for heavy features
-
Split platform-specific dependencies
// ✅ GOOD: Dynamic import for heavy feature const HeavyChart = dynamic(() => import('./heavy-chart'))
Navigation Performance
Preload screens for better UX:
import { useRouter } from 'solito/router'
export function HomeScreen() { const router = useRouter()
// Preload profile screen on hover/focus const handlePreload = () => { router.push('/profile', undefined, { shallow: true }) }
return ( <Link href="/profile" onHoverIn={handlePreload}> <Text>Profile</Text> </Link> ) }
Image Optimization
Use Next.js Image on web, optimized Image on native:
// packages/app/components/optimized-image.tsx import { Platform } from 'react-native' import NextImage from 'next/image' import { Image as RNImage } from 'react-native'
export function OptimizedImage({ src, width, height, alt }) { if (Platform.OS === 'web') { return <NextImage src={src} width={width} height={height} alt={alt} /> }
return <RNImage source={{ uri: src }} style={{ width, height }} /> }
Testing Strategy
Unit Tests
Test shared logic in packages/app:
// packages/app/lib/tests/format.test.ts import { formatCurrency } from '../format'
describe('formatCurrency', () => { it('formats USD correctly', () => { expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56') }) })
Component Tests
Use React Native Testing Library:
import { render, screen } from '@testing-library/react-native' import { HomeScreen } from '../screen'
test('renders welcome message', () => { render(<HomeScreen />) expect(screen.getByText('Welcome!')).toBeTruthy() })
E2E Tests
Playwright for web, Detox for native:
// apps/next/e2e/home.spec.ts import { test, expect } from '@playwright/test'
test('navigates to profile', async ({ page }) => { await page.goto('/') await page.click('text=Profile') await expect(page).toHaveURL('/profile') })
Deployment
Web (Next.js)
Deploy to Vercel (recommended):
cd apps/next vercel deploy
Configure environment variables in Vercel dashboard.
Mobile (Expo)
Build with EAS:
cd apps/expo
Install EAS CLI
npm install -g eas-cli
Configure project
eas build:configure
Build for iOS
eas build --platform ios
Build for Android
eas build --platform android
Submit to stores
eas submit
Over-the-air updates:
eas update --branch production
References
This skill includes detailed reference documentation:
-
references/navigation-patterns.md
-
Link usage, router hooks, params, navigation state
-
references/monorepo-setup.md
-
Workspace configuration, package structure, dependencies
-
references/platform-specific.md
-
Platform detection, conditional rendering, file extensions
-
references/common-pitfalls.md
-
Import issues, navigation bugs, platform quirks
Load these references as needed to inform implementation decisions.
Additional Resources
Solito Documentation:
-
Official Docs: https://solito.dev/
-
Solito Starter: https://github.com/nandorojo/solito
-
Example Apps: https://github.com/nandorojo/solito/tree/master/example-monorepos
Related Technologies:
-
Expo Router: https://docs.expo.dev/router/introduction/
-
Next.js App Router: https://nextjs.org/docs/app
-
NativeWind: https://www.nativewind.dev/
-
React Navigation: https://reactnavigation.org/
State Management:
-
Zustand: https://github.com/pmndrs/zustand
-
Jotai: https://jotai.org/
-
Redux Toolkit: https://redux-toolkit.js.org/
Authentication:
-
Clerk: https://clerk.com/
-
Supabase: https://supabase.com/
-
NextAuth.js: https://next-auth.js.org/
Remember: Share code by default, create platform-specific code only when necessary. Design your navigation structure early, test on both platforms regularly, and keep packages/app as the single source of truth for your application logic.