Passing Objects Using Expo Router
Problem Statement
When building mobile apps with Expo Router, a common requirement is passing complex data between screens. Specifically, developers need to send JavaScript objects (like product or user data) to detail screens without making redundant API calls.
The challenge arises because Expo Router requires navigation state to be serialized into URL parameters, which causes problems:
- Passing entire objects leads to URL encoding issues with special characters
- Complex objects with URLs or nested structures cause route errors
- Data types like Dates convert to strings
- Security risks exposing sensitive data in URLs
- Potential URL length limitations (~2000 characters)
Recommended Solution: State Management Approach
The optimal solution uses global state instead of URL parameters. Pass only an identifier via the route path, then retrieve the full object from application state.
Implementation Steps
- Create a global state context:
import React, { createContext, useState, useContext } from 'react';
const ItemCacheContext = createContext<{
cache: Record<string, any>;
addItem: (item: any) => void;
}>({
cache: {},
addItem: () => {},
});
export function ItemCacheProvider({ children }: { children: React.ReactNode }) {
const [cache, setCache] = useState<Record<string, any>>({});
const addItem = (item: any) => {
setCache(prev => ({ ...prev, [item.id]: item }));
};
return (
<ItemCacheContext.Provider value={{ cache, addItem }}>
{children}
</ItemCacheContext.Provider>
);
}
export function useItemCache() {
return useContext(ItemCacheContext);
}
- Modify your navigation logic:
import { router } from 'expo-router';
import { useItemCache } from '../context/ItemCache';
export default function ListScreen() {
const { addItem } = useItemCache();
const handlePress = (item) => {
addItem(item);
router.push(`/details/${item.id}`);
};
return (
<FlatList
data={items}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => handlePress(item)}>
<Text>{item.title}</Text>
</TouchableOpacity>
)}
/>
);
}
- Retrieve in your detail screen:
import { useLocalSearchParams } from 'expo-router';
import { useItemCache } from '../../context/ItemCache';
export default function ItemDetail() {
const { id } = useLocalSearchParams();
const { cache } = useItemCache();
const item = cache[id as string];
if (!item) {
return <Text>Item not found</Text>;
}
return (
<View>
<Text>{item.title}</Text>
<Image source={{ uri: item.imageURL }} style={{ width: 200, height: 200 }} />
</View>
);
}
Tip
Wrap your app in the provider:
import { ItemCacheProvider } from './context/ItemCache';
export default function App() {
return (
<ItemCacheProvider>
<Stack />
</ItemCacheProvider>
);
}
Key Advantages
- Avoids URL encoding issues and limitations
- Maintains object integrity (dates, nested objects, etc.)
- No additional network requests required
- Keeps URL clean and RESTful
- Improves performance for large datasets
- Follows React Navigation best practices
Alternative Approaches
URL Parameters (Simple Primitives Only)
For single primitive values:
router.push(`/details/${item.id}?name=${encodeURIComponent(item.name)}`);
const { id, name } = useLocalSearchParams();
Base64 Serialization (Small Objects Only)
Warning
Use only for small, non-sensitive objects due to URL length limitations.
import base64 from 'base-64';
const serializedItem = base64.encode(JSON.stringify(item));
router.push(`/details/${serializedItem}`);
const { data } = useLocalSearchParams<{ data: string }>();
const item = JSON.parse(base64.decode(data));
Best Practices Summary
Preferred:
- Pass object identifiers (IDs)
- Use context API, Zustand, or React Query
- Implement caching mechanisms
Avoid:
- Passing large objects via URLs
- Putting sensitive data in URLs
- Using
JSON.stringify
without Base64 encoding - Complex nested objects as params