DEV Community

Rafał Goławski
Rafał Goławski

Posted on

Create Redux-like state management with React ⚛

Introduction

Redux is probably the most popular state management library in React environment. At the time I'm writing this article, it has nearly 6.3 million weekly downloads on npm, but despite the fact that it's so popular it doesn't mean that it's a must-have in every project.

In this article, I would like to show you how to create a Redux-like approach to state management using only React built-in utilities.

Before we begin, I would like to note that this article is for educational purposes only and if you're about to start working on a commercial application that contains a lot of complex business logic, it would be better to use Redux or some other state management library e.g. MobX, just to avoid additional overhead and refactoring in the future.

Code

To keep it as simple as possible, let's create some basic counter app that has two options - incrementing and decrementing counter value. We will start from declaring initial state and types for our actions.

type State = { counter: number };

type Action = { type: "INCREMENT" } | { type: "DECREMENT" };

const initialState: State = { counter: 0 };
Enter fullscreen mode Exit fullscreen mode

Now we need to create reducer - a simple function that is responsible for modifying and returning updated state based on action type.

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "INCREMENT":
      return {
        ...state,
        counter: state.counter + 1
      };
    case "DECREMENT":
      return {
        ...state,
        counter: state.counter - 1
      };
    default:
      return state;
  }
};
Enter fullscreen mode Exit fullscreen mode

Once we have our reducer ready, we can pass it to the useReducer hook that returns current state paired with dispatch method that's responsible for executing actions, but in order to use it all across our application we need some place where we can store it. For that, we will use React context.

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

const StoreContext = createContext<[State, Dispatch<Action>]>([
  initialState,
  () => {} // initial value for `dispatch`
]);

export const StoreProvider = ({ children }: { children: ReactNode }) => (
  <StoreContext.Provider value={useReducer(reducer, initialState)}>
    {children}
  </StoreContext.Provider>
);

export const useStore = () => useContext(StoreContext);
Enter fullscreen mode Exit fullscreen mode

Take a look at the useStore hook we created using useContext. This hook will allow us to access state and dispatch in each child component of StoreProvider.

In this example, I will use StoreProvider in render method which will cause our state to be accessible globally, but I would like to note that you should keep your state as close to where it's needed as possible, since updates in context will trigger re-render in each of the providers' child components which might lead to performance issues once your application grows bigger.

import { render } from "react-dom";
import App from "./App";
import { StoreProvider } from "./store";

const rootElement = document.getElementById("root");

render(
  <StoreProvider>
    <App />
  </StoreProvider>,
  rootElement
);
Enter fullscreen mode Exit fullscreen mode

Now we can create a UI for our counter app and see useStore hook in action.

export default function App() {
  const [state, dispatch] = useStore();

  return (
    <div className="container">
      <button onClick={() => dispatch({ type: "INCREMENT" })}>Increment</button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>Decrement</button>
      <p>Counter: {state.counter}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

And that's it!

Demo

If you want to take a closer look at code and see how this application works live, check out this sandbox 👀

Thanks for reading! 👋

Top comments (0)