Jest Redux Persist: TypeError: Cannot read property 'catch' of undefined at writeStagedState - react-native

I'm trying to test my LoginScreen with Jest and Typescript. I use redux and redux-persist for storage and have set the storage up to use AsyncStorage as part of the config. I suspect that redux-persist is attempting to rehydrate after the built-in time-out function it uses runs out and tries to set storage to default storage? I'm getting the following error:
console.error
redux-persist: rehydrate for "root" called after timeout. undefined
undefined
at _rehydrate (node_modules/redux-persist/lib/persistReducer.js:70:71)
at node_modules/redux-persist/lib/persistReducer.js:102:11
at tryCallOne (node_modules/promise/setimmediate/core.js:37:12)
at Immediate._onImmediate (node_modules/promise/setimmediate/core.js:123:15)
Currently my test looks like this:
describe('Testing LoginScreen', () => {
it('should render correctly', async () => {
const { toJSON } = render(<MockedNavigator component={LoginScreen} />);
await act(async () => await flushMicrotasksQueue());
expect(toJSON()).toMatchSnapshot();
});
});
and my MockNavigator looks like this:
type MockedNavigatorProps = {
component: React.ComponentType<any>;
params?: {};
};
const Stack = createStackNavigator();
const MockedNavigator = (props: MockedNavigatorProps) => {
return (
<MockedStorage>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name='MockedScreen'
component={props.component}
initialParams={props.params}
/>
</Stack.Navigator>
</NavigationContainer>
</MockedStorage>
);
};
export default MockedNavigator;
Here is the way I'm creating my storage:
import 'react-native-gesture-handler';
import * as React from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from '../src/AppState/store';
type MockedStorageProps = {
children: any;
};
const MockedStorage = (props: MockedStorageProps) => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
{props.children}
</PersistGate>
</Provider>
);
};
export default MockedStorage;

I resolved this same error using this advice from an issue on the redux-persist repo: https://github.com/rt2zz/redux-persist/issues/1243#issuecomment-692609748.
(It also had the side-effect of avoiding logging errors in test from redux-logger.)
jest.mock('redux-persist', () => {
const real = jest.requireActual('redux-persist');
return {
...real,
persistReducer: jest
.fn()
.mockImplementation((config, reducers) => reducers),
};
});
#alexbrazier:
It basically just bypasses redux-persist by returning the reducers
directly without wrapping them in redux-persist.

Related

React Native createContext() and useContext() returning null

I have tried looking at other posts with similar errors but I can't manage to find one that makes it work as expected.
AuthContext.js
import React from "react";
const AuthContext = React.createContext();
export default AuthContext;
const AuthContextProvider = ({ children }) => {
const authContext = React.useMemo(
() => ({
signIn: async (data) => {
await AsyncStorage.setItem('userToken', data.token);
await AsyncStorage.setItem('user', JSON.stringify(data));
dispatch({type: 'SIGN_IN', token: data.token, user: data});
},
signOut: async () => {
await AsyncStorage.removeItem('userToken');
await AsyncStorage.removeItem('user');
dispatch({type: 'SIGN_OUT'});
}
}),
[]
);
return (
<AuthContext.Provider
value={{
authContext
}}
>
{children}
</AuthContext.Provider>
);
};
Then in App.js
import { AuthContextProvider } from './AuthContext';
....
return (
<PaperProvider theme={theme}>
<AuthContextProvider>
<SafeAreaProvider>
<NavigationContainer>
<DetailsScreen />
</NavigationContainer>
</SafeAreaProvider>
</AuthContextProvider>
</PaperProvider>
);
Then in DetailsScreen.js
import { AuthContext } from "../AuthContext";
constructor(props) {
const {context} = useContext(AuthContext);
console.log("-----------------------------", context); // returns undefined
super(props, context);
}
The error this block of code is causing is:
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined.
I am out of ideas as to what could be wrong.
The context AuthContext that you created in AuthContext.js exports the context as default, but you import it as named-import, ie using import {} from. So the useContext hook takes as an argument a null instead of the actual context created by React.createContext()
Then be careful that const {context} = useContext(AuthContext); is also wrong as the hook will return the object {authContext: {...}} which means that you have to do const {authContext} = useContext(AuthContext);
In the Provider you can avoid passing value={{authContext}} and instead pass value={authContext} then you can just const authContext = useContext(AuthContext);

React initial value of createContext is used instead of provided one

I'm creating a Context with the boolen isDark inside my App. The boolean isDark is created with useState and I provide this boolean and a function to change the boolean to a ThemeContext to access it further down the component tree.
Down below I'm creating the ThemeContext with the boolean initialized to false and a function that just warns in the console that the initial value is being used:
//ThemeContext.tsx
export type ContextType = {
isDark: boolean
toggleTheme: () => void
}
const ThemeContext = createContext<ContextType>({
isDark: false,
toggleTheme: () => console.warn('Still using initial value'),
})
export const useTheme = () => useContext(ThemeContext)
export default ThemeContext
Here I'm providing the theme and the functionality to change it through the toggleTheme function:
//CustomThemeProvider.tsx
export const CustomThemeProvider: React.FC = ({ children }) => {
const [isDark, setDark] = useState(false)
const toggleTheme = () => {
console.log('Change theme')
setDark(!isDark)
}
const providerTheme = useMemo(
() => ({ isDark, toggleTheme }),
[isDark, toggleTheme],
)
return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<ThemeContext.Provider value={providerTheme}>
{children}
</ThemeContext.Provider>
</ThemeProvider>
)
}
I now want to access the boolean and the toggleTheme function and do that through my custom hook (useTheme) created at the start, that just uses useContext:
//App.tsx
export default function App() {
const { isDark, toggleTheme } = useTheme()
return (
<CustomThemeProvider>
<Box flex={1} justifyContent="center">
<Paper title="Test Title">
<Switch onValueChange={toggleTheme} value={isDark} />
</Box>
</CustomThemeProvider>
)
}
When I now try to switch the theme with the Switch component (React Native), I get the console warning that my initial function is being called. That means that my toggleTheme function is still the initial function () => console.warn('Still using initial value') even though I provided a new function, that should change the isDark boolean with my ThemeContext.Provider.
Why is my inital function still being called by the Switch instead of my provided one to change the theme?
Your useTheme() is getting the value from the default state since a Provider above it in the component tree is not found (it is at the same level).
Just wrap your application with your CustomThemeProvider (or a level above):
ReactDOM.render(
<CustomThemeProvider>
<App />
</CustomThemeProvider>,
document.getElementById('root')
);
Be careful too with the setDark(!isDark), you should implement it getting the previous state setDark(state => !state) since setting the state is deferred until re-render.
Working Stackblitz
By the way, <ThemeProvider theme={isDark ? darkTheme : lightTheme}>, is that line a typo? If you are trying to split the Context in two (value and dispatch, which it is a nice idea), I would do it as follows:
const ThemeContext = createContext({
isDark: false
});
export const useTheme = () => useContext(ThemeContext);
export default ThemeContext;
const ToggleThemeContext = createContext({
toggleTheme: () => console.warn('Still using initial value')
});
export const useToggleTheme = () => useContext(ToggleThemeContext);
export default ToggleThemeContext;
//CustomThemeProvider.tsx
export const CustomThemeProvider = ({ children }) => {
const [isDark, setDark] = useState(false);
const memoToggleTheme = useCallback(() => setDark(state => !state), [
setDark
]);
return (
<ToggleThemeContext.Provider value={memoToggleTheme}>
<ThemeContext.Provider value={isDark}>{children}</ThemeContext.Provider>
</ToggleThemeContext.Provider>
);
};
Working Stackblitz memoizing the component which dispatches the action because otherwise it will be re-rendered by the App component when the theme changes.
By doing that only the component that uses the value will be re-rendered.
Let me link you an article I wrote the last day about everything related to React Context, including optimization React Context, All in One

Could not find "store" in the context of "Connect(HomeScreen)". Either wrap the root component in a... or pass a custom React context provider

Check multitude of questioned already asked and but still can't figure this one out.
We are rewriting our authentication layer using
export default AuthContext = React.createContext();
and wrapping it around our AppNavigator
function AppNavigator(props) {
const [state, dispatch] = useReducer(accountReducer, INITIAL_STATE);
const authContext = React.useMemo(
() => ({
loadUser: async () => {
const token = await keychainStorage.getItem("token");
if (token) {
await dispatch({ type: SIGN_IN_SUCCESS, token: token });
}
},
signIn: async (data) => {
client
.post(LOGIN_CUSTOMER_RESOURCE, data)
.then((res) => {
const token = res.data.accessToken;
keychainStorage.setItem("token", token);
dispatch({ type: SIGN_IN_SUCCESS, token: token });
})
.catch((x) => {
dispatch({ type: SIGN_IN_FAIL });
});
},
signOut: () => {
client.delete({
LOGOUT_CUSTOMER_RESOURCE
});
dispatch({ type: SIGN_OUT_SUCCESS });
}
}),
[]
);
console.log("token start", state.token);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer
theme={MyTheme}
ref={(navigatorRef) => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
onStateChange={(state) => {
NavigationService.setAnalytics(state);
}}
>
<AppStack.Navigator initialRouteName="App" screenOptions={hideHeader}>
{state.token != null ? (
<AppStack.Screen name="App" component={AuthMainTabNavigator} />
) : (
<>
<AppStack.Screen name="App" component={MainTabNavigator} />
<AppStack.Screen name="Auth" component={AuthNavigator} />
</>
)}
</AppStack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}
export default AppNavigator;
App.js - render fucnction
<Root>
<StoreProvider store={store} context={AuthContext}>
<PersistGate loading={null} persistor={persistor}>
<SafeAreaProvider>
<AppNavigator context={AuthContext}/>
</SafeAreaProvider>
</PersistGate>
</StoreProvider>
</Root>
HomeScreen.js
export default connect(mapStateToProps, mapDispatchToProps, null, { context: AuthContext })(HomeScreen);
But still receiving
Error: Could not find "store" in the context of "Connect(HomeScreen)". Either wrap the root component in a <Provider>, or pass a custom React context provider to <Provider> and the corresponding React context consumer to Connect(HomeScreen) in connect options.
We have gone through the REDUX documentation:
https://react-redux.js.org/using-react-redux/accessing-store#using-the-usestore-hook
Simply can not work out why we are receiving this error.
I'm not sure what you're trying to accomplish here, but this is very wrong:
export default connect(mapStateToProps, mapDispatchToProps, null, { context: AuthContext })(HomeScreen);
It looks like you're mixing up two different things. You're trying to create a context for use with your own auth state, but you're also trying to use that same context instance to override React-Redux's own default context instance. Don't do that! You should not be passing a custom context instance to connect and <Provider> except in very rare situations.
I understand what you are trying to achieve only after reading through your discussion in the comments with #markerikson.
The example from the React Navigation docs creates a context AuthContext in order to make the auth functions available to its descendants. It needs to do this because the state and the dispatch come from the React.useReducer hook so they only exist within the scope of the component.
Your setup is different because you are using Redux. Your state and dispatch are already available to your component through the React-Redux context Provider and can be accessed with connect, useSelector, and useDispatch. You do not need an additional context to store your auth info.
You can work with the context that you already have using custom hooks. Instead of using const { signIn } = React.useContext(AuthContext) like in the example, you can create a setup where you would use const { signIn } = useAuth(). Your useAuth hook can access your Redux store by using the React-Redux hooks internally.
Here's what that code looks like as a hook:
import * as React from 'react';
import * as SecureStore from 'expo-secure-store';
import { useDispatch } from "react-redux";
export const useAuth = () => {
// access dispatch from react-redux
const dispatch = useDispatch();
React.useEffect(() => {
// same as in example
}, []);
// this is the same as the example too
const authContext = useMemo(
() => ({
signIn: async data => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async data => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
// but instead of passing that `authContext` object to a `Provider`, just return it!
return authContext;
}
In your component, which must be inside your React-Redux <Provider>:
function App() {
const { signIn } = useAuth();
const [username, setUsername] = React.useState('');
return (
<Button onPress={() => signIn(username)}>Sign In</Button>
)
}

Multiple React-dnd jest tests "Cannot have two HTML5 backends at the same time"

I have a jest test file with a number of tests in it.
import React from 'react';
import configureStore from 'redux-mock-store';
import {Provider} from "react-redux";
import renderer from "react-test-renderer";
import HTML5Backend from "react-dnd-html5-backend";
import {DragDropContextProvider} from "react-dnd";
describe('My Component Tests', () => {
let mockStore;
let store;
beforeEach(() => {
mockStore = configureStore();
store = mockStore(mockData);
});
test(' test1', () => {
const cmpt = <Provider store={store}>
<DragDropContextProvider backend={HTML5Backend}>
<MyComponent state={1}/>
</DragDropContextProvider>
</Provider>;
const tree = renderer.create(cmpt).toJSON();
expect(tree).toMatchSnapshot();
});
test(' test2', () => {
const cmpt = <Provider store={store}>
<DragDropContextProvider backend={HTML5Backend}>
<MyComponent state={2}/>
</DragDropContextProvider>
</Provider>;
const tree = renderer.create(cmpt).toJSON();
expect(tree).toMatchSnapshot();
});
});
The first test always work
But the subsequent ones always come up with this error :
Error: Uncaught [Error: Cannot have two HTML5 backends at the same time.]
I am guessing this is because the HTMLBackend is treated as a singleton, and is used across tests which is not what I want. I want tests which run independantly.
Is there some was of creating an instance of the HTMLBackend in the beforeEach() function.
I have tried to encapsule the HTML5Backend into a singleton, but I get the same Problem :
let html5Backend = null;
function getSingleton() {
if (!html5Backend) {
html5Backend = HTML5Backend;
debugger;
}
return html5Backend;
}
I solved this by referencing HTMLBackend instance in the "describe" level, like so :
describe('My Component Tests', () => {
let mockStore;
let store;
let htmlbe = HTML5Backend; //reference instance here!!!
beforeEach(() => {
mockStore = configureStore();
store = mockStore(mockData);
});
test(' test1', () => {
const cmpt = <Provider store={store}>
<DragDropContextProvider backend={htmlbe }>
<MyComponent state={1}/>
</DragDropContextProvider>
</Provider>;
const tree = renderer.create(cmpt).toJSON();
expect(tree).toMatchSnapshot();
});
test(' test2', () => {
const cmpt = <Provider store={store}>
<DragDropContextProvider backend={htmlbe }>
<MyComponent state={2}/>
</DragDropContextProvider>
</Provider>;
const tree = renderer.create(cmpt).toJSON();
This is the equivalent of having a singleton across all tests.

Cannot listen for a key that isn't associated with a Redux Store - React Navigtion

I just upgraded my React Navigation to version 1.0.0. They have new ways to integrate the navigation and Redux. Here's my code
configureStore.js
export default (rootReducer, rootSaga) => {
const middleware = []
const enhancers = []
/* ------------- Analytics Middleware ------------- */
middleware.push(ScreenTracking)
const sagaMiddleware = createSagaMiddleware({ sagaMonitor })
middleware.push(sagaMiddleware)
const navMiddleware = createReactNavigationReduxMiddleware('root', state => state.nav)
middleware.push(navMiddleware)
/* ------------- Assemble Middleware ------------- */
enhancers.push(applyMiddleware(...middleware))
/* ------------- AutoRehydrate Enhancer ------------- */
// add the autoRehydrate enhancer
if (ReduxPersist.active) {
enhancers.push(autoRehydrate())
}
const store = createAppropriateStore(rootReducer, compose(...enhancers))
// kick off root saga
sagaMiddleware.run(rootSaga)
return store
}
ReduxNavigation.js
const addListener = createReduxBoundAddListener('root')
// here is our redux-aware our smart component
function ReduxNavigation (props) {
const { dispatch, nav } = props
const navigation = ReactNavigation.addNavigationHelpers({
dispatch,
state: nav,
uriPrefix: prefix,
addListener
})
return <AppNavigation navigation={navigation} />
}
const mapStateToProps = state => ({ nav: state.nav })
export default connect(mapStateToProps)(ReduxNavigation)
ReduxIndex.js
export default () => {
/* ------------- Assemble The Reducers ------------- */
const rootReducer = combineReducers({
//few reducers
})
return configureStore(rootReducer, rootSaga)
}
App.js
const store = createStore()
class App extends Component {
render () {
console.disableYellowBox = true
return (
<Provider store={store}>
<RootContainer />
</Provider>
)
}
}
export default App
And I got an error of
Cannot listen for a key that isn't associated with a Redux store. First call createReactNavigationReduxMiddleware so that we know when to trigger your listener
I hope someone can help me and please let me know if you needed more information
Thanks
It is clearly mentioned in the react-navigation docs that the Note: createReactNavigationReduxMiddleware must be run before createReduxBoundAddListener.
Whenever you do use the module after importing it, the listener is being called before the store is initialized.
So the simple fix is put the addListener in the ReduxNavigation function as
// here is our redux-aware our smart component
function ReduxNavigation (props) {
const addListener = createReduxBoundAddListener('root')
const { dispatch, nav } = props
const navigation = ReactNavigation.addNavigationHelpers({
dispatch,
state: nav,
uriPrefix: prefix,
addListener
})
return <AppNavigation navigation={navigation} />
}
const mapStateToProps = state => ({ nav: state.nav })
export default connect(mapStateToProps)(ReduxNavigation)
or you may make a wrapper class to the current class and bind the store to it as here
class RootContainer extends Component {
render () {
return (
<View style={{flex: 1, backgroundColor: '#fff'}}>
<StatusBar translucent barStyle='dark-content' backgroundColor='#fff' />
<ReduxNavigation/>
</View>
)
}
}
class App extends Component {
render () {
console.disableYellowBox = true
return (
<Provider store={store}>
<RootContainer />
</Provider>
)
}
}
I have made a sample starter kit for the same.Please checkout the link below
Sample Starter Kit
For those who struggle with it, be sure the import class in your App.js are first
import configureStore from '../Redux/configureStore'
(where you configure your Navigation Middleware)
and second or after:
import ReduxNavigation from '../Navigation/ReduxNavigation'
(where you call createReduxBoundAddListener )
Otherwise you'll keep having this message