Friday, October 18, 2019

useReducer

useReducer is one of the more powerful hooks that help you handle your data. This is akin to reducer you'd have come across with any Redux implementation. Reducer concept is simple. It's a simple JS function that allows you to be focussed on what you want to do. It returns a simple state and action. Based on your action invoked state is updated. That's it. Nothing more and nothing less.


To slide in easily let's try a simple counter example from our previous cases. But before we get into the example, lte's look at what a reducer is? It's a simple javascript function. It has nothing to do with either Redux or React. All that a reducer does is what the name suggests. "Reduces" or alters the state.

Any reducer takes 2 parameters - state and action. Based on the type of action, state is updated.

const counterReducer = (state, action) => { console.log("Invoked - Reducer"); switch (action.type) { case "INCREMENT": console.log("Value before increment", state); state = state + 1; console.log("Value post increment", state); break; // return state; case "DECREMENT": state = state - 1; // return state; break; default: break; } return state; };

As you see in the code above check action type, update state which in this case is a simple variable and that makes updating state easier.

Full code of the components is as below

const Counter = () => {
  const [state, dispatch] = useReducer(counterReducer, 1);
  return (
    <div>
      <div className="row">
        <form className="col s12">
          <div className="row">
            <div className="input-field col s4">
              <label htmlFor="counter">{state}</label>
            </div>
          </div>
          <div className="row">
            <div className="input-field col s4">
              <button
                className="btn blue darken-5"
                onClick={e => {
                  e.preventDefault();
                  dispatch({ type: "INCREMENT" });
                }}
              >
                Increment
              </button>
            </div>
            <div className="input-field col s4">
              <button
                className="btn red darken-5"
                onClick={e => {
                  e.preventDefault();
                  dispatch({ type: "DECREMENT" });
                }}
              >
                Decrement
              </button>
            </div>
          </div>
        </form>
      </div>
    </div>
  );
};

export default Counter;

Important thing to notice above is how we use 'useReducer' hook. First, we start by declaring it from 'react' package. We use the hook and pass reducer as a parameter along with initial value. What we get back is state and dispatch. We no longer deal with 'state' directly. 'dispatch' is the way forward. With dispatch code splatter is avoided and all UI related business validations can be handled in reducer. Another advantage is capacity to do uit testing and physical code separation - Reducer's can be defined in a different file, this way code is lot more clean and easier to comprehend.

Our next stop is to have a more complicated reducer with an object. Let's a simple ToDo reducer. As you can see from the code below, we can easily extend this to any bigger or smaller complex object!

const reducer = (state, action) => {
  switch(action.type) {
    case 'ADD_TODO':
      state = {
        todos: [...state.todos, {task:action.todo, completed: false}],
        todoCount: state.todoCount +  1
      }
      break;
    case 'TOGGLE_TODO':
      state = {
        todos: state.todos.map((todo, index) => index === action.index ? { ...todo, completed: !todo.completed } : todo),
        todoCount: state.todoCount
      }
      break;
    case 'DELTE_TODO':
      state = {
        todoCount: state.todoCount - 1,
        todos: state.todos.filter((todo) =>  todo.task !== action.task)
      }
      break;
    default:
      break;
  }
  console.log(state, action);
  return state;
}

Everything is the same as in previous example - State, action, but what changes is how we update state. For 'ADD_TODO' we update 'todos' item with new item, without leaking what it contains. Rest other cases are self explanatory

Code for the component is as below. It's no different from any other component, excpet for how we handle events to update data

const ToDo = () => {
  const [text, setText] = useState('');
  const [{todos, todoCount}, dispatch] = useReducer(reducer, {todos: [], todoCount: 0});
  return (
    <div className="">
      <div className="row">
        <form className="col s12" onSubmit={(e) => {e.preventDefault();}}>
          <div className="row">
            <div className="input-field col s6">
              <input placeholder="New Todo" name="todo_text" type="text" value={text} 
                onChange={(e) => setText(e.target.value)}/>
              <label htmlFor="todo_text">New Todo</label>
            </div>
            <div className="input-field col s6">
              <button className="btn geen darken-5" onClick={(e) => {
                e.preventDefault();
                dispatch({type: 'ADD_TODO', todo: text});
                setText("");
              }}>Add</button>
              <p>Total items - {todoCount}</p>
            </div>
          </div>
        </form>
      </div>
      {
        todos.length <= 0 ? null : todos.map((todo, index) => {
          return (
              <div className="row" key={index}>
                <div className="input-field col s6" style={{
                  textDecoration: todo.completed ? "line-through" : ""
                }}>
                  <p>{todo.task}</p>
                </div>
                <div className="input-field col s1">
                  <button className="btn green darken-5" onClick={(e) => { dispatch({type: 'TOGGLE_TODO', index})}}>Toggle</button>
                </div>
                <div className="input-field col s1">
                  <button className="btn red darken-5" onClick={(e) => { dispatch({type:'DELTE_TODO', task: todo.task})}}>Delete</button>
                </div>
              </div>
          )
        })
      }
    </div>
  )
}

export default ToDo;

Redcuer's in a simple or even a complex component, does not do justice to its capability. Together with context, Reducer's make a huge impact to overall synergy of application. Not only does it offer separation of concerns, but as stated earlier, it allows for applications to be unit tested easily and above all, allows for easier agile development methodologies, a big plus for companies across geographies. Let's look at them text Redux + Context

No comments: