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
Related
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);
I'm new to React Native, and my understanding is that functional components and hooks are the way to go. What I'm trying to do I've boiled down to the simplest case I can think of, to use as an example. (I am, by the way, writing in TypeScript.)
I have two Independent components. There is no parent-child relationship between the two. Take a look:
The two components are a login button on the navigation bar and a switch in the enclosed screen. How can I make the login button be enabled when the switch is ON and disabled when the switch is OFF?
The login button looks like this:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const handleClick = () => {
navigation.navigate('Away');
};
// I want the 'disabled' value to update based on the state of the switch.
return (
<Button title="Login"
color="white"
disabled={false}
onPress={handleClick} />
);
};
As you can see, right now I've simply hard-coded the disabled setting for the button. I'm thinking that will no doubt change to something dynamic.
The screen containing the switch looks like this:
const HomeScreen = () => {
const [isEnabled, setEnabled] = useState(false);
const toggleSwitch = () => setEnabled(value => !value);
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={isEnabled}
/>
</SafeAreaView>
);
};
What's throwing me for a loop is that the HomeScreen and LoginButton are setup like this in the navigator stack. I can think of no way to have the one "know" about the other:
<MainStack.Screen name="Home"
component={HomeScreen}
options={{title: "Home", headerRight: LoginButton}} />
I need to get the login button component to re-render when the state of the switch changes, but I cannot seem to trigger that. I've tried to apply several different things, all involving hooks of some kind. I have to confess, I think I'm missing at least the big picture and probably some finer details too.
I'm open to any suggestion, but really I'm wondering what the simplest, best-practice (or thereabouts) solution is. Can this be done purely with functional components? Do I have to introduce a class somewhere? Is there a "notification" of sorts (I come from native iOS development). I'd appreciate some help. Thank you.
I figured out another way of tracking state, for this simple example, that doesn't involve using a reducer, which I'm including here for documentation purposes in hopes that it may help someone. It tracks very close to the accepted answer.
First, we create both a custom hook for the context, and a context provider:
// FILE: switch-context.tsx
import React, { SetStateAction } from 'react';
type SwitchStateTuple = [boolean, React.Dispatch<SetStateAction<boolean>>];
const SwitchContext = React.createContext<SwitchStateTuple>(null!);
const useSwitchContext = (): SwitchStateTuple => {
const context = React.useContext(SwitchContext);
if (!context) {
throw new Error(`useSwitch must be used within a SwitchProvider.`);
}
return context;
};
const SwitchContextProvider = (props: object) => {
const [isOn, setOn] = React.useState(false);
const [value, setValue] = React.useMemo(() => [isOn, setOn], [isOn]);
return (<SwitchContext.Provider value={[value, setValue]} {...props} />);
};
export { SwitchContextProvider, useSwitchContext };
Then, in the main file, after importing the SwitchContextProvider and useSwitchContext hook, wrap the app's content in the context provider:
const App = () => {
return (
<SwitchContextProvider>
<NavigationContainer>
{MainStackScreen()}
</NavigationContainer>
</SwitchContextProvider>
);
};
Use the custom hook in the Home screen:
const HomeScreen = () => {
const [isOn, setOn] = useSwitchContext();
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={setOn}
value={isOn}
/>
</SafeAreaView>
);
};
And in the Login button component:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const [isOn] = useSwitchContext();
const handleClick = () => {
navigation.navigate('Away');
};
return (
<Button title="Login"
color="white"
disabled={!isOn}
onPress={handleClick} />
);
};
I created the above by adapting an example I found here:
https://kentcdodds.com/blog/application-state-management-with-react
The whole project is now up on GitHub, as a reference:
https://github.com/software-mariodiana/hellonavigate
If you want to choose the context method, you need to create a component first that creates our context:
import React, { createContext, useReducer, Dispatch } from 'react';
type ActionType = {type: 'TOGGLE_STATE'};
// Your initial switch state
const initialState = false;
// We are creating a reducer to handle our actions
const SwitchStateReducer = (state = initialState, action: ActionType) => {
switch(action.type){
// In this case we only have one action to toggle state, but you can add more
case 'TOGGLE_STATE':
return !state;
// Return the current state if the action type is not correct
default:
return state;
}
}
// We are creating a context using React's Context API
// This should be exported because we are going to import this context in order to access the state
export const SwitchStateContext = createContext<[boolean, Dispatch<ActionType>]>(null as any);
// And now we are creating a Provider component to pass our reducer to the context
const SwitchStateProvider: React.FC = ({children}) => {
// We are initializing our reducer with useReducer hook
const reducer = useReducer(SwitchStateReducer, initialState);
return (
<SwitchStateContext.Provider value={reducer}>
{children}
</SwitchStateContext.Provider>
)
}
export default SwitchStateProvider;
Then you need to wrap your header, your home screen and all other components/pages in this component. Basically you need to wrap your whole app content with this component.
<SwitchStateProvider>
<AppContent />
</SwitchStateProvider>
Then you need to use this context in your home screen component:
const HomeScreen = () => {
// useContext returns an array with two elements if used with useReducer.
// These elements are: first element is your current state, second element is a function to dispatch actions
const [switchState, dispatchSwitch] = useContext(SwitchStateContext);
const toggleSwitch = () => {
// Here, TOGGLE_STATE is the action name we have set in our reducer
dispatchSwitch({type: 'TOGGLE_STATE'})
}
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={switchState}
/>
</SafeAreaView>
);
};
And finally you need to use this context in your button component:
// We are going to use only the state, so i'm not including the dispatch action here.
const [switchState] = useContext(SwitchStateContext);
<Button title="Login"
color="white"
disabled={!switchState}
onPress={handleClick} />
Crete a reducer.js :
import {CLEAR_VALUE_ACTION, SET_VALUE_ACTION} from '../action'
const initialAppState = {
value: '',
};
export const reducer = (state = initialAppState, action) => {
if (action.type === SET_VALUE_ACTION) {
state.value = action.data
}else if(action.type===CLEAR_VALUE_ACTION){
state.value = ''
}
return {...state};
};
Then action.js:
export const SET_VALUE_ACTION = 'SET_VALUE_ACTION';
export const CLEAR_VALUE_ACTION = 'CLEAR_VALUE_ACTION';
export function setValueAction(data) {
return {type: SET_VALUE_ACTION, data};
}
export function clearValueAction() {
return {type: CLEAR_VALUE_ACTION}
}
In your components :
...
import {connect} from 'react-redux';
...
function ComponentA({cartItems, dispatch}) {
}
const mapStateToProps = (state) => {
return {
value: state.someState,
};
};
export default connect(mapStateToProps)(ComponentA);
You can create more components and communicate between them, independently.
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>
)
}
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.
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