I am quite sure a lot of React and React Native devs are familiar with using Redux to manage application state. A few months back I wrote an article on how you can use context in place of Redux for managing global state in React. It's good practice to always keep state as close to where it is needed as possible and this is quite easy to achieve with React because of the pretty simple API of react router. On the other hand, this practice can pose some difficulty with React Native because of the quite complex API of React Navigation. Although there are other alternatives for navigation in React Native such as react router native, React Navigation appears to be the most commonly used navigation library in react native. So, here is a way devs could structure their context providers in react native:
// placing all providers in the app's root
<AuthContext.provider value={authValue}>
<ArticleContext.provider value={articleValue}>
<UserContext.provider value={userValue}>
<Navigator />
</UserContext.provider>
</ArticleContext.provider>
</AuthContext.provider>
Let's assume Navigator is the navigation component that routes to all other components in the App, then having your context providers setup like above could have a negative impact on your app's performance because it means the entire app will rerender when any of the providers value changes including components that do not need or make use of this update. In this article, I will show us a pretty neat way we can set up our navigation and context so that components are only rendered under providers that they need updates from.
In our example app, we will have users context, articles context and auth context. I will eventually shed light on the articles component to show how we can consume context.
Creating Contexts
We will start by creating our various contexts. Instead of using my providers directly, I love to abstract them in other components I term as 'controllers'. This makes it easy to isolate and modify the logic for creating and updating context value. The controllers return our providers
This is the content of our auth context:
import React, { useReducer, useMemo } from 'react';
import PropTypes from 'prop-types';
const initialState = {
loggedIn: false,
user: {}
};
const initialContext = [{ ...initialState }, () => {}];
export const AuthContext = React.createContext(initialContext);
const updater = (state, update) => {
return { ...state, ...update };
};
export function AuthController(props) {
const [authState, updateAuth] = useReducer(updater, initialState);
const value = useMemo(() => [authState, updateAuth], [authState]);
return (<AuthContext.Provider value={value}>
{props.children}
</AuthContext.Provider>);
}
AuthController.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
For user context, we have:
import React, { useReducer, useMemo } from 'react';
import PropTypes from 'prop-types';
const initialState = {
user: {}
};
const initialContext = [{ ...initialState }, () => {}];
export const UserContext = React.createContext(initialContext);
const updater = (state, update) => {
return { ...state, ...update };
};
export function UserController(props) {
const [userState, updateUser] = useReducer(updater, initialState);
const value = useMemo(() => [userState, updateUser], [userState]);
return (<UserContext.Provider value={value}>
{props.children}
</UserContext.Provider>);
}
UserController.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
and lastly, the article context:
import React, { useReducer, useMemo } from 'react';
import PropTypes from 'prop-types';
const initialState = {
articles: []
};
const initialContext = [{ ...initialState }, () => {}];
export const ArticleContext = React.createContext(initialContext);
const reducer = (state, action) => {
switch (action.type) {
case "get":
return {...state, articles: action.articles }
case "add":
return { ...state, articles: [...state.articles, action.article] };
case "delete":
const articles = [...state.articles];
const filteredArticles = articles.filter(article => article.id !== action.articleId);
return { ...state, articles:filteredArticles };
default:
throw new Error("Unrecognized action");
}
};
export function ArticleController(props) {
const [articleState, dispatch] = useReducer(reducer, initialState);
const value = useMemo(() => [articleState, dispatch], [articleState]);
return (<ArticleContext.Provider value={value}>
{props.children}
</ArticleContext.Provider>);
}
ArticleController.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
That's all our contexts. We pass in an array with two items as the value to our context provider. The first item in the array is our state and the second is a function that updates state. This value has to be memoized to prevent continuous rerendering due to the value receiving a new reference every time the component is rendered.
Splitting navigation and context providers
First, we will start by creating our main navigation. Ensure you have react-navigation installed
npm i react-navigation
We will define our main navigator, which is a combination of sub navigators.
import { createStackNavigator, createAppContainer } from 'react-navigation';
import User from './user/';
import Article from './articles/';
const Navigator = createStackNavigator(
{
user: {
screen: User
},
article: {
screen: Article
}
},
{
initialRouteName: 'article'
}
);
export default createAppContainer(Navigator);
Then we create a sub navigator for components relating to the user profile.
import React from 'react';
import { createStackNavigator } from 'react-navigation';
import PropTypes from 'prop-types'
import UserDetails from './user-details.js';
import EditUser from './edit-user.js';
import UserController from '../contexts/user-context.js'
const UserNavigator = createStackNavigator({
userDetails: {
screen: UserDetails
},
editUser: {
screen: Edituser
}
},
{
initialRouteName: 'userDetails',
});
export default function User(props) {
return (
<UserController>
<UserNavigator navigation={props.navigation} />
</UserController>
);
}
User.router = UserNavigator.router
User.propTypes = {
navigation: PropTypes.object
};
And similarly, a sub navigator for article related components
import React from 'react';
import PropTypes from 'prop-types'
import { createStackNavigator } from 'react-navigation';
import ListArticles from './all-articles.js';
import AddArticle from './add-article.js';
import ArticlesController from '../contexts/article-context.js'
const ArticleNavigator = createStackNavigator({
listArticles: {
screen: ListArticles
},
addArticle: {
screen: AddArticle
}
},
{
initialRouteName: 'articleDetails',
});
export default function Article(props) {
return (
<ArtileController>
<ArticleNavigator navigation={props.navigation} />
</ArticleController>
);
}
Article.router = ArticleNavigator.router
Article.propTypes = {
navigation: PropTypes.object
};
What we have done so far is splitting our navigators so that we can wrap each one in its respective provider. Our controllers render the providers. What about our auth context? Since authentication can be a concern across our entire app, we can wrap our entire navigator in it so that every component has access to auth state.
import React from 'react';
import Navigator from './navigator';
import { AuthController } from './context/auth-context';
export default function App() {
return (
<AuthController>
<Navigator />
</AuthController>
);
}
Instead of placing all the paths in our main navigator, we've broken them down into various sub navigators and will render them as children of their respective providers and also import them in the main navigator. To learn more about navigation in react native, you can check out the react navigation docs.
Consuming Contex
Next step, we consume our contexts. In our ListArticles component, here is how we are consuming the articles context.
import React, {useEffect, useContext} from 'react';
import {Text, FlatList, ScrollView, TouchableOpacity} from 'react-native';
import PropTypes from 'prop-types';
import {getArticles, removeAricleFromDatabase} from 'api';
import {ArticleContext} from './context/article-context';
export default function ListArticles (props) {
const [articles, dispatch] = useContext(ArticleContext);
useEffect(() => {
getArticles()
.then(articles => dispatch({type:'get', articles})
}, []);
const deleteArticle = (article) => {
removeArticleFromDatabase(article)
.then((data) => dispatch({type: 'delete', articleId: data.id}));
const Item = ({id, title}) => {
return (
<View>
<Text>{item.title}</Text>
<TouchableOpacity onPress={(id) => deleteArticle(id)}>
<Text>x</Text>
</TouchableOpacity>
</View>
)
}
return (
<ScrollView>
<FlatList
data={articles}
renderItem={({item}) => <Item title={item.title} id={item.id}/>}
keyExtractor={item => item.id}
/>
<TouchableOpacity
onPress={() => props.navigation.navigate('addArticle')}>
<Text>Add new article</Text>
</TouchableOpacity>
</ScrollView>
);
}
We are consuming the articles context here using react's useContext hook. We pass our context as a parameter to the hook and it returns the value passed in the provider. Dispatching actions we want to carry out updates our context provider value. We won't get our values if the provider is not present in the component tree hierarchy.
Similarly, we can dispatch an action for adding an article.
import React, {useContext} from 'react';
import {ArticleContext} from './context/article-context';
import {saveArticleInDatabase } from 'api';
const [_, dispatch] = useContext(ArticleContext);
const addArticle = (article) => {
saveArticleInDatabase(article)
.then((data) => dispatch({type: 'add', article: data}));
}
/* render beautiful jsx */
Every other context we have in our app can be consumed in the same fashion, with each context provider only parent to components that consume it in order to prevent unnecessary re-renders.
None of the patterns adopted here is cast in stone. This is just a guide to optimally use context to manage our React native application's state. To learn more about React context, here is something from the official react docs.
Top comments (3)
Nice, I recently created a state management library (useGlobalHook) using this Hooks and Context concept.
Features: Lightweight (1kb), No external dependencies, Nestable, Support for class components (yeah, hooks for the class 💯) etc.
devhammed / use-global-hook
Painless global state management for React using Hooks and Context API in 1KB!
use-global-hook
Installation
Quick Example
Excellent. It will be nice if you can explain how "use-global-hook" can be used to rewrite the example given in this post. Thanks.
This just saved my life, sending love bro. Keep moving in life. :)