Implementing Context API + useReducer to build our own Redux

Implementing Context API + useReducer to build our own Redux

Redux is a library for managing the global application state for web apps. It is widely used with React but, Redux adds to the overall size of your app. For small apps, we can use Context API with useReducer to manage the global state.

What is Context API?

In react we often come to a point when we have to pass props to components. But, the problem occurs when we have to pass props to nested components. We may have to pass props to a component that doesn't need to use that data. This is also called prop drilling. Here, Context API comes to the rescue. It helps us to pass props across all components.

According to React docs, “Context provides a way to pass data through the component tree without having to pass props down manually at every level.” Context API gives us a clear and easy way to share states between different components without passing down the props all the time.

Context API

  • Provider provide state to children components. It passes the value of context and components which are consuming this value will re-render when value change.
  • Consumer are components that consume that state.
  • createContext() create new context using createContext()

Let's Create Context

// data-context.js
import { createContext, useContext } from "react";

// creating context
const DataContext = createContext();

export const DataProvider = ({children}) => {
    <DataContext.Provider value={{some_value}}>
        {children}
    </DataContext.Provider>
}
// exporting context as "useData" custom hook
export const useData = () => useContext(DataContext);

What is useReducer?

useReducer hook is a better alternative to useState when you have complex logic and state updates. Base logic of hook is similar to javascript Array.prototype.reduce() .

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

useReducer accepts two argument's: reducer function and initialState, and return two values as current value of the state and dispatch function.

function reducer(state, action)

reducer function takes state and action. It updates the value of the state and action determines how the value of the state will be updated.

dispatch is used to pass the action to the reducer function. It dispatches an action that is used to update the state. dispatch dispatches an object in the format:

{ type: "ACTION_TYPE", payload: "value" }

Implement Context API and useReducer.

Let's build a simple create, read and delete operation of the Todo app.

1. Create InitialState

const initialState = {
  todos: []
};

So here we create initialState for our context. todos is an empty array in which a new todo task will be added.

2. Create Context

As shown at the start of the article we create our context.

import { createContext, useContext, useReducer } from "react";

const initialState = {
  todos: []
};

// reducer-function

// creating context
const DataContext = createContext();

export const DataProvider = ({ children }) => {
  // useReducer here
  return (
    <DataContext.Provider value={{some_value}}>
      {children}
    </DataContext.Provider>
  );
};
// exporting context as "useData"
export const useData = () => useContext(DataContext);

Now let's add the reducer function and pass values to context.

3. reducer function

const reducer = (state, action) => {
  switch (action.type) {
    case "ADD_TODO":
      return { ...state, todos: state.todos.concat(action.payload) };

    case "DELETE_TODO":
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload)
      };

    default:
      return state;
  }
};

We create a reducer function. Depending on action.type we perform updates on the state. action.payload contains a value that will help the reducer to perform updates.

For example: For ADD_TODO, action.payload will contain new todo which will be added to the state.

4. Adding useReducer and passing value to Context

// state is destructured here
const [{ todos }, dispatch] = useReducer(reducer, initialState);

Now we pass value to Context. In Code, it looks like this:

import { createContext, useContext, useReducer } from "react";
// initialState
const initialState = {
  todos: []
};
// reducer function
const reducer= (state, action) => {
  switch (action.type) {
    case "ADD_TODO":
      return { ...state, todos: state.todos.concat(action.payload) };

    case "DELETE_TODO":
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload)
      };

    default:
      return state;
  }
};

// creating context
const DataContext = createContext();

export const DataProvider = ({ children }) => {
//  Initialized useReducer and destructure state
  const [{ todos }, dispatch] = useReducer(reducer, initialState);
  return (
// passing value to context
    <DataContext.Provider value={{ todos, dispatch }}>
      {children}
    </DataContext.Provider>
  );
};

// exporting context as "useData" custom hook
export const useData = () => useContext(DataContext);

Now Context + useReducer is completed.

In App.js

import { useState } from "react";
import { useData } from "./context/data-context";
import { v4 } from "uuid";
import "./styles.css";

export default function App() {
// get dispatch and state i.e todos from context
  const { todos, dispatch } = useData();
  const [newTask, setTask] = useState("");

  const addNewTodo = () => {
    dispatch({ type: "ADD_TODO", payload: { id: v4(), todo: newTask } });
  };

  const deleteTask = (todoId) => {
    dispatch({ type: "DELETE_TODO", payload: todoId });
  };

  return (
    <div className="App">
      <div>
        <input
          type="text"
          placeholder="Add todo"
          onChange={(e) => setTask(e.target.value)}
        />
        <button onClick={() => addNewTodo()}>ADD</button>
      </div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <div>{todo.todo}</div>
            <button onClick={() => deleteTask(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

What's happening here?

  1. We import useData from context. const { todos, dispatch } = useData(); extract whatever value we need from the context.
  2. addNewTodo dispatches action ADD_TODO with payload of newTask and id
  3. deleteTask dispatcher action DELETE_TODO with a payload of taskId, which we use to remove a task from the state.

import { v4 } from "uuid"; is random id generator

Here CodeSandBox


Conclusion

  • Context API helps us to pass data directly to components, helps us to avoid prop drilling.
  • Using useReducer we create our store and use Context to pass the store.
  • For more complex apps you should use Redux

Resources