Creating stores using React context, hooks, and Immer

Creating stores using React context, hooks, and Immer

When creating any medium-to-large React applications, it's useful to have a store to contain your core application data. We don't want to be loading in the same data from APIs in scattered components, and we don't want to have to deal with the prop-drilling problem (passing through props down multiple levels in the React tree).

When creating any medium-to-large React applications, it's useful to have a store to contain your core application data. We don't want to be loading in the same data from APIs in scattered components, and we don't want to have to deal with the prop-drilling problem (passing through props down multiple levels in the React tree).

There are many application data management solutions out there, with Redux and MobX being two of the most popular. In this article we'll be creating our own home-grown store management solution using React context, hooks, and Immer.

Immer is an awesome library that allows you to perform mutations on non-primitive data structures in JavaScript, while still preserving the old data. It does this by creating a "draft copy" of the data structure you want to edit, and crawls through it and creating ES6 proxies to trap any mutations you perform. Those mutations are then recorded and replayed against a deep copy of your original data structure.

To start things off, we will create two React contexts: one to contain the store data, and one to allow editing that data. We'll do this using React's createContext API:

const initialState = {
  /* whatever you want */
}

const StateContext = React.createContext(initialState) const UpdateContext = React.createContext(null) // soon to be populated with an updater function

We can even be clever and have the UpdateContext provider have a default updater function that throws an error in development mode to ensure that we always have an enclosing provider:

function invariantUpdaterFn() {
  if (process.env.NODE_ENV === 'development') {
    throw new Error('Updater was called without an enclosing provider.')
  }
}
const UpdateContext = React.createContext(invariantUpdaterFn)

Next, we want to encapsulate the two contexts into a single provider, so that they're always paired with each other.

export function StoreProvider({ children }) {
  return (
    <UpdateContext.Provider>
      <StateContext.Provider>{children}</StateContext.Provider>
    </UpdateContext.Provider>
  )
}

But we actually want to add the values for our providers so that they can actually be updated! We can leverage a built-in hook for that:

export function StoreProvider({ children }) {
  const [state, setState] = React.useState(initialState)
  return (
    <UpdateContext.Provider value={setState}>
      <StateContext.Provider value={state}>{children}</StateContext.Provider>
    </UpdateContext.Provider>
  )
}

This approach would work for the simplest kind of updater function, where a consumer can just pass in an entirely new store state and the entire state will be replaced. We want something better though; we want to be able to leverage the functionality of Immer to be able to just edit the state, which gives the user the most power while also preserving the old state. To do that, we can use a reducer function instead, using React's useReducer hook:

import produce from 'immer'

export function StoreProvider({ children }) { const [state, updater] = React.useReducer(produce, initialState) return ( <UpdateContext.Provider value={updater}> <StateContext.Provider value={state}> {children} </StateContext.Provider> </UpdateContext.Provider> ) }

The useReducer hook takes a reducer function as its first parameter, and the initial state as the second parameter. The reducer function itself has a signature that takes the current state as its first parameter, and some kind of action for the second parameter.

The action itself can be anything (in canonical Redux it's a plain object with a type and a payload). In our case however, the action will be some updater function that takes a proxied copy of the state, and mutates it. Luckily for us, that's exactly the same function signature that Immer's produce function expects (because it's modeled as a reducer)! So we can just pass the producefunction as-is to useReducer.

This completes the implementation of our provider, which implements the necessary logic to update our store's state. Now we need to provide a way for users to actually be able to grab the store state, as well as update it as necessary. We can create a custom hook for that!

export function useHook() {
  return [useContext(StateContext), useContext(UpdateContext)]
}

This custom hook will return a tuple that can be deconstructed into the state, and the updater function, much like the useState hook.

With our implementation complete, this would be how an application would use this (with our favorite example, the Todo app):

// store.js
import React from 'react'
import produce from 'immer'

// an array of todos, where a todo looks like this: // { id: string; title: string; isCompleted: boolean } const initialTodos = []

const StateContext = React.createContext(initialTodos) const UpdateContext = React.createContext(null)

export function TodosProvider({ children }) { const [todos, updateTodos] = React.useReducer(produce, initialTodos) return ( <UpdateContext.Provider value={updateTodos}> <StateContext.Provider value={todos}> {children} </StateContext.Provider> </UpdateContext.Provider> ) }

export function useTodos() { return [React.useContext(StateContext), React.useContext(UpdateContext)] }

// app.js import { TodosProvider } from 'store'

export function App() { return ( <TodosProvider> {/* ... some deep tree of components */} </TodosProvider> ) }

// todo-list.js import { useTodos } from 'store'

export function TodoList() { const [todos, updateTodos] = useTodos()

const completeTodo = id => updateTodos(todos => { todos.find(todo => todo.id === id).isCompleted = true })

const deleteTodo = id => updateTodos(todos => { const todoIdxToDelete = todos.findIndex(todo => todo.id === id) todos.splice(todoIdxToDelete, 1) })

return ( <ul> {todos.map(todo => ( <li key={todo.id}> <span>{todo.title}</span> <button>Complete</button> <button>Delete</button> </li> ))} </ul> ) }

It's that easy! Our logic for creating the store is so generic, that we can even wrap it up into our own createStore function:

// create-store.js
import React from 'react'
import produce from 'immer'

export function createStore(initialState) { const StateContext = React.createContext(initialState) const UpdateContext = React.createContext(null)

function StoreProvider({ children }) { const [state, updateState] = React.useReducer(produce, initialState) return ( <UpdateContext.Provider value={updateState}> <StateContext.Provider value={state}> {children} </StateContext.Provider> </UpdateContext.Provider> ) }

function useStore() { return [React.useContext(StateContext), React.useContext(UpdateContext)] }

return { Provider: StoreProvider, useStore } }

// app.js import { createStore } from 'create-store'

const TodosStore = createStore([])

export const useTodos = TodosStore.useStore

export function App() { return <TodosStore.Provider>{/* ... */}</TodosStore.Provider> }

// todo-list import { useTodos } from 'app'

export function TodoList() { const [todos, updateTodos] = useTodos() /* ... */ }

This approach works very well for small applications, where the React tree is shallow and debugging won't take forever. However for larger applications or larger teams you probably want to use Redux as it enforces a specific style, and also allows you to debug actions better by inspecting the dev tools.


By : Ferdy Budhidharma


reactjs

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

Why ReactJS is better for Web Application Development?

Web Application Development is the point of contact for a business in today's digital era. It is important to choose the right platform for Web Application Development to build a high end Web

ReactJs Course | How to build a YouTube Clone using ReactJS

Hello, this is a new tutorial series about ReactJs. In this series, I will teach you two important topics. First, I will be teaching you what is ReactJs library and why this is so powerful and expandable. We will see what are the important...

Top Reasons to Choose ReactJS for Front-End Development - Prismetric

Why ReactJS is perfect choice for your next web application? Get to know all about ReactJS development and its benefits in detail for frontend development.

Highcharts with Reactjs

#reactjs #highcharts #therichpost https://therichpost.com/highcharts-with-reactjs-working-tutorial/ Highcharts with Reactjs Please like, share and subscribe....

ReactJS Development Services | ReactJS Development Company - Chapter 247 Infotech

Chapter 247 Infotech is a leading ReactJS development company in India, USA, offering ReactJS development services at par to a spectrum of business domains from E-commerce, healthcare to Edutech at