Persist state with react-navigation AppContainer - javascript

I updated react-navigation for a react-native project from v2.x to v3.x. For the v2.x I had this rendered at root:
const AppNavigator = createStackNavigator({...})
const App = () => <AppNavigator persistenceKey={"NavigationState"} />;
export default App;
I need to persist the state, thats why I used persistenceKey
For the v3.x of react-navigation an app container is required, but I'm having problems figuring out how to implement the same state persistence.
This is my new code with the v3.x
const AppNavigator = createStackNavigator({...})
const AppContainer = createAppContainer(AppNavigator)
const App = () => <AppContainer />;
export default App;
How do I persist the state this way?
Thanks
EDIT:
I've tried this:
const AppNavigator = createStackNavigator({...})
const persistenceKey = "persistenceKey"
const persistNavigationState = async (navState) => {
try {
await AsyncStorage.setItem(persistenceKey, JSON.stringify(navState))
} catch(err) {
// handle the error according to your needs
}
}
const loadNavigationState = async () => {
const jsonString = await AsyncStorage.getItem(persistenceKey)
return JSON.parse(jsonString)
}
const AppNavigationPersists = () => <AppNavigator
persistNavigationState={persistNavigationState}
loadNavigationState={loadNavigationState}
/>
const AppContainer = createAppContainer(AppNavigationPersists)
export default AppContainer;
but I get this error:
Cannot read property 'getStateForAction' of undefined

You may need to update react-navigation to >= 3.10.0.
Per the react-navigation changelog, they only now only support persistNavigationState and loadNavigationState on react-navigation#^3.10.
You can still use persistenceKey on versions lower than 3.10.
---EDIT---
An example of a version <3.10.0:
const AppNavigator = createStackNavigator({...})
const App = () => <AppNavigator persistenceKey={"NavigationState"} />;
export default App;
An example implementation for a version >= 3.10.0:
const AppNavigator = createStackNavigator({...});
const persistenceKey = "persistenceKey"
const persistNavigationState = async (navState) => {
try {
await AsyncStorage.setItem(persistenceKey, JSON.stringify(navState))
} catch(err) {
// handle the error according to your needs
}
}
const loadNavigationState = async () => {
const jsonString = await AsyncStorage.getItem(persistenceKey)
return JSON.parse(jsonString)
}
const App = () => <AppNavigator persistNavigationState={persistNavigationState} loadNavigationState={loadNavigationState} />;

You can check the docs example for v3.x.
const AppNavigator = createStackNavigator({...});
const persistenceKey = "persistenceKey"
const persistNavigationState = async (navState) => {
try {
await AsyncStorage.setItem(persistenceKey, JSON.stringify(navState))
} catch(err) {
// handle the error according to your needs
}
}
const loadNavigationState = async () => {
const jsonString = await AsyncStorage.getItem(persistenceKey)
return JSON.parse(jsonString)
}
const App = () => <AppNavigator persistNavigationState={persistNavigationState} loadNavigationState={loadNavigationState} />;
To solve the problem in const AppContainer = createAppContainer(AppNavigator) what you can do is create another component that return AppNavigator with the persistence.
const AppNavigationPersists = () => <AppNavigator
persistNavigationState={persistNavigationState}
loadNavigationState={loadNavigationState}
/>
const AppContainer = createAppContainer(AppNavigationPersists)

Just implement it the same way as in the example. In my case I forgot to update react-navigation to version ^3.11.0 and I also forgot to import AsyncStorage. Somehow react native wasn't complaining about AsyncStorage not being there. This is why the state persistence didn't seem to work.
import {
AsyncStorage
} from "react-native";
const AppNavigator = createStackNavigator({...});
const AppContainer = createAppContainer(AppNavigator);
const persistenceKey = "persistenceKey"
const persistNavigationState = async (navState) => {
try {
await AsyncStorage.setItem(persistenceKey, JSON.stringify(navState))
} catch(err) {
// handle the error according to your needs
}
}
const loadNavigationState = async () => {
const jsonString = await AsyncStorage.getItem(persistenceKey)
return JSON.parse(jsonString)
}
const App = () => <AppContainer persistNavigationState={persistNavigationState} loadNavigationState={loadNavigationState} renderLoadingExperimental={() => <ActivityIndicator />}/>;

The doc is not clear but you actually need to pass the props persistNavigationState and loadNavigationState directly to the AppContainer component:
<AppContainer
persistNavigationState={persistNavigationState}
loadNavigationState={loadNavigationState}
/>

If you are using createAppContainer with react navigation 4, here is the solution which worked for me.
const App: () => React$Node = () => {
const persistenceKey = "persistenceKey"
const persistNavigationState = async (navState) => {
try {
await AsyncStorage.setItem(persistenceKey, JSON.stringify(navState))
} catch(err) {
// handle error
}
}
const loadNavigationState = async () => {
const jsonString = await AsyncStorage.getItem(persistenceKey)
return JSON.parse(jsonString)
}
return(
<View style={{flex: 1, backgroundColor: '#000000'}}>
<AppContainer
persistNavigationState={persistNavigationState}
loadNavigationState={loadNavigationState}
/>
</View>
);
};

Related

getInitialProps cant be accessed

I am having trouble accessing 'initialReduxState' when setting the hook with 'setReduxStore' in getinitialProps and cant seem to figure out why. Below is a snippet of the code.
export default (App) => {
return function Redux() {
let [reduxStore, setReduxStore] = useState();
let [persistor, setPersistor] = useState();
Redux.getInitialProps = async (appContext) => {
const reduxStore = getOrCreateStore();
// Provide the store to getInitialProps of pages
appContext.ctx.reduxStore = reduxStore;
let appProps = {};
if (App.getInitialProps) {
appProps = await App.getInitialProps(appContext);
}
return {
...appProps,
initialReduxState: reduxStore.getState(),
};
};
setReduxStore(getOrCreateStore(initialReduxState));
setPersistor(persistStore(reduxStore));
return <App {...props} reduxStore={reduxStore} persistor={persistor} />;
};
};
Does anyone have an idea as to why I am getting the following error, 'initialReduxState is not defined'? Thanks!

How to fix TypeError: navigation.setOptions is not a function

I'm trying to implement react native test library with jest to my app.
For now I have a problem with the navigation on my component.
When I'm running the test, I've got an error :
TypeError: navigation.setOptions is not a function
Here is my component:
const initialState: StateTypes = {
toShowAsGridLayout: false,
isLoadingMoreContacts: false
};
export const Main: FC<Props> = ({ navigation }) => {
const dispatch = useDispatch();
const { data } = useSelector((state: RootState) => state.appReducer);
const [state, setState] = useState<StateTypes>(initialState);
useLayoutEffect(() => {
navigation.setOptions({
title: state.isLoadingMoreContacts ? strings.LOADING : strings.ALL_CONTACTS + ' - ' + data.length,
headerRight: () => (
<TouchableOpacity style={styles.changeLayoutButton} onPress={changeLayout}>
<Text style={styles.changeLayoutText}>{state.toShowAsGridLayout ? strings.LIST : strings.GRID}</Text>
</TouchableOpacity>
)
});
}, [state.isLoadingMoreContacts, state.toShowAsGridLayout])
return (
<View style={styles.container}>
{renderLayout()}
</View>
);
};
Here is a router:
const SplashStack = createStackNavigator();
const MainStack = createStackNavigator();
export const RootNavigator = () => {
const { isDataLoading } = useSelector((state: RootState) => state.appReducer);
return (
isDataLoading
? <SplashStack.Navigator>
<SplashStack.Screen name={'SplashStack'} component={Splash} />
</SplashStack.Navigator>
: <MainStack.Navigator>
<MainStack.Screen name={'Main'} component={Main} />
<MainStack.Screen name={'ContactDetails'} component={ContactDetails} />
</MainStack.Navigator>
);
};
And a test itself:
import React from 'react';
import { render } from '#testing-library/react-native';
import { Main } from '../Main';
import * as redux from 'react-redux';
import strings from '../../constants/strings';
import mocks from '../../mocks';
describe('dispatch mock', () => {
it('should dispatch mock', () => {
const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
const useSelectorSpy = jest.spyOn(redux, 'useSelector');
const mockDispatchFn = jest.fn();
useDispatchSpy.mockReturnValue(mockDispatchFn);
useSelectorSpy.mockReturnValue({ data: mocks });
const { getByText } = render(<Main navigation={({})} />);
getByText(strings.ALL_CONTACTS);
});
});
How can i fix this error ? What should I pass to navigation props in line :
const { getByText } = render(<Main navigation={({})} />);
You need to pass object with setOptions method.
const { getByText } = render(<Main navigation={
{
setOptions: (props: { title: string, headerRight: React.FC }) => void
}
} />);
This answer might be relevant
For my purpose solution was very easy, I just added ?. at the end of setOptions
useLayoutEffect(() => {
navigation.setOptions?.({ // <--- CHANGED HERE
title: state.isLoadingMoreContacts ? strings.LOADING : strings.ALL_CONTACTS + ' - ' + data.length,
headerRight: () => (
<TouchableOpacity style={styles.changeLayoutButton} onPress={changeLayout}>
<Text style={styles.changeLayoutText}>{state.toShowAsGridLayout ? strings.LIST : strings.GRID}</Text>
</TouchableOpacity>
)
});
}, [state.isLoadingMoreContacts, state.toShowAsGridLayout])
I am also face the same issue. With below code changes I have resolved my issue.
For more info please check this answer
const createTestProps = (props: Object) => ({
navigation: {
navigate: jest.fn(),
setOptions: jest.fn()
},
...props
});
const props = createTestProps({});
const { toJSON } = render(<MainScreen {...props} />);
You have to mock the properties and function that your are using in your screen has to be mocked like above. For more info please check this answer

Using React Context with AsyncStorage using hooks

My goal is to use custom hooks created from Context to pass and modify stored values
The final goal is to use something like useFeedContext() to get or modify the context values
What I am actually getting is either the functions that I call are undefined or some other problem ( I tried multiple approaches)
I tried following this video basics of react context in conjunction with this thread How to change Context value while using React Hook of useContext but I am clearly getting something wrong.
Here is what I tried :
return part of App.js
<FeedProvider mf={/* what do i put here */}>
<Navigation>
<HomeScreen />
<ParsedFeed />
<FavScreen />
</Navigation>
</FeedProvider>
Main provider logic
import React, { useState, useEffect, useContext, useCallback } from "react";
import AsyncStorage from "#react-native-async-storage/async-storage";
const FeedContext = React.createContext();
const defaultFeed = [];
const getData = async (keyName) => {
try {
const jsonValue = await AsyncStorage.getItem(keyName);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (e) {
console.log(e);
}
};
const storeData = async (value, keyName) => {
console.log(value, keyName);
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(keyName, jsonValue);
} catch (e) {
console.log(e);
}
};
export const FeedProvider = ({ children, mf }) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
const [feedLoaded, setFeedLoaded] = useState(false);
let load = async () => {
let temp = await AsyncStorage.getItem("mainUserFeed");
temp != null
? getData("mainUserFeed").then((loadedFeed) => setMainFeed(loadedFeed))
: setMainFeed(defaultFeed);
setFeedLoaded(true);
};
useEffect(() => {
load();
}, []);
useCallback(async () => {
if (!feedLoaded) {
return await load();
}
}, [mainFeed]);
const setFeed = (obj) => {
setMainFeed(obj);
storeData(mainFeed, "mainUserFeed");
};
return (
<FeedContext.Provider value={{ getFeed: mainFeed, setFeed }}>
{children}
</FeedContext.Provider>
);
};
//export const FeedConsumer = FeedContext.Consumer;
export default FeedContext;
The custom hook
import { useContext } from "react";
import FeedContext from "./feedProviderContext";
export default function useFeedContext() {
const context = useContext(FeedContext);
return context;
}
What I would hope for is the ability to call the useFeedContext hook anywhere in the app after import like:
let myhook = useFeedContext()
console.log(myhook.getFeed) /// returns the context of the mainFeed from the provider
myhook.setFeed([{test:1},{test:2}]) /// would update the mainFeed from the provider so that mainFeed is set to the passed array with two objects.
I hope this all makes sense, I have spend way longer that I am comfortable to admit so any help is much appreciated.
If you want to keep using your useFeedContext function, I suggest to move it into the your 'Provider Logic' or I'd call it as 'FeedContext.tsx'
FeedContext.tsx
const FeedContext = createContext({});
export const useFeedContext = () => {
return useContext(FeedContext);
}
export const AuthProvider = ({children}) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
...
return (
<FeedContext.Provider value={{mainFeed, setMainFeed}}>
{children}
</FeedContext.Provider>
);
};
YourScreen.tsx
const YourScreen = () => {
const {mainFeed, setMainFeed} = useFeedContext();
useEffect(() => {
// You have to wait until mainFeed is defined, because it's asynchronous.
if (!mainFeed || !mainFeed.length) {
return;
}
// Do something here
...
}, [mainFeed]);
...
return (
...
);
};
export default YourScreen;

React Native HOC component

I am trying to do a withCache HoC component but having some problems...
Thats the HoC:
// HOC for cached images
const withCache = (Component) => {
const Wrapped = (props) => {
console.log(props);
const [uri, setUri] = useState(null);
useEffect(() => {
(async () => {
const { uri } = props;
const name = shorthash.unique(uri);
const path = `${FileSystem.cacheDirectory}${name}`;
const image = await FileSystem.getInfoAsync(path);
if (image.exists) {
console.log("Read image from cache");
setUri(image.uri);
return;
} else {
console.log("Downloading image to cache");
const newImage = await FileSystem.downloadAsync(uri, path);
setUri(newImage.uri);
}
})();
}, []);
return <Component {...props} uri={uri} />;
};
Wrapped.propTypes = Component.propTypes;
return Wrapped;
};
export default withCache;
The thing is that "Component" is a custom Image component with specific propTypes and defaultProps.
How do I use this component? I have tried:
const CachedImage = withCache(<MyCustomImage uri={"https://..."} height={100} ripple />)
...
return (<CachedImage />)
but not working :( What I want is to pass a boolean prop to my custom image component named "cached", and if true return the custom image component wrapped in the HOC
In order to use the HOC, you would create the instance outside of the functional component like
const CachedImage = withCache(MyCustomImage)
and use it like
const MyComp = () => {
...
return (<CachedImage uri={"https://..."} height={100} ripple />)
}
Final implementation of the HoC, in case someone finds it useful in the future.
import React, { useState, useEffect } from "react";
import shorthash from "shorthash";
import * as FileSystem from "expo-file-system";
// HOC for cached images
const withCache = (Component) => {
const Wrapped = (props) => {
console.log(props);
const [uri, setUri] = useState(null);
useEffect(() => {
(async () => {
const { uri } = props;
const name = shorthash.unique(uri);
const path = `${FileSystem.cacheDirectory}${name}`;
const image = await FileSystem.getInfoAsync(path);
if (image.exists) {
console.log("Read image from cache");
setUri(image.uri);
return;
} else {
console.log("Downloading image to cache");
const newImage = await FileSystem.downloadAsync(uri, path);
setUri(newImage.uri);
}
})();
}, []);
// Needs to have the final uri before render the image
return uri && <Component {...props} uri={uri} />;
};
Wrapped.propTypes = Component.propTypes;
return Wrapped;
};
export default withCache;

Using the Context API gives me undefined

So I'm using Auth0 for my user sign up. I'm trying to get the user id under sub:value to add to my database to identify with the post of a user. I'm trying to use a Context API in order to get the user info to put in my database.
react-auth0-spa.js
// src/react-auth0-spa.js
import React, { useState, useEffect, useContext } from "react";
import createAuth0Client from "#auth0/auth0-spa-js";
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
export const Auth0Context = React.createContext();
export const useAuth0 = () => useContext(Auth0Context);
export const Auth0Provider = ({
children,
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
...initOptions
}) => {
const [isAuthenticated, setIsAuthenticated] = useState();
const [user, setUser] = useState();
const [auth0Client, setAuth0] = useState();
const [loading, setLoading] = useState(true);
const [popupOpen, setPopupOpen] = useState(false);
useEffect(() => {
const initAuth0 = async () => {
const auth0FromHook = await createAuth0Client(initOptions);
setAuth0(auth0FromHook);
if (window.location.search.includes("code=") &&
window.location.search.includes("state=")) {
const { appState } = await auth0FromHook.handleRedirectCallback();
onRedirectCallback(appState);
}
const isAuthenticated = await auth0FromHook.isAuthenticated();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const user = await auth0FromHook.getUser();
setUser(user);
}
setLoading(false);
};
initAuth0();
// eslint-disable-next-line
}, []);
const loginWithPopup = async (params = {}) => {
setPopupOpen(true);
try {
await auth0Client.loginWithPopup(params);
} catch (error) {
console.error(error);
} finally {
setPopupOpen(false);
}
const user = await auth0Client.getUser();
setUser(user);
setIsAuthenticated(true);
};
const handleRedirectCallback = async () => {
setLoading(true);
await auth0Client.handleRedirectCallback();
const user = await auth0Client.getUser();
setLoading(false);
setIsAuthenticated(true);
setUser(user);
};
return (
<Auth0Context.Provider
value={{
isAuthenticated,
user,
loading,
popupOpen,
loginWithPopup,
handleRedirectCallback,
getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
logout: (...p) => auth0Client.logout(...p)
}}
>
{children}
</Auth0Context.Provider>
);
};
other.js (trying to get user info from react-auth0-spa.js)
class AddAlbum extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
let value = this.context;
console.log(value);
}
render() {
return (
)
}
AddAlbum.contextType = Auth0Context;
This gives me user: undefined
In my index.js I have this
ReactDOM.render(
<Auth0Provider
domain={config.domain}
client_id={config.clientId}
redirect_uri={window.location.origin}
onRedirectCallback={onRedirectCallback}
>
<App />
</Auth0Provider>,
document.getElementById("root")
);
Which I believe is giving me these results:
So I'm wondering why my Context API isn't working and giving me user: undefined.
You're logging the user when the component first mounts, which is long before the await auth0FromHook.getUser() call will complete. Log it in a componentDidUpdate, or check in a parent if that value is available, and don't mount the child component until it is.

Categories

Resources