β‘ CLI Code Generators
Shipnative includes code generators that create fully-functional screens, components, stores, and API endpoints with a single command. No more copying and pasting boilerplate!
For AI Users: You donβt need these generators when using AI! Just ask your AI to βcreate a ProfileSettings screenβ and it will generate the code. These generators are for manual development or when you want a quick starting point.
π― Why Use Generators?
Without generators:
# You have to:
1. Create a new file
2. Write all the imports
3. Set up the component structure
4. Add styling boilerplate
5. Remember the correct patterns
6. Export the component
With generators:
yarn generate screen ProfileSettings
# Done! β
Fully functional screen ready to customize
What you get:
β
Correct file structure
β
All imports included
β
Styling with Unistyles/design tokens
β
TypeScript types
β
Follows project patterns
β
Ready to customize
π Quick Reference
# Generate a screen
yarn generate screen ProfileSettings
# Generate a component
yarn generate component UserCard
# Generate a Zustand store
yarn generate store notifications
# Generate an API endpoint
yarn generate api user-profile
π± Screen Generator
What It Does
Creates a complete screen component with:
Gradient background
Safe area handling
ScrollView for content
Proper styling with design tokens
Navigation ready
yarn generate screen < ScreenNam e >
Example
yarn generate screen ProfileSettings
Creates: apps/app/app/screens/ProfileSettingsScreen.tsx
Generated code:
import { View , ScrollView } from 'react-native'
import { StyleSheet , useUnistyles } from 'react-native-unistyles'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { LinearGradient } from 'expo-linear-gradient'
import { Text } from '@/components/Text'
export const ProfileSettingsScreen = () => {
const { theme } = useUnistyles ()
const insets = useSafeAreaInsets ()
return (
< View style = {styles. container } >
< LinearGradient
colors = { [
theme.colors.gradientStart,
theme.colors.gradientMiddle,
theme.colors.gradientEnd,
]}
style={styles.gradient}
>
<ScrollView
contentContainerStyle={[
styles.scrollContent,
{ paddingTop: insets.top + theme.spacing.lg }
]}
>
<Text preset="heading">Profile Settings</Text>
{ /* Add your content here */ }
</ScrollView>
</LinearGradient>
</View>
)
}
const styles = StyleSheet.create((theme) => ({
container: { flex: 1 },
gradient: { flex: 1 },
scrollContent: {
paddingHorizontal: theme . spacing . lg ,
paddingBottom: 100 ,
},
}))
Next Steps After Generation
Add to navigation :
// apps/app/app/navigators/AppNavigator.tsx
import { ProfileSettingsScreen } from '@/screens/ProfileSettingsScreen'
< Stack . Screen name = "ProfileSettings" component = { ProfileSettingsScreen } />
Add TypeScript type :
// apps/app/app/navigators/navigationTypes.ts
export type AppStackParamList = {
ProfileSettings : undefined // or pass params: { userId: string }
}
Navigate to it :
navigation . navigate ( 'ProfileSettings' )
π§© Component Generator
What It Does
Creates a reusable component with:
TypeScript props interface
Unistyles styling
Design tokens
Export ready
yarn generate component < ComponentNam e >
Example
yarn generate component UserCard
Creates: apps/app/app/components/UserCard.tsx
Generated code:
import { View , Pressable } from 'react-native'
import { StyleSheet , useUnistyles } from 'react-native-unistyles'
import { Text } from './Text'
export interface UserCardProps {
name : string
email ?: string
avatar ?: string
onPress ?: () => void
}
export const UserCard = ({ name , email , onPress } : UserCardProps ) => {
const { theme } = useUnistyles ()
return (
< Pressable
style = {styles. container }
onPress = { onPress }
>
< Text preset = "subheading" > { name } </ Text >
{ email && < Text preset = "body" > { email } </ Text > }
</ Pressable >
)
}
const styles = StyleSheet . create (( theme ) => ({
container: {
backgroundColor: theme . colors . card ,
borderRadius: theme . radius . lg ,
padding: theme . spacing . md ,
marginBottom: theme . spacing . md ,
borderWidth: 1 ,
borderColor: theme . colors . border ,
},
}))
Using Your Component
import { UserCard } from '@/components/UserCard'
< UserCard
name = "John Doe"
email = "john@example.com"
onPress = {() => navigation.navigate( 'Profile' , { userId : '123' })}
/>
Exporting Your Component
Add to apps/app/app/components/index.ts:
export * from './UserCard'
Now you can import it from anywhere:
import { UserCard } from '@/components'
ποΈ Store Generator
What It Does
Creates a Zustand store with:
TypeScript types
Persistence (saves to device storage)
Common CRUD actions
Loading states
yarn generate store < storeNam e >
Example
yarn generate store notifications
Creates: apps/app/app/stores/notificationsStore.ts
Generated code:
import { create } from 'zustand'
import { persist , createJSONStorage } from 'zustand/middleware'
import { storage } from '@/utils/storage'
export interface NotificationsState {
// State
items : any []
loading : boolean
// Actions
fetchItems : () => Promise < void >
addItem : ( item : any ) => void
removeItem : ( id : string ) => void
clearAll : () => void
}
export const useNotificationsStore = create < NotificationsState >()(
persist (
( set , get ) => ({
items: [],
loading: false ,
fetchItems : async () => {
set ({ loading: true })
try {
// TODO: Fetch from your API
const items = []
set ({ items , loading: false })
} catch ( error ) {
console . error ( 'Error fetching notifications:' , error )
set ({ loading: false })
}
},
addItem : ( item ) => {
set (( state ) => ({
items: [ ... state . items , item ],
}))
},
removeItem : ( id ) => {
set (( state ) => ({
items: state . items . filter (( item ) => item . id !== id ),
}))
},
clearAll : () => {
set ({ items: [] })
},
}),
{
name: 'notifications-storage' , // Saved to device storage
storage: createJSONStorage (() => ({
getItem : ( key ) => storage . getString ( key ) ?? null ,
setItem : ( key , value ) => storage . set ( key , value ),
removeItem : ( key ) => storage . delete ( key ),
})),
},
),
)
Using Your Store
import { useNotificationsStore } from '@/stores/notificationsStore'
function NotificationsScreen () {
const { items , loading , fetchItems , addItem } = useNotificationsStore ()
useEffect (() => {
fetchItems ()
}, [])
if ( loading ) return < Spinner />
return (
< View >
{ items . map ( item => (
< Text key = {item. id } > {item. message } </ Text >
))}
</ View >
)
}
What is Persistence?
Persistence means the store saves to device storage. When the user closes and reopens the app, the data is still there!
Example:
// User adds a notification
addItem ({ id: '1' , message: 'Hello!' })
// User closes app
// User reopens app
// Data is still there!
console . log ( items ) // [{ id: '1', message: 'Hello!' }]
π API Generator
What It Does
Creates an API service with:
TypeScript types
CRUD operations (Create, Read, Update, Delete)
Error handling
Supabase integration
yarn generate api < endpointNam e >
Example
yarn generate api user-profile
Creates: apps/app/app/services/api/userProfile.ts
Generated code:
import { supabase } from '@/services/supabase'
export interface UserProfile {
id : string
username : string
full_name : string
avatar_url ?: string
bio ?: string
}
export const userProfileApi = {
/**
* Get user profile by ID
*/
async getProfile ( userId : string ) : Promise < UserProfile | null > {
const { data , error } = await supabase
. from ( 'profiles' )
. select ( '*' )
. eq ( 'id' , userId )
. single ()
if ( error ) {
console . error ( 'Error fetching profile:' , error )
return null
}
return data
},
/**
* Update user profile
*/
async updateProfile (
userId : string ,
updates : Partial < UserProfile >
) : Promise < boolean > {
const { error } = await supabase
. from ( 'profiles' )
. update ( updates )
. eq ( 'id' , userId )
if ( error ) {
console . error ( 'Error updating profile:' , error )
return false
}
return true
},
/**
* Delete user profile
*/
async deleteProfile ( userId : string ) : Promise < boolean > {
const { error } = await supabase
. from ( 'profiles' )
. delete ()
. eq ( 'id' , userId )
if ( error ) {
console . error ( 'Error deleting profile:' , error )
return false
}
return true
},
}
Using Your API
import { userProfileApi } from '@/services/api/userProfile'
// Get profile
const profile = await userProfileApi . getProfile ( 'user-123' )
// Update profile
const success = await userProfileApi . updateProfile ( 'user-123' , {
full_name: 'Jane Doe' ,
bio: 'Mobile app developer' ,
})
// Delete profile
await userProfileApi . deleteProfile ( 'user-123' )
Using with React Query (Recommended)
For better caching and loading states, use React Query:
import { useQuery , useMutation , useQueryClient } from '@tanstack/react-query'
import { userProfileApi } from '@/services/api/userProfile'
export function useUserProfile ( userId : string ) {
return useQuery ({
queryKey: [ 'user-profile' , userId ],
queryFn : () => userProfileApi . getProfile ( userId ),
enabled: !! userId , // Only run if userId exists
})
}
export function useUpdateUserProfile () {
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : ({ userId , updates }) =>
userProfileApi . updateProfile ( userId , updates ),
onSuccess : ( _ , { userId }) => {
// Refresh the profile data after update
queryClient . invalidateQueries ({ queryKey: [ 'user-profile' , userId ] })
},
})
}
Usage in component:
function ProfileScreen ({ userId }) {
const { data : profile , isLoading } = useUserProfile ( userId )
const updateProfile = useUpdateUserProfile ()
const handleSave = () => {
updateProfile . mutate ({
userId ,
updates: { bio: 'New bio!' }
})
}
if ( isLoading ) return < Spinner />
return (
< View >
< Text >{profile. full_name } </ Text >
< Button onPress = { handleSave } title = "Save" />
</ View >
)
}
π‘ Tips & Best Practices
When to Use Generators vs AI
Use Generators Use AI Quick file creation Complex features with business logic Starting point for manual coding When you want AI to handle patterns Learning the codebase structure Building complete flows end-to-end Simple CRUD operations Custom implementations
Naming Conventions
Screens:
Use PascalCase: ProfileSettings, UserDashboard
Generator adds βScreenβ suffix automatically
File: ProfileSettingsScreen.tsx
Component: ProfileSettingsScreen
Components:
Use PascalCase: UserCard, PostList
File: UserCard.tsx
Component: UserCard
Stores:
Use camelCase: notifications, userPreferences
Generator adds βStoreβ suffix automatically
File: notificationsStore.ts
Hook: useNotificationsStore
APIs:
Use kebab-case: user-profile, post-comments
File: userProfile.ts (camelCase)
Object: userProfileApi
Customizing Templates
Want to change what the generators create? Edit the generator script!
Location: scripts/generate.js (create this file if it doesnβt exist)
Example: Add default error handling to all API generators:
// In generateApi function
const template = `
export const ${ name } Api = {
async get(id: string) {
try {
const { data, error } = await supabase
.from(' ${ name } ')
.select('*')
.eq('id', id)
.single()
if (error) throw error
return data
} catch (error) {
console.error('[ ${ name } Api] Get error:', error)
Sentry.captureException(error) // Add Sentry tracking
return null
}
},
}
`
π Troubleshooting
Command not found: generate
Cause: The generator script isnβt set up in your project.Solution:
Check if scripts/generate.js exists
Verify package.json has the script:
{
"scripts" : {
"generate" : "node scripts/generate.js"
}
}
Run yarn install to refresh scripts
Generated file has wrong imports
Cause: Path aliases may not be configured.Solution:
Check tsconfig.json has:
{
"compilerOptions" : {
"paths" : {
"@/*" : [ "./app/*" ]
}
}
}
Update the generator template to use correct paths
Screen doesn't show in navigation
Cause: You need to manually add it to the navigator.Solution:
Import the screen in AppNavigator.tsx
Add <Stack.Screen> entry
Add TypeScript type to navigationTypes.ts
See Screen Generator for details
Store doesn't persist data
Cause: Storage permissions or middleware configuration.Solution:
Check that MMKV storage is initialized in utils/storage.ts
Verify the store has persist() middleware
Check device storage permissions
Try clearing app data and reinstalling
API returns null in mock mode
Cause: Mock Supabase doesnβt have the table youβre querying.Solution:
Use mock helpers to seed data:
import { mockSupabaseHelpers } from '@/services/mocks/supabase'
mockSupabaseHelpers . seedTable ( 'profiles' , [
{ id: '1' , username: 'johndoe' , full_name: 'John Doe' }
])
Or add real Supabase credentials to test with real data
π Next Steps
Pro Tip: Generate files as a starting point, then customize them for your needs. Donβt be afraid to modify the generated code - itβs yours to own!