If you’ve had to work on an application where more than two components with different ancestry had to share the same state, you understand that passing props to all these components can get messy fast. State management is a way to manage this data across our app, from the value of a text field to the rows on a table.
Enter state management libraries, like Redux. These libraries aimed to solve this problem, but they still weren’t perfect. The truth is, the perfect state management library does not exist. There are too many different factors to consider when choosing one, like the size of your app, what you want to achieve, and how much state is shared.
In this article, we’ll be looking at some state management options to help you make a decision on which to use in your React Native apps. I will compare the developer experience of state management with the React Context API, Hookstate, and Easy-Peasy.
There are so many articles already written about popular state managers like Redux, so I will be discussing these smaller ones to help you make an informed decision.
In order to follow along with this article, you should have the following:
I will be using Yarn for this article, but if you prefer npm, be sure to replace the commands with the npm equivalents.
Due to the nature of this article, we won’t be building a new app from scratch. Because I’ll only be discussing how these libraries compare, I set up a demo app where you can follow along as I demonstrate their strengths and weaknesses.
You can find my demo app at this Github repo. If you clone it locally and install the necessary dependencies, you’ll see that branches have been created for examples of each library we’ll be discussing:
git clone https://github.com/edmund1645-demos/comparing-rn-state-lib
After cloning the repo to your local machine, install the dependencies using whichever package manager you prefer:
npm install #or yarn install
You can take a look around the main
branch, especially the App.js
file, to get an understanding of how the app is structured before we implement state management.
Run the app using this command:
yarn ios #or npm #or yarn android
We’ll be looking at the Context API first. Now, I know what you’re thinking: the Context API is not a “standalone” library. While this is true, it’s still an option worth considering.
Check out the context-example
branch after cloning the repo and installing the dependencies:
git checkout context-example
Now take a look at the contexts/CartContext.js
file:
import React, { createContext } from 'react'; export const initialState = { size: 0, products: {}, }; export const CartContext = createContext(initialState);
We use the createContext
method from React to create a context object and export it. We also pass in a default value.
In App.js
, we first import the CartContext
object and the default value initialState
.
After importing, we need to set up a useReducer
hook to modify the state based on the type of action:
// import context import { CartContext, initialState } from './contexts/CartContext.js'; // reducer function const reducer = (state, action) => { switch (action.type) { case 'ADD_TO_CART': if (!state.products[`item-${action.payload.id}`]) { return { size: (state.size += 1), products: { ...state.products, [`item-${action.payload.id}`]: { ...action.payload, quantity: 1 } } }; } else { let productsCopy = { ...state.products }; productsCopy[`item-${action.payload.id}`].quantity += 1; return { size: (state.size += 1), products: productsCopy }; } } }; export default function App() { // set up reducer hook const [state, dispatch] = useReducer(reducer, initialState); // create a Provider and use the return values from the reducer hook as the Provider's value return ( <CartContext.Provider value={[state, dispatch]}> {/* other components */} </CartContext.Provider> ) }
When setting up a context with values that need to be modified, like in our example, we need to use a reducer hook. You’ll notice we are using the values from this reducer hook in the Provider in the code above. This is because the reducer
function updates the state, so we want to make the state (and the function to modify it) available across all of our components.
A bit later, you’ll see why children components have access to the value of the Provider and not the default value with which the context was created.
Next, take a look at the components/ProductCard.jsx
file:
import React, {useContext} from 'react' import {CartContext} from '../contexts/CartContext' const ProductCard = ({ product }) => { const [state, dispatch] = useContext(CartContext) function addToCart() { dispatch({type: 'ADD_TO_CART', payload: product}) } return ( {/* children */} ) }
In order to access the values with which the cart context was created, we need to import it and pass it to the useContext
hook.
Notice how the returned value is the array we passed to the Provider earlier, and not the default value the context was created with. This is because it uses the value on the matching Provider up the tree; if there was no CartContext.Provider
up the tree, the returned value would be initialState
.
When the cart button is clicked, addToCart
is invoked, and an action is dispatched to our reducer function to update the state. If you look at the reducer
function again, you’ll notice an object is being returned; this object is the new state.
Every time we dispatch an action, a new state is returned just to update a single property on that large object.
Let’s look at the cart screen (screens/Cart.jsx
):
import React, {useContext} from 'react' import { CartContext } from '../contexts/CartContext' const Cart = () => { const [state, dispatch] = useContext(CartContext) return ( {/* children */ ) }
Here we are using the same pattern as ProductCard.jsx
, only this time we’re using just the state
to render cart items.
useReducer
useReducer
Hookstate comes with a different approach to state management. It’s simple enough for small applications and flexible enough for relatively large applications.
Checkout the hookstate-example
branch:
git checkout hookstate-example
With Hookstate, we use the concept of global state in state/Cart.js
. The library exports two functions: createState
to create a new state by wrapping some properties and methods around the default state and returning it, and useState
to use the state returned from createState
or another useState
.
import { createState, useState } from '@hookstate/core'; const cartState = createState({ size: 0, products: {}, }); export const useGlobalState = () => { const cart = useState(cartState); return { get: () => cart.value, addToCart: (product) => { if (cart.products[`item-${product.id}`].value) { cart.products[`item-${product.id}`].merge({ quantity: cart.products[`item-${product.id}`].quantity.value + 1 }); cart.size.set(cart.size.value + 1); } else { cart.products.merge({ [`item-${product.id}`]: { ...product, quantity: 1 } }); cart.size.set(cart.size.value + 1); } }, }; };
With the way Hookstate is structured, we can also export a helper function for interacting with components inside the state.
All we need to do is import useGlobalState
, invoke it in a functional component, and destructure any of the methods from the returned objects (depending on what we want to achieve).
Here’s an example of how we use the addToCart
method in components/ProductCard.jsx
:
import { useGlobalState } from '../state/Cart'; const ProductCard = ({ product }) => { // invoke the function to return the object const state = useGlobalState() function addToCart() { // pass the product and let the helper function deal with the rest state.addToCart(product) } return ( {/* products */} ) }
And on the Cart
page in /screens/Cart.js
:
import { useGlobalState } from '../state/Cart'; const Cart = () => { const {products} = useGlobalState().get() return ( {/* render every item from the cart here */} ) }
The best part of Hookstate is that every property or method (both nested and at the top level) in the global state is a type of state, and has various methods to directly modify itself. It is reactive enough to update the state in all components across the app.
I know I said there weren’t any “perfect” alternatives, but it seems Hookstate is attempting to disprove my theory. There is, however, a negligible factor to consider: Hookstate isn’t very well known – it has about 3,000 weekly downloads on npm, so there’s a chance the community around it is small.
Easy-Peasy is an abstraction of Redux, built to expose an easy API that greatly improves the developer experience whilst retaining all the benefits Redux has to offer.
I noticed that working with Easy-Peasy is like working with a combination of the two examples above, because you have to wrap the entire application around a provider (don’t worry, you only need to do that once, unless you want modular states).
To import Easy-Peasy, copy the following into App.js
:
import { StoreProvider } from 'easy-peasy'; import cartStore from './state/cart'; export default function App() { return ( <> <StoreProvider store={cartStore}> {/* children */} </StoreProvider> </> );
You can import hooks from the library to pick out specific parts of the global state that you need inside your components.
Let’s take a look at /state/Cart.js
:
import { createStore, action } from 'easy-peasy'; export default createStore({ size: 0, products: {}, addProductToCart: action((state, payload) => { if (state.products[`item-${payload.id}`]) { state.products[`item-${payload.id}`].quantity += 1; state.size += 1; } else { state.products[`item-${payload.id}`] = { ...payload, quantity: 1 }; state.size += 1; } }), });
We use createStore
to spin up a global store. The object passed is called the “model”. When defining the model, we can also include properties like actions. Actions allow us update the state in the store.
In components/ProductCard.jsx
, we want to use the addProductToCart
action, so we make use of the useStoreActions
hook from Easy-Peasy:
import React from 'react'; import { useStoreActions } from 'easy-peasy'; const ProductCard = ({ product }) => { const addProductToCart = useStoreActions((actions)=> actions.addProductToCart) function addToCart() { addProductToCart(product) } return ( {/* children */} ) }
If we wanted to use the state in a component, we use the useStoreState
hook as seen in screens/Cart.jsx
:
import React from 'react'; import { useStoreState } from 'easy-peasy'; const Cart = () => { const products = useStoreState((state)=> state.products) return ( {/* children */} ) }
In this article, we looked at the comparison between the Context API with hooks, Hookstate, and Easy-Peasy.
To summarize, using the Context API with hooks on demo projects would be ideal, but when your applications start growing in size, it becomes hard to maintain. This is where Hookstate and Easy-Peasy shine.
Hookstate and Easy-Peasy both expose easy APIs to manage the state, and are performant in unique ways. Easy-Peasy was built over Redux so you have those added benefits, and Hookstate has a suite of extensions for implementing features in your application, like local storage persistence for the state.
Many alternatives were not mentioned in this article due to length, so here are some honorable mentions:
You can find the repository for this project here, in case you’d like to inspect the code for each example.
LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.