app-generator / docs

App Generator - The Official Documentation | AppSeed
https://docs.appseed.us
1 stars 1 forks source link

[React] useReducer #110

Open mahfujul-helios opened 1 month ago

mahfujul-helios commented 1 month ago

useReducer

what is useReducer?

useReducer is a React hook that provides an alternative to useState for managing state in functional components, especially when dealing with complex state logic. While useState is primarily used for managing independent pieces of state, useReducer is more suitable for managing state that involves multiple sub-values or when the next state depends on the previous one.

how it's works ?

useReducer is a React hook designed for managing state in functional components by employing a reducer function. This hook offers an alternative to the simpler useState hook, especially suitable for handling complex state logic or scenarios where state transitions are dependent on previous state values or multiple actions. To utilize useReducer, you define a reducer function that takes the current state and an action as parameters, computing and returning the next state based on these inputs. An initial state is provided when invoking useReducer, and the hook returns both the current state and a dispatch function. Dispatching actions triggers the execution of the reducer function, updating the component's state accordingly. By structuring state management in this way, useReducer promotes better organization and scalability, particularly in larger applications where state management becomes more intricate.

For those who may be unfamiliar with Redux, we’ll explore this concept a bit further. There are three main building blocks in Redux:

Let’s see how these building blocks compare to managing state with the useReducer Hook. Below is an example of a store in Redux:

import { createStore } from 'redux'

const store = createStore(reducer, [preloadedState], [enhancer])

In the code below, we initialize state with the useReducer Hook:

const initialState = { count: 0 }

const [state, dispatch] = useReducer(reducer, initialState)

The reducer function in Redux will accept the previous app state and the action being dispatched, calculate the next state, and return the new object. Reducers in Redux follow the syntax below:

(state = initialState, action) => newState

Let’s consider the following example:

// notice that the state = initialState and returns a new state

const reducer = (state = initialState, action) => {
   switch (action.type) {
      case 'ITEMS_REQUEST':
         return Object.assign({}, state, {
            isLoading: action.payload.isLoading
         })
      case ‘ITEMS_REQUEST_SUCCESS':
         return Object.assign({}, state, {
            items: state.items.concat(action.items),
            isLoading: action.isLoading
         })
      default:
         return state;
   }
}
export default reducer;

useReducer doesn’t use the (state = initialState, action) => newState Redux pattern, so its reducer function works a bit differently. The code below shows how you’d create reducers with React’s useReducer:

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}  

Below is an example of an action that can be carried out in Redux:

{ type: ITEMS_REQUEST_SUCCESS, payload: { isLoading: false } }

// action creators
export function itemsRequestSuccess(bool) {
   return {
      type: ITEMS_REQUEST_SUCCESS,
      payload: {
      isLoading: bool,
    }
   }
}

// dispatching an action with Redux
dispatch(itemsRequestSuccess(false))    // to invoke a dispatch function, you need to pass action as an argument to the dispatch function

Actions in useReducer work in a similar way:

// not the complete code
switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    default:
      throw new Error();
  }

// dispatching an action with useReducer
 <button onClick={() => dispatch({type: 'increment'})}>Increment</button>

If the action type in the code above is increment, our state object is increased by 1.

The reducer function

The JavaScript reduce() method executes a reducer function on each element of the array and returns a single value. The reduce() method accepts a reducer function, which itself can accept up to four arguments. The code snippet below illustrates how a reducer works:

const reducer = (accumulator, currentValue) => accumulator + currentValue;
[2, 4, 6, 8].reduce(reducer)
// expected output: 20

In React, useReducer essentially accepts a reducer function that returns a single value:

  const [count, dispatch] = useReducer(reducer, initialState);

The reducer function itself accepts two parameters and returns one value. The first parameter is the current state, and the second is the action. The state is the data we are manipulating. The reducer function receives an action, which is executed by a dispatch function:

function reducer(state, action) { }

dispatch({ type: 'increment' })

The action is like an instruction you pass to the reducer function. Based on the specified action, the reducer function executes the necessary state update. If you’ve used a state management library like Redux before, then you’ve probably come across this state management pattern.

Specifying the initial state

The initial state is the second argument passed to the useReducer Hook, which represents the default state:


const initialState = { count: 1 }

// wherever our useReducer is located
const [state, dispatch] = useReducer(reducer, initialState, initFunc)

If you don’t pass a third argument to useReducer, it will take the second argument as the initial state. The third argument, which is the init function, is optional. This pattern also follows one of the golden rules of Redux state management: the state should be updated by emitting actions. Never write directly to the state.

However, it’s worth noting that the Redux state = initialState convention doesn’t work the same way with useReducer because the initial value sometimes depends on props.

Creating the initial state lazily

In programming, lazy initialization is the tactic of delaying the creation of an object, the calculation of a value, or some other expensive process until the first time it is needed.

As mentioned above, useReducer can accept a third parameter, which is an optional init function for creating the initial state lazily. It lets you extract logic for calculating the initial state outside of the reducer function, as seen below:

const initFunc = (initialCount) => {
    if (initialCount !== 0) {
        initialCount=+0
    }
  return {count: initialCount};
}

// wherever our useReducer is located
const [state, dispatch] = useReducer(reducer, initialCount, initFunc);

If the value is not 0 already, initFunc above will reset initialCount to 0 on page mount, then return the state object. Notice that this initFunc is a function, not just an array or object.

The dispatch method

The dispatch function accepts an object that represents the type of action we want to execute when it is called. Basically, it sends the type of action to the reducer function to perform its job, which, of course, is updating the state.

The action to be executed is specified in our reducer function, which in turn, is passed to the useReducer. The reducer function will then return the updated state.

The actions that will be dispatched by our components should always be represented as one object with the type and payload key, where type stands as the identifier of the dispatched action and payload is the piece of information that this action will add to the state. dispatch is the second value returned from the useReducer Hook and can be used in our JSX to update the state:

// creating our reducer function
function reducer(state, action) {
  switch (action.type) {
   // ...
      case 'reset':
          return { count: action.payload };
    default:
      throw new Error();
  }
}

// wherever our useReducer is located
const [state, dispatch] = useReducer(reducer, initialCount, initFunc);

// Updating the state with the dispatch functon on button click
<button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button>

Notice how our reducer function uses the payload that is passed from the dispatch function. It sets our state object to the payload, i.e., whatever the initialCount is. Note that we can pass the dispatch function to other components through props, which alone is what allow us to replace Redux with useReducer.

Let’s say we have a component that we want to pass as props to our dispatch function. We can easily do that from the parent component:

<Increment count={state.count} handleIncrement={() => dispatch({type: 'increment'})}/>

Now, in the child component, we receive the props, which, when emitted, will trigger the dispatch function and update the state:

<button onClick={handleIncrement}>Increment</button>

Building a simple counter app with the useReducer Hook

Now, let’s put our knowledge to use by building a simple counter app with the useReducer Hook:


import React, { useReducer } from 'react';

const initialState = { count: 0 }
 // The reducer function
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    case 'reset':
      return {count: state.count = 0}
    default:
     return { count: state.count  }
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    <div>
      Count: {state.count}
       <br />
       <br/>
       <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
       <button onClick={() => dispatch({ type: 'decrement'})}>Decrement</button>
       <button onClick={() => dispatch({ type: 'reset'})}>Reset</button>
    </div>
  );
};

export default Counter;

First, we initialize the state with 0, then we create a reducer function that accepts the current state of our count as an argument and an action. The state is updated by the reducer based on the action type. increment, decrement, and reset are all action types that, when dispatched, update the state of our app accordingly.

To increment the state count const initialState = { count: 0 }, we simply set count to state.count + 1 when the increment action type is dispatched.

conclusion

useReducer is a powerful tool offered by React for managing state in functional components, especially in scenarios where state logic is complex or involves multiple actions. By employing a reducer function, useReducer provides a structured approach to state management, promoting better organization and scalability in larger applications. It offers an alternative to the simpler useState hook, allowing developers to handle more intricate state transitions and dependencies with ease. While it may introduce a bit more boilerplate compared to useState, useReducer proves invaluable for maintaining clean, maintainable, and efficient codebases, particularly in projects where state management plays a critical role.