July 29, 2021
Comparing React Native state management libraries

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.

Prerequisites

In order to follow along with this article, you should have the following:

Working knowledge of React and React Native

Node.js, npm, or Yarn installed on your machine (npm comes packaged with Node.js)
XCode or Android Studio installed on your machine
The other necessary dependencies outlined in the React Native docs

I will be using Yarn for this article, but if you prefer npm, be sure to replace the commands with the npm equivalents.

Setting up a demo app

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.

Clone the repo

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

Install dependencies

After cloning the repo to your local machine, install the dependencies using whichever package manager you prefer:

npm install
#or
yarn install

Run the app

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

Managing the state with the React Context API

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.

Pros of using the Context API and useReducer

Ideal for small projects
Doesn’t impact bundle size

Cons of using the Context API with useReducer

Updating large objects can get messy quickly
May be unsuitable for large projects, because you need to stack multiple Providers up on the tree if the need arises

Managing the state with Hookstate

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.

Pros of using Hookstate

Easy APIs to get the job done
Performant
Plenty of extensions to create more feature-reach applications
Fully typed system

Cons of using Hookstate

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.

Managing the state with Easy-Peasy

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 */}
)
}

Pros of using Easy-Peasy

Fully reactive
Built on Redux so there’s support for Redux Dev tools and more
Easy APIs

Cons of using Easy-Peasy

Increased bundle size. If this is a big deal for you, Easy-Peasy may not be your ideal library

Conclusion

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:

Mobx
Recoil

You can find the repository for this project here, in case you’d like to inspect the code for each example.

The post Comparing React Native state management libraries appeared first on LogRocket Blog.

Leave a Reply

Your email address will not be published. Required fields are marked *

Send