React Journey: Part 5 - Context API

Zaki Mohammed Zaki Mohammed
Oct 08, 2023 | 8 min read | 451 Views | Comments

Every journey ends somewhere. Our React journey will come to an end with 360-degree shift in our previous approaches of communicating between components. Don't get afraid, this change is helpful and a must for our growing apps. In this article, we will touch base and talk about the concept of Context API in React and how it can allow to manage props and states like a pro.

Our journey will end with a bang. For most of us when we start exploring React, things seems pretty straight forward but until we get ourselves stuck in the mess of props and states.

The problem with props is called prop-drilling, due the this you have to pass the props from parent to child and to next child, and the chain goes on. Same for the events, you have to bring back the event till the parent level where you have the original state. For getting rid of prop-drilling we use Context API (common interview question).

To bring you out from there React offers Context API. Context API is a way to manage and share state data across components. It provides:

  • Global State Management
  • Avoids Prop Drilling
  • Provider - Consumer Pattern
  • Lots of Use Cases

Frankly speaking if you are unaware of any state management framework or provider consumer pattern you might feel Context API little difficult to digest. But no worries, in this article we will first understand the steps to start working with Context API and then will consume it across. We will take simplest example from our Notesy app first and convert it to Context API instead of props.

We will head in this direction:

  1. Configuration
  2. Select and Dispatch

1. Configuration

Follow below steps in order to configure Context API in your project base.

  • Create Context
  • Create Reducer Function
  • Create Provider – Initial State, State and Dispatch
  • Wrap Components

Create Context

Create a .jsx file that exports the context from it. For this create a context folder within your src directory, furthermore you can create one more folder (note) within context folder to maintain multiple context easily.

// context\note\NoteContext.jsx

import { createContext } from 'react';

const noteContext = createContext();

export default noteContext;

Here, we are calling the createContext() method of React and exporting it as default.

Create Reducer Function

Create a .jsx file within same note directory.

// context\note\NoteReducers.jsx

export const noteReducers = (state, { type, payload }) => {
  switch (type) {
    default:
      return state;
  }
};

Here, we have created a reducer function. The reducer function actually updates the state present in the context. The reducer function accepts state and action object with type and payload (optional). The state parameter holds the current state within the context and the action object defines the current action that arrives with payload as optional parameter.

For now, we are having one default case where we are returning the entire state object without changing anything.

Create Provider

Create a .jsx file within same note directory.

// context\note\NoteProvider.jsx

import NoteContext from './NoteContext';

const NoteProvider = ({ children }) => {
  return <NoteContext.Provider>{children}</NoteContext.Provider>;
};

export default NoteProvider;

Here, a provider is needed to wrap components those will use the note context states. Notice, we are importing the note context and through that we are creating a provider. The children will be the components will wrap later.

Initial State:

// context\note\NoteProvider.jsx

import NoteContext from './NoteContext';

const NoteProvider = ({ children }) => {
  const initialState = {
    notes: [],
    loader: false,
  };

  return <NoteContext.Provider>{children}</NoteContext.Provider>;
};

export default NoteProvider;

Here, we have to define an initial state of our context state, where notes property is initially empty array and loader is set to false.

State and Dispatch:

// context\note\NoteProvider.jsx

import { useReducer } from 'react';
import { noteReducers } from './NoteReducers';

...

const initialState = {...};

const [state, dispatch] = useReducer(noteReducers, initialState);

const value = { ...state, dispatch };

return <NoteContext.Provider value={value}>
  {children}
</NoteContext.Provider>;

...

We have to the use the hook named useReducers() of React. This hook takes 2 arguments, the noteReducers that we have created previously and initial state object. As we have followed the steps, we have them both handy. The useReducers() hook gives you state and dispatch properties that we will going to use throughout in our components to work with context anywhere.

Wrap Components

Finally, we need to wrap all the components within NoteProvider boundary where we want to use the note context:

// pages\Home.jsx

<NoteProvider>
  <Form />
  <Loader />
  <List />
  <Empty />
</NoteProvider>

Note: Don't do this operation right away otherwise your code will break, and you cry in corner. Except this step, you can complete all the other steps till now.

2. Select and Dispatch

Now is the time to start using our Context API. But wait, till now we don't have any reducer or action to work with. So, for a simple demonstration "How to do select and dispatch" we will take the loader state and show hide loader through Context API. Follow my lead!

// pages\Home.jsx

<NoteProvider>
  <Form add={add} loader={loader} />
  <List notes={notes} remove={remove} getAll={getAll} loader={loader} />
  <Empty notes={notes} />
  <Loader />
</NoteProvider>

In the Home.jsx, wrap every component within NoteProvider and keep Loader component without the condition.

Select using context

// components\Loader.jsx

...
import noteContext from '../context/note/NoteContext';

const Loader = () => {
  const { loader } = useContext(noteContext);

  return (
    <>
      {loader && (...)}
    </>
  );
};

export default Loader;

Here, in the Loader component useContext() hook to get the noteContext, from the noteContext we are taking loader state out and using the state property to define the loader UI will be shown or not. The Context API will look more like a refrigerator, if you want to eat the apple you take it out keeping everything else as is.

Dispatch action using context

Before doing dispatch, we need to create appropriate reducer to handle the dispatched action, for this we need to update the reducer file to handle the change operation in case of load operation.

// context/note/NoteReducers.jsx

export const noteReducers = (state, { type, payload }) => {
  switch (type) {
    case 'load':
      return {
        ...state,
        loader: payload,
      };
    default:
      return state;
  }
};

Here, we have added a case called load type in which we are keeping all the other state as is using spread operator and then updating the state of loader with payload.

// components\Item.jsx

...
import { useContext } from 'react';
import NoteContext from '../context/note/NoteContext';

const Item = ({ note }) => {
  const { dispatch } = useContext(NoteContext);

  const handleRemove = async () => {
    try {
      dispatch({ type: 'load', payload: true });
      ...
    } catch (error) {
    } finally {
      dispatch({ type: 'load', payload: false });
	}
  };

  return (...);
};

export default Item;

Let us now dispatch the load action. Here, we are considering the simplest component among other that is Item.jsx. We want to dispatch the action to show loader by changing the state of loader from false to true (remember we have set the initial state as false). The useContext() hook gives you states and dispatch method, here in Item.jsx we want to dispatch only so we are taking the dispatch method out. We then calls the dispatch method with action object, where type defines the action to be taken. Note: It is very important for you to provide the type as is, otherwise, the action will not be dispatched.

To protect ourselves to do such silly mistake it's better to create all the actions in a sperate file, so to avoid any typos. Create a action file in note-context folder:

// context/note/NoteAction.jsx

export const noteActions = {
  load: state => ({
    type: 'load',
    payload: state,
  }),
};

Now, in our Item.jsx file we can use this load method to provide us the action object:

// components\Item.jsx

import { noteActions } from '../context/note/NoteAction';

const Item = ({ note }) => {
  const { dispatch } = useContext(NoteContext);

  const handleRemove = async () => {
    try {
      dispatch(noteActions.load(true));
      ...
    } catch (error) {
    } finally {
      dispatch(noteActions.load(false));
    }
  };

  return (...);
};

export default Item;

Clean and crispy! You can now run the app to check the loader is appearing for the remove action or not. Note that loader will now not work for other actions.

Now, you can give a try to convert other operations in a similar manner. Give it a shot!

Fast Forwarding

For the sake of this article, we are fast forwarding the other operations here itself:

// context/note/NoteAction.jsx

export const noteActions = {
  getAll: notes => ({
    type: 'getAll',
    payload: notes,
  }),
  add: note => ({
    type: 'add',
    payload: note,
  }),
  remove: id => ({
    type: 'remove',
    payload: id,
  }),
  load: state => ({
    type: 'load',
    payload: state,
  }),
};

Here, we have added all the remaining note actions, namely getAll, add, and remove.

// context/note/NoteReducers.jsx

export const noteReducers = (state, { type, payload }) => {
  switch (type) {
    case 'getAll':
      return {
        ...state,
        notes: payload,
      };
    case 'add':
      return {
        ...state,
        notes: [payload, ...state.notes],
      };
    case 'remove':
      return {
        ...state,
        notes: state.notes.filter(i => i.id !== payload),
      };
    case 'load':
      return {
        ...state,
        loader: payload,
      };
    default:
      return state;
  }
};

Here, we have added reducers for the corresponding actions: getAll, add and remove. For getAll we are overriding the notes array with the provided payload, this will be used in case of get all listing action. In the add action we are appending the payload at the start of the array and overriding the notes state. For remove we are filtering out the removed note and overriding the notes state. Notice that it will always a immutable operation just like useState() hook.

// pages\Home.jsx

...

<NoteProvider>
  <Form />
  <Loader />
  <List />
  <Empty />
</NoteProvider>

...

Now, wrap everything within NoteProvider in the Home.jsx file.

// components\Form.jsx

...
import { useContext } from 'react';
import NoteContext from '../context/note/NoteContext';
import { noteActions } from '../context/note/NoteAction';

const Form = () => {
  const [note, setNote] = useState('');
  const { dispatch } = useContext(NoteContext);

  const handleSubmit = async event => {
    try {
      ...

      dispatch(noteActions.load(true));

      const data = await NoteService.add(newNote);
      
      dispatch(noteActions.add(data));
    } catch (error) {
    } finally {
      setNote('');
      dispatch(noteActions.load(false));
    }
  };
};

For the Form component start using the dispatch method of NoteContext, for making load and add actions.

// components\List.jsx

...
import { useContext } from 'react';
import NoteContext from '../context/note/NoteContext';
import { noteActions } from '../context/note/NoteAction';

const List = () => {
  const { notes, dispatch } = useContext(NoteContext);

  useEffect(() => {
    const getNotes = async () => {
      try {
        dispatch(noteActions.load(true));

        const data = await NoteService.getAll();
        const payload = data.slice().reverse();

        dispatch(noteActions.getAll(payload));
      } catch (error) {
      } finally {
        dispatch(noteActions.load(false));
      }
    };

    getNotes();
  }, [dispatch]);
  
  ...
};

Finally, in the the List.jsx component use the getAll and load actions. Make sure to add dispatch to the useEffect() hook array to react to the change when dispatch method is called.

Its over its done!

Go slow for the fast-forwarding part if you are trying Context API for the first time, it will be little overwhelming. Take components one by one, in the middle of the change some of the operations will not work until you have all the piece of the puzzle aligned properly.

With the Context API you are not doing anything new, we are just getting rid to the problem of prop-drilling and using states more maturely. If you have noticed, we are now not passing any props or events to these child components. Everything is working as per state changes.

Looking Back:


Zaki Mohammed
Zaki Mohammed
Learner, developer, coder and an exceptional omelet lover. Knows how to flip arrays or omelet or arrays of omelet.