Redux has become the defacto standard for state management in React. It's a great tool for handling global state, and its sheer popularity means you'll probably want to learn it at some point.
Redux isn't the easiest concept to learn though. Even though the docs are good (and are being rewritten to be even better), it's often hard to understand the concept of Redux' uni-directional data flow, dispatching, reducing, actions and what have you. I struggled with it myself, when I first came across Redux.
Luckily for us, Redux isn't as complicated as it looks. As a matter of fact, you can implement a working version of the core parts or Redux in 27 lines of code!
This article will take you through how you can implement an API similar to Redux yourself. Not because you'll want to do just that, but because it'll help you understand how Redux works!
What is Redux, really? 🤔
The core part of Redux the store. This store contains a single state tree. The store lets you read the state, dispatch actions to update the state, subscribe and unsubscribe for updates to that state, that's about it.
This store is passed around your application. If you're using React, you're probably passing your store to react-redux
's <Provider />
component, which lets you access it in other parts of your application by wrapping your component with connect()
.
Let's implement Redux!
We're going to re-implement Redux by implementing the createStore
method. It does what it says on the tin - it gives us a store instance we can play with. The store is just an object with a few methods on it, so it doesn't need to be fancy.
Step 1: getState
Let's start off small, by implementing the getState
method:
function createStore() {
let state = {};
return {
getState() {
return state;
}
};
}
When we call createStore
, we create an empty state object. This is that single state tree you keep hearing about. We return our "store", which is just an object with one property - a getState
function. Calling this getState
function grants access to the state
variable inside of the createStore
closure.
This is how we'd use it:
import { createStore } from './redux';
const store = createStore();
const state = store.getState();
Step 2: Accept a reducer
One of the core concepts of Redux is the reducer. A Redux reducer is a function that accepts the current state and an action, and returns the next state (the state after an action has happened). Here's a simple example:
function countReducer(state = 0, action) {
if (action.type === 'INCREMENT') return state + 1;
if (action.type === 'DECREMENT') return state - 1;
return state;
}
Here - the countReducer
responds to two actions - INCREMENT
and DECREMENT
. If the action passed doesn't match either, the current state is returned.
To continue our journey in understanding Redux, we need to take a quick break, and understand the data flow of Redux:
- The user dispatches an action
- The action is passed to your reducer
- The reducer returns the new state
- The state is updated in the store
- Anyone interested in the new state gets notified.
In order for us to follow this flow, we need our store to have a reducer! Let's pass that in as the first argument:
function createStore(initialReducer) {
let reducer = initialReducer;
let state = reducer({}, { type: '__INIT__' });
return {
getState() {
return state;
}
};
}
Here, we accept a reducer, and call it to get our initial state. We "trigger" an initial action, and pass in an empty object to our state.
Redux actually lets us pass in pre-calculated state when we create our store. This might have been persisted in local storage, or come from the server side. Anyhow, adding support for it is as simple as passing an initialState
argument to our createStore
function:
function createStore(initialReducer, initialState = {}) {
let reducer = initialReducer;
let state = reducer(initialState, { type: '__INIT__' });
return {
getState() {
return state;
}
};
}
Great! Now we even support server side rendering - that's pretty neat!
Step 3: Dispatch actions!
The next step in our Redux journey is to give the user some way to say that something happened in our app. Redux solves this by giving us a dispatch
function, which lets us call our reducer with an action.
function createStore(initialReducer, initialState = {}) {
let reducer = initialReducer;
let state = reducer(initialState, { type: '__INIT__' });
return {
getState() {
return state;
},
dispatch(action) {
state = reducer(state, action);
}
};
}
As we can tell from the implementation, the concept of "dispatching" an action is just calling our reducer function with the current state and the action we passed. That looks pretty simple!
Step 4: Subscribing to changes
Changing the state isn't worth much if we have no idea when it happens. That's why Redux implements a simple subscription model. You can call the store.subscribe
function, and pass in a handler for when the state changes - like this:
const store = createStore(reducer);
store.subscribe(() => console.log('The state changed! 💥', store.getState()));
Let's implement this:
function createStore(initialReducer, initialState = {}) {
let reducer = initialReducer;
let subscribers = [];
let state = reducer(initialState, { type: '__INIT__' });
return {
getState() {
return state;
},
dispatch(action) {
state = reducer(state, action);
subscribers.forEach(subscriber => subscriber());
},
subscribe(listener) {
subscribers.push(listener);
}
};
}
We create an array of subscribers, which starts out as empty. Whenever we call our subscribe
function, the listener is added to the list. Finally - when we dispatch an action, we call all subscribers to notify them that the state has changed.
Step 5: Unsubscribing to changes
Redux also lets us unsubscribe from listening to state updates. Whenever you call the subscribe
function, an unsubscribe function is returned. When you want to unsubscribe, you would call that function. We can augment our subscribe
method to return this unsubscribe
function:
function createStore(initialReducer, initialState = {}) {
let reducer = initialReducer;
let subscribers = [];
let state = reducer(initialState, { type: '__INIT__' });
return {
getState() {
return state;
},
dispatch(action) {
state = reducer(state, action);
subscribers.forEach(subscriber => subscriber());
},
subscribe(listener) {
subscribers.push(listener);
return () => {
subscribers = subscribers.filter(subscriber => subscriber !== listener);
};
}
};
}
The unsubscribe
function removes the subscriber from the internal subscriber-registry array. Simple as that.
Step 6: Replacing the reducer
If you're loading parts of your application dynamically, you might need to update your reducer function. It's not a very common use-case, but since it's the last part of the store API, let's implement support for it anyways:
function createStore(initialReducer, initialState = {}) {
let reducer = initialReducer;
let subscribers = [];
let state = reducer(initialState, { type: '__INIT__' });
return {
getState() {
return state;
},
dispatch(action) {
state = reducer(state, action);
subscribers.forEach(subscriber => subscriber(state));
},
subscribe(listener) {
subscribers.push(listener);
return () => {
subscribers = subscribers.filter(subscriber => subscriber !== listener);
};
},
replaceReducer(newReducer) {
reducer = newReducer;
this.dispatch({ type: '__REPLACE__' });
}
};
}
Here we simply swap the old reducer with the new reducer, and dispatch an action to re-create the state with the new reducer, in case our application needs to do something special in response.
Step 7: What about store enhancers?
We've actually left out a pretty important part of our implementation - store enhancers. A store enhancer is a function that accepts our createStore
function, and returns an augmented version of it. Redux only ships with a single enhancer, namely applyMiddleware
, which lets us use the concept of "middleware" - functions that let us do stuff before and after the dispatch
method is called.
Implementing support for store enhancers is 3 lines of code. If one is passed - call it and return the result of calling it again!
function createStore(initialReducer, initialState = {}, enhancer) {
if (enhancer) {
return enhancer(createStore)(initialReducer, initialState);
}
let reducer = initialReducer;
let subscribers = [];
let state = reducer(initialState, { type: '__INIT__' });
return {
getState() {
return state;
},
dispatch(action) {
state = reducer(state, action);
subscribers.forEach(subscriber => subscriber(state));
},
subscribe(listener) {
subscribers.push(listener);
return () => {
subscribers = subscribers.filter(subscriber => subscriber !== listener);
};
},
replaceReducer(newReducer) {
reducer = newReducer;
this.dispatch({ type: '__REPLACE__' });
}
};
}
Step 8? There is no step 8!
That's it! You've successfully re-created the core parts of Redux! You can probably drop these 27 lines into your current app, and find it working exactly as it is already.
Now, you probably shouldn't do that, because the way Redux is implemented gives you a ton of safeguards, warnings and speed optimizations over the implementation above - but it gives you the same features!
If you want to learn more about how Redux actually works, I suggest you have a look at the actual source code. You'll be amazed at how similar it is to what we just wrote.
Takeaways
There isn't really any point in re-implementing Redux yourself. It's a fun party trick, at best. However, seeing how little magic it really is will hopefully improve your understanding of how Redux works! It's not a mysterious black box after all - it's just a few simple methods and a subscription model.
I hope this article has solidified your knowledge on Redux and how it works behind the scenes. Please let me know in the comments if you still have questions, and I'll do my best to answer them!
Top comments (10)
Sometimes less is more...
I would say 'most of the times'.
Thanks for a very helpful article. I have dabbled with Redux before, and while I think I had a decent grasp of the concept, seeing how it actually works, and how simple the mechanisms are really makes it a lot easier to wrap my head around.
Great article! I never dived deep into Redux's core logic but this is really fascinating.
Would you mind checking my last article about middlewares? Maybe you can give me some good advices!
dev.to/pigozzifr/lets-play-with-re...
Last time I played with Redux, there was the concept of smart and dumb components, and smart components had to be connected to Redux. Your article seems to indicate just adding subscribers, which is slick.. but.. how do you manage re-rendering only components that are connected to stores/actions so that for example, you can have a component (or subset of components) re-render only when a specific action/state occurs, and not have the state call all subscribers on all changes? Is that no longer the way Redux works? Or is that more advanced and this intro was to keep it simple?
This intro is actually pretty framework agnostic - talking about how Redux is implemented, not how it connects to React.
That being said, the way the connect method from react-redux works is by subscribing on mount, unsubscribing on unmount, and implementing a clever shouldComponentUpdate based on the output of your mapStateToProps method. You could create a similar HOC with this implementation, too!
For anyone interested in redux, reading the source really is a great way to see what it can do.
It is surprisingly small, and fast to read through, as this article indicates. :)
I love the process of what Redux is and how to implement it step by step as it helps understand how Redux works 🙂
I'd love it if you wanted to dissect my Redux one line replacement hook... useSync
dev.to/chadsteele/redux-one-liner-...
It was a nice explanation in simple words. hats off. but I would like to touch more on enhancers. Still have some doubts on its implementation.