Understanding React Hooks - Part Four

Understanding React Hooks - Part Four

You probably have some experience of using Redux (react-redux) with class components, and if that is the case, then you will understand how useReducer works. The concepts are basically the same: actions, reducers, dispatch, store, and state. Even if, in general, it seems very similar to react-redux, they have some differences. The main difference is that react-redux provides middleware and wrappers such as thunk, sagas, and many more besides, while useReducer just gives you a dispatch method that you can use to dispatch plain objects as actions. Also, useReducer doesn't have a store by default; instead you can create one using useContext, but this is just reinventing the wheel.

Let's create a basic application to understand how useReducer works. You can start by creating a new React app:

create-react-app reducer --template typescript

Then, as always, you can delete all files in your src folder except App.tsx and index.tsx to start a brand-new application. We will create a basic Notes application where we can list, delete, create, or update our notes using useReducer. The first thing you need to do is import the Notes component, which we will create later, into your App component:

import Notes from './Notes';

function App() {
  return (
    <Notes />
  );
}

export default App;

Now, in our Notes component, you first need to import useReducer and useState:

import { useReducer, useState, ChangeEvent } from 'react';

Then we need to define some TypeScript types that we need to use for our Note object, the Redux action, and the action types:

type Note = {
  id: number
  note: string
}

type Action = {
  type: string
  payload?: any
}

type ActionTypes = {
  ADD: 'ADD'
  UPDATE: 'UPDATE'
  DELETE: 'DELETE'
}

const actionType: ActionTypes = {
  ADD: 'ADD',
  DELETE: 'DELETE',
  UPDATE: 'UPDATE'
}

After this, we need to create initialNotes (also known as initialState) with some dummy notes:

const initialNotes: Note[] = [
  {
    id: 1,
    note: 'Note 1'
  },
  {
    id: 2,
    note: 'Note 2'
  }
]

If you remember how the reducers work, then this will seem very similar to how we handle the reducer using a switch statement, so as to perform basic operations such as ADD, DELETE, and UPDATE:

const reducer = (state: Note[], action: Action) => {
  switch (action.type) {
    case actionType.ADD:
      return [...state, action.payload]
    case actionType.DELETE:
      return state.filter(note => note.id !== action.payload)
    case actionType.UPDATE:
      const updatedNote = action.payload
      return state.map((n: Note) => n.id === updatedNote.id ? updatedNote: n)
    default:
      return state
  }
}

Finally, the component is very straightforward. Basically, you get the notes and the dispatch method from the useReducer Hook (similar to useState), and you need to pass the reducer function and initialNotes (initialState):

const Notes = () => {
  const [notes, dispatch] = useReducer(reducer, initialNotes)
  const [note, setNote] = useState('')
  ...
}

Then, we have a handleSubmit function to create a new note when we write something in the input. Then, we press Enter:

const handleSubmit - (e: ChangeEvent<HTMLInputElement>) => {
  e.preventDefault();

  const newNote = {
    id: Date.now(),
    note
  }

  dispatch({ type: actionType.ADD, payload: newNote })
}

Finally, we render our Notes list with map, and we also create two buttons, one for delete and one for update, and then the input should be wrapped into a <form> tag:

return (
  <div>
  <h2>Notes</h2>
  <ul>
    {notes.map((n: Note) => (
      <li key={n.id}>
        {n.note} {' '}
        <button
          onClick={() => dispatch({
            type: actionType.DELETE,
            payload: n.id
          })}
        >
          X
       </button>
       <button
         onClick={() => dispatch({
           type: actionType.UPDATE,
           payload: {...n, note}
         })}
      >
        Update
      </button>
     </li>
    ))}
  </ul>
    <form onSubmit={handleSubmit}>
      <input
        placeholder="New note"
        value={note}
        onChange={e => setNote(e.target.value)}
      />
    </form>
  </div>
)

export default Notes

If you run the application, you should see the following output:

                                   

As you can see in the React Dev Tools, the Reducer object contains two notes that we have defined as our initial state. Now, if you write something in the input and you press Enter, you should be able to create a new note:

                                   

Then, if you want to delete a note, you just need to click on the X button. Let's remove Note 2:

                                   

Finally, you can write anything you want in the input, and if you click on the Update button, you will change the note value:

                                 

Nice, huh? As you can see the useReducer Hook is pretty much the same as redux in terms of the dispatch method actions, and reducers, but the main difference is that this is limited just to the context of your component and its child, so if you need a global store to be accessible from your entire application then you should use react-redux instead.

Thanks for reading!