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:
- The three basic Hooks:
useState
,useEffect
,useContext
. - The seven additional Hooks:
useReducer
,useCallback
,useMemo
,useRef
,useImperativeHandle
,useLayoutEffect
,useDebugValue
. - And understand the endless potential of custom 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
‘ssetState
. - 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.