You might not need redux | React Context API | useReducer

You might not need redux | React Context API | useReducer

Introduction

You've already heard of Redux as the best state management tool for React especially, and no doubt it is best, but in most of the cases or in medium-level apps we might don't need Redux.

What is Redux?

image.png

Redux works with three main things, action, reducers & store.

Action dispatches with payload and type, reducer manipulate or store the state in Redux store


I saw many people using redux in their small apps because they think they should use redux for state management, but sometimes the app is not much complex to use a tool like this, so in this case, what should you do?

React 16 and Hooks:

In React 16 many hooks are introduced to make things easy and now we can only work with functional components and using hooks to make things easy for all of us.

There are many hooks in the list

Basic Hooks

  • useState
  • useEffect
  • useContext

Other Hooks

  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

The Context API and useReducer:

Some of the basic hooks we used daily in our React App, but we have two main hooks that can re-write or replace the Redux logic and its implementation, and those are useReducer & useContext (Context API), let's see them in action.

Create a new react app with this command:

create-react-app myapp

In your src folder create a new folder called state and in this create a new file called `posts.js, and insert code below

// posts.js

export const initialState = {
  loading: false,
  posts: [],
};

export const actions = {
  GET_POSTS: 'GET_POSTS',
  GET_POSTS_SUCCESS: 'GET_POSTS_SUCCESS',
  GET_POSTS_FAILURE: 'GET_POSTS_FAILURE',
};

export const postReducer = (state, action) => {
  switch (action.type) {
    case actions.GET_POSTS:
      return { ...state, loading: true };
    case actions.GET_POSTS_SUCCESS:
      return {
        ...state,
        message: 'Posts fetched successfully',
        posts: action.payload,
        loading: false,
      };
    case actions.GET_POSTS_FAILURE:
      return { ...state, message: 'Something went wrong', loading: false };
    default:
      return state;
  }
};
  • In the code above first, we have initialState of an app,
  • After that, we have some action types which can be any string or enums you want.
  • And at the end, we have our postReducer which will be responsible to mutate the state and update UI accordingly.

All these things are just pure javascript and nothing else.

Now in src/App.js,

//App.js

import { useEffect, useReducer } from 'react';
import {
  actions,
  initialState,
  postReducer,
} from './state/posts';
import './App.css';

function App() {
  const [state, dispatch] = useReducer(postReducer, initialState);

  useEffect(() => {
    dispatch({
      type: actions.GET_POSTS,
    });
    async function fetchPosts() {
      try {
        const posts = await (
          await fetch('https://jsonplaceholder.typicode.com/posts')
        ).json();
        dispatch({
          type: actions.GET_POSTS_SUCCESS,
          payload: posts,
        });
      } catch (error) {
        dispatch({
          type: actions.GET_POSTS_FAILURE,
        });
      }
    }
    fetchPosts();
  }, []);

  return (
     <div>
        {state.loading && <p>LOADING ........</p>}
        <ul>
        {state.posts?.map((post) => {
          return (
            <div key={post.id}>
              <li>
                <b>
                  <h3>{post.title}</h3>
                </b>
                <p>{post.body}</p>
              </li>
              <hr />
            </div>
          );
        })}
      </ul>
      </div>
  );
}

export default App;
  • In the code above we have use the useReducer hook and passed it in our postReducer and initialState from src/state/posts.js.
  • Then in the useEffect hook, we have some data from API and after fetching data we are dispatching an action to save that data into the state of the app.
  • And now in UI we are simply mapping through the data show it on screen.

So now our reducer is ready to make an action, but wait there is one problem.

Suppose we have <PostList/> components have we have to get the state of an app in child component that is <PostList/>, so now if you use useReducer in another child component you will not get any state but an empty array.

But why?

Because our data is dispatched from Parent component and cannot be avaialble to it's child unlsess we passes it from the props or saved that data into our Global Context or Store Context.

So here comes the context API.

In src/state/posts.js create a new variable called StoreContext like this:

//posts.js

import React from 'react';

export const StoreContext = React.createContext(null);

// further code

in src/App.js, wrap the component with Context provider and pass the app state and dispatcher to it like this:

//App.js

// above code

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

 <StoreContext.Provider value={{ dispatch, state }}>
      <div>
        {state.loading && <p>LOADING ........</p>}
        <ul>
        {state.posts?.map((post) => {
          return (
            <div key={post.id}>
              <li>
                <b>
                  <h3>{post.title}</h3>
                </b>
                <p>{post.body}</p>
              </li>
              <hr />
            </div>
          );
        })}
      </ul>
      </div>
</StoreContext.Provider>

// further code

Now create a new component in the src folder called PostList.js and insert code below:

//PostList.js

import React, { useContext } from 'react';
import { StoreContext } from './state/posts';

export const Posts = () => {
  const store = useContext(StoreContext);
  const state = store.state;

  return (
    <div>
      <ul>
        {state.posts?.map((post) => {
          return (
            <div key={post.id}>
              <li>
                <b>
                  <h3>{post.title}</h3>
                </b>
                <p>{post.body}</p>
              </li>
              <hr />
            </div>
          );
        })}
      </ul>
    </div>
  );
};
  • Here you can use the useContext hook and pass it the StoreContext you created before, and now all the state and its dispatcher will be available to it's a child or even grandchild components.

Conclusion

This doesn't mean you shouldn't use Redux but if we have a small or medium-size app which can share data easily then we should use the power of context API and useReducer, and make our own redux structure that can be easily customizable and scalable.

Full Tutorial:

Source Code: