Give an upgrade to your useReducer hook

Give an upgrade to your useReducer hook

If you had to ask a React developer - what is the most important thing to know about in React, most probably the answer would have been that knowing about "state", and how it works behind the scenes, is the most important principle in React.

There are several ways to manage your state in modern React setups. The most common way is the simple useState hook which lets you easily track and change your state mode.

But sometimes you want to manage a more complex state. For that, you have several state management tools, like Redux. Redux is not only about having a "global" state. Redux also applies the state management tool in a particular way which is known as the dispatch(action) -- reducer --- store cycle.

In this article, I will briefly explain the how Redux state cycle works, but the topic of this article is not Redux.

I will show you how you can upgrade your useReducer hook, which is a hook released by the React team, and make it have functionality like the Reduxs' reducer. We will dive into the source code of this upgraded reducer which is called useEnhancedReducer, and it's going to be very interesting so let's not waste any time and get going!

State cycle in Redux

In Redux, if you want to perform changes to the state, you have to go through a cycle of some functions that are in charge to update your state. Let's examine how it works.

First stop - Dispatch: Dispatch is a function that is basically in charge to start the whole process of adjusting the state. It can be called anywhere in your application using the useDispatch hook and you pass to it the action you want to trigger.

Second stop - Action: As said, the action is triggered by dispatch. The action has an "action type" that tells the reducer what kind of changes to the state we want to return to the store. An action also typically has a "payload" that is simply arguments we can pass to the reducer, in addition to the action type.

Third stop - The Reducer: The reducer function is the part that is in charge of making the actual changes to the state. We decide what changes will occur upon an action type being triggered and the reducer then returns it to the final stop - the store.

Final stop - The Store: The store is just the place that holds your state. To the store, you pass the Reducer as an argument, in order to make changes to the store, and then you wrap your whole app with a provider that is in charge to connect your application with the store, so you can have access to it from all components.

I really like the allegory the Redux team has come with in the official docs:

You can think of dispatching actions as "triggering an event" in the application. Something happened, and we want the store to know about it. Reducers act like event listeners, and when they hear an action they are interested in, they update the state in response.


Reducers are not unique to Redux

In February 2019, with the release of React 16.8, The React team has come up with the concept of Hooks. One of the hooks that were introduced to us is the useReducer hook. The useRudcer hook lets you manage your state in a way that is almost identical to the way that Redux lets you manage your state but without a Redux setup.

You usually use the useReducer hook where you have to manage your state globally like with the in React context or when you have a more complex state you want to manage in a single component.

Even though the setup of the useReducer hook is almost identical to the Reduxs' setup, there are some differences.

Middleware and getState in Redux

In most Redux setups, you usually apply middleware to your store. You can think of middleware as a filter for your actions before they get to the reducers and then to the store. By definition, an action function must return an object in order to manipulate the reducer and then the state.

The problem is, that sometimes you do want to make side-effects in your actions, and that is where middleware comes into place. It lets you overwrite the "rules" of Reduxs' actions and use more complex operations in your action functions.

For example, with the thunk middleware, you can use an action as a thunk instead of a plain object. Thunk is a function that returns another function. By that, we are able to make asynchronous actions like HTTP requests and so on, and then dispatch a "regular" action to our reducer.

It looks something like this:

export const onDeletedItem = itemId => {
  return (dispatch) => {
    
    axios
      .post('https://localhost:3003/deletedItem', {
        id: itemId,
      })
      .then(response => {
        dispatch(deletedSuccess(itemId));
      })
      .catch(err => console.log(err));
  };
};

As you can see we have a function (onDeletedItem) that returns another function (anonymous function) that is a thunk, and that is possible thanks to the Redux-thunk middleware.

Another thing missing in the useReducer hook, is the ability to get the current state in a convenient way. In a Redux setup, we have a built in function that is called getState. With getState we are able to get the current state of our store. Let's update our last snippet and see how it works:

export const onDeletedItem = itemId => {
  return (dispatch, getState) => {

    const userId = getState().auth.userId

    axios
      .post('https://localhost:3003/deletedItem', {
        itemId,
        userId
      })
      .then(response => {
        dispatch(deletedSuccess(itemId));
      })
      .catch(err => console.log(err));
  };

};

Now we are able to pass our user id from our store with the request.

The useReducer hook does not offer us a way to apply a middleware and getState with its default behavior, but we are going to implement it now!


Implementing middleware and getState in useReducer

To implement both middleware and a getState function, we have to make an upgrade to our useReducer hook. The source code of the following examples was taken from here.

So first let's see how we implement the getState function. It looks like this:

export const useEnhancedReducer = (reducer, initState, initializer) => {
  const lastState = useRef(initState)
  const getState = useCallback(() => lastState.current, [])
  return [
    ...useReducer(
      (state, action) => lastState.current = reducer(state, action),
      initState,
      initializer
    ),
    getState
  ]
}

So let's break up what's going here. We initialized a new function that receives 3 arguments which are identical to the arguments that our useReducer function usually received: A reducer function, the initial state, and the initializer which is helpful if you want the initial state to be different depending on a situation.

Next, we are creating the getState function. First, we create a constant named lastState that has a ref to the initState. By that, we are able to keep track of any changes to our state. We then create the getState function itself: The function is wrapped with the useCallback hook. This is made to ensure that the getState function is constant on every render, so you can pass it to some hook without adding them to the dependency list or store them somewhere else to call them from outside.

We then return our lastState variable with the .current property to get the most recent snapshot of our state.

Lastly, we return an array with:

  • Our original useReducer hook with all its properties(using the spread operator). We pass to it our reducer, initState and initializer we received from our useEnhancedRudcer hook so it can process them.
  • Our getState function.

Now we can run our new improved hook:

const [state, dispatch, getState] = useEnahancedReducer(reducer, initState)

Congrats! Now we can use the getState function to receive our most recent snapshot of our state.

Now let's implement the middleware, this will be more challenging. The implementation of it looks like this:

const useEnhancedReducer = (reducer, initState, initializer, middlewares = []) => {
  const lastState = useRef(initState)
  const getState = useCallback(() => lastState.current, [])
  const enhancedReducer = useRef((state, action) => lastState.current = reducer(
    state,
    action
  )).current 
  const [state, dispatch] = useReducer(
      enhancedReducer,
      initState,
      initializer
    )
  const middlewaresRef = useRef(middlewares)
  const enhancedDispatch = useMemo(()=>middlewaresRef.current.reduceRight(
    (acc, mdw) => action  ?=> mdw(state)(getState)(acc)(action),
    dispatch
  ), []) // value of middlewares is memoized in the first time of calling useEnhancedReducer(...)
  return [state, enhancedDispatch, getState]
}

As we can see we added another argument to our useEnhancedReducer hook - middlewares. It lets us add to an array of middlewares we want to apply to our useEnhancedReducer hook.

We run the useReducer hook with our useEnhancedRuducer Hook as an argument for the reducer so we can have access to the state and dispatch.

Now it's time to create our enhancedDispatch function that will act as a middleware for our actions so we can use dispatch with more complex functions (and not just plain objects).

We keep a ref for our middleware array and we use the built-in javascript array function reduceRight. This will let us "jump" with our actions between our middlewares. We then pass the dispatch function as well so we can trigger the "regular" dispatch from our middlewares as well.

We give our function the signature of middlewares which is state => getState => next => action.

We can understand it better if we examine a simple middleware function:

const logMiddleware = state => getState => next => action  ?=> {

  console.log('before action', action, getState())


  next(action)
 

 console.log('after action', action, getState())

Now we can clearly see the pattern. The middleware will run with 'before action' when triggered. We will log to the screen our action and the current state with the getState function. Then we call next and pass it the action.

Remember where this next was in the previous snippet? You're right it was in the place of the acc (which stands for accumulator). What does it mean? that each middleware will now run 'before action' and afterward it will call next passing it the action again. This will cause the first middleware (in index 0) to be applied first in the 'before action' round and last in the 'after action' round (due to how the call stack works in js).

Now middleware takes the next() dispatch function, and returns a dispatch function, which in turn serves as next() to the middleware to the left, and so on. It's still useful to have access to the state object and to the getState() function, so we keep them at the top-level argument.

Further study

That was some in-depth analysis. I hope you had a good time. If you had a hard time following up or you just want to dive even deeper I encourage you to go into the following resources:



Alaa Bashiyi

Frontend developer at Toluna Corporate

3 年

Looks like it's time to try useReducer out !

要查看或添加评论,请登录

??????? Nitsan Cohen的更多文章

  • Testing flow Building a React app

    Testing flow Building a React app

    Testing is a field in programming that is usually neglected, especially for beginners. The main misconception is about…

    3 条评论
  • Testing flow creating a React Web-page

    Testing flow creating a React Web-page

    Testing is a field in programming that is usually neglected, especially for beginners. The main misconception is about…

    2 条评论
  • Crash course about CORS

    Crash course about CORS

    This article is dedicated to my dear brother Omer Cohen who's always pushing towards being a better developer. Access…

    5 条评论
  • Move on from React already!

    Move on from React already!

    So first of all, sorry for the frightening title (and picture). I really don't want you to just ditch React.

    2 条评论
  • React Bootstrap vs Bootstrap - Comparison

    React Bootstrap vs Bootstrap - Comparison

    One of the core concepts in React is the creation of custom components and the reuse of them. There are several React…

    15 条评论
  • You have to know closures to be a (good) React developer

    You have to know closures to be a (good) React developer

    I hear a lot about the misconception that being a React developer does not mean you have to have a deep understanding…

    17 条评论

社区洞察

其他会员也浏览了