Persist React Navigation Router State with Expo Router
You might want to save the user's location in the app, so that they are immediately returned to the same location after the app is restarted. This is especially valuable during development because it allows the developer to stay on the same screen when they refresh the app.
A reference post for anyone looking to persist the state of a expo-router app during development
I always find myself copying this small bit of code from react navigation docs. I need to equivilant for expo-router.
If this has been helpful to you, please let me know on twitter or don't feel shy about buying me a coffee or starring this repo on github
React Navigation
import * as React from "react";
import { Linking, Platform } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { NavigationContainer } from "@react-navigation/native";
const PERSISTENCE_KEY = "NAVIGATION_STATE_V1";
export default function App() {
const [isReady, setIsReady] = React.useState(Platform.OS === "web"); // Don't persist state on web since it's based on URL
const [initialState, setInitialState] = React.useState();
React.useEffect(() => {
const restoreState = async () => {
try {
const initialUrl = await Linking.getInitialURL();
if (initialUrl == null) {
// Only restore state if there's no deep link
const savedStateString = await AsyncStorage.getItem(PERSISTENCE_KEY);
const state = savedStateString
? JSON.parse(savedStateString)
: undefined;
if (state !== undefined) {
setInitialState(state);
}
}
} finally {
setIsReady(true);
}
};
if (!isReady) {
restoreState();
}
}, [isReady]);
if (!isReady) {
return null;
}
return (
<NavigationContainer
initialState={initialState}
onStateChange={(state) =>
AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state))
}
>
{/* ... */}
</NavigationContainer>
);
}
Expo Router
import { useNavigation, NavigationState } from "@react-navigation/native";
import { useCallback, useState, useEffect } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
return function (this: any, ...args: Parameters<T>) {
const context = this;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
function useDebounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): T {
const debouncedFunc = useCallback(debounce(func, wait), [func, wait]);
return debouncedFunc as T;
}
const PERSISTENCE_KEY = "NAVIGATION_STATE_V1";
/**
* Persist the navigation state to AsyncStorage
* @returns The initial state of the navigation
*
* @example
* const initialState = usePersistNavigationState()
* return <Stack initialRouteName={initialState || 'index'} />
*/
export function usePersistNavigationState() {
const [initialState, setInitialState] = useState<NavigationState>();
const navigation = useNavigation();
useEffect(() => {
if (__DEV__) {
const restoreState = async () => {
try {
const savedStateString = await AsyncStorage.getItem(PERSISTENCE_KEY);
if (savedStateString) {
const state = JSON.parse(savedStateString);
setInitialState(state);
navigation.reset(state);
}
} catch (e) {
if (e instanceof Error) {
console.warn("Failed to restore navigation state:", e.message);
}
}
};
restoreState();
}
}, [navigation]);
const debouncedSaveState = useDebounce(async (state: NavigationState) => {
if (__DEV__) {
try {
await AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state));
} catch (e) {
if (e instanceof Error) {
console.warn("Failed to save navigation state:", e);
}
}
}
}, 1000);
useEffect(() => {
if (__DEV__) {
const saveState = () => {
const state = navigation.getState();
if (state) {
debouncedSaveState(state);
}
};
const unsubscribe = navigation.addListener("state", saveState);
return unsubscribe;
}
}, [navigation, debouncedSaveState]);
return __DEV__ ? initialState : null;
}