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 };
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;
}
};
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);
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
);
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>
);
}
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)