React Hooks review #4: useReducer

This is chapter one of the seven additional React Hooks series, variants of the first three basic hook, but written to handle edge cases. And still, the goal was simple: summarize all the eleven official React hooks:

useReducer

An astute reader may have been asking this all along. I mean, setState is generally the same thing, right? Return a stateful value and a function to re-render a component with that new value…

Getting to Know the useReducer React Hook

The useReducer is similar to useState, but have some additional usages that suite it in the following cases:

  • State dependency: When one state depends on the previous one.
  • Calculation: Where there are complicated transitions between the states.
  • Encapsulation: To keep the component simple in cases there is logic in the transition.

Syntax:

const [state, dispatchFunc] = useReducer(reducerFunc, initialArg[, initFunc]);
  • state: The current state.
  • dispatchFunc: The equivalent to useState‘s setState.
  • reducerFunc: Receives two values, the previous state and the “action” object from the “dispatchFunc”.
  • initialArg: The first state.
  • initFunc: Optional. When you need to run some initializing logic. The method receives the “initialArg”, and needs to pass it on (return) to the “reducerFunc”.

The most simple example: the reducer just returns a random number.

const reducer = (state,action)=>{
    return Math.random();
}

const App = () => {
    const [state, dispatch] = React.useReducer(reducer,"click me");

    return <div>
        <p>Random number displayer:</p>
        <p onClick={()=>dispatch()}>{state}</p>
    </div>
}

The second example makes more sense:

  • It has state dependency.
  • The reducer receives and behaves according to the different “action” sent bt the dispatch method.
const App = ()=>{
    const reducer = (state,action) => {
        if(action.type === "stepBackward"){
            console.log('Event stepping backward is doing a step!!!!')
        }
        return {steps: state.steps+1};
    }
    const [state,dispatch] = React.useReducer(reducer,{steps:0});

    return <div>
        <p>Step counter: You made {state.steps} steps</p>
        <button onClick={()=>dispatch({type:"stepForward"})}>step forward</button>
        <button onClick={()=>dispatch({type:"stepBackward"})}>step backward</button>
    </div>
}

State dependency

Let’s say we want an undo button or some text value.

const App = () => {
    const [text,setText] = React.useState('');
    const [state, dispatch] = React.useReducer(reducer,{text:defaultText,history:[]});

    return <div>
        <h2>{state.text}</h2>
        <input value={text} onInput={e => setText(e.target.value)}/>
        <button  onClick={()=>{dispatch({type:"do",text:text}); setText('')}}>Set</button>
        <button  onClick={()=>{dispatch({type:"un-do"}); setText('')}}>Redo</button>
    </div>
}

As can be seen, only the “do” action sends the new value. The “undo” have just the action type.

For that, we need the reducer to keep all the historical values. And the reduced need to check if the current action sends a new value, or request to go back.

const defaultText = "Write something";
const reducer = (state,action)=>{
    switch(action.type){
        case 'do':
            if(action.text){
                state.history.push(state.text);
                state.text = action.text;
            }
            break;
        case 'un-do':
            state.text = state.history.pop();
            if(!state.text){
                state.text = defaultText;
            }
            break;
    }
    return {...state}
}

Calculation

Let’s say have some complicated transitions, like a safe box that needs some combination to open.

const SafeBox = () => {
    const [state, dispatch] = React.useReducer(reducer,null,defaultState);

    return <div>
        <p>The locker is {state.isLocked?'locked':'open'}</p>

        <button onClick={()=>dispatch({type:"left"})}> + </button>

         {state.currentNumber} 

        <button onClick={()=>dispatch({type:"right"})}> - </button>
        
        <p>Can you open it?</p>
    </div>
}

Here the safe box state needs to have the current number, the current lock state, and the previous steps. In addition to that, the reducer has some complicated logic that we don’t want to calculate inside the React component.

const reducer = (state,action) => {
    const direction = action.type === "left"?  '<' : '>';
    const nextNumber = action.type === "left"?  1 : -1;
    state.currentNumber += nextNumber;
    state.movements.push(direction + state.currentNumber);
    
    //Check the combination
    if(state.movements.join('').includes('<2<3>2<3')){
        state.isLocked = false;
    }
    return {...state};
}

Encapsulated logic

Let’s take the previous example a step forward.

To have a ToDoList we need several state variables and some logic. While using useState approach, the ToDo data is spread inside the state (the C of the MVC) with the logic (M). So everything is in the component(MC are inside the V). With useReduce you can move the login from the component and have Model/View/Controller separation.

We don’t need to add a watcher for inner value or trigger when an item added to the list.

const reducer = (state,action) => {
    switch(action.type){
        case "flip":
            // Toggle todo
            let todo = state.todos.find(todo=> todo.id === action.id);
            todo.status = !todo.status;
            break;
        case "add":
            // Add new doto to the list
             state.todos.push({title:action.title,id:Math.random(),status:true});
            break;
    }
    
    return {...state};
}
// Convert string array to a todo data list
const initMethod = (todos) => {
    todos.map(todo=> {
        todo.id = Math.random();
        todo.status = true;
    })
    return {todos:todos};
}
let defaultTodos = [{title:'Something to do'}];
const App = () => {
    const [input, setInput] = React.useState('');
    // All the todo data and logic
    const [state, dispatch] = React.useReducer(reducer,defaultTodos,initMethod);

    return <div>
        <p>ToDo List:</p>
        {state.todos.map(todo => <p key={todo.id} onClick={()=>dispatch({type:"flip",id:todo.id})} style={todo.status?{}:{textDecoration: "line-through"}}>{todo.title}</p>)}
        <input value={input} onInput={e => setInput(e.target.value)}/>
        <button  onClick={()=>{dispatch({type:"add",title:input}); setInput('')}}>Add</button>
    </div>
}

Until the next chapter about useCallback, that is.