Writing Redux in 15 lines of code
2020 Update: You might be interested in learning how to build a store from scratch which works with Redux Devtools and uses React Hooks to access it anywhere instead:
I was inspired to write this today after a colleague new to React complained about how much boilerplate is involved when using Redux (and Redux-Saga). So I put this together for a couple reasons:
- We mistakenly think verbosity is necessary for a one-way data-flow.
- We use Redux without understanding how it works.
For our simple Redux store we want the following features:
- A function to update the store state (because we need to notify subscribers when the state changes) — update(storeKey, updateFn)
- A function to subscribe to updates — subscribe(fn)
- A function to get the internal state (this isn’t necessary but encourages the user to not directly manipulate the state object) — getState()
class Store {
constructor(initialState) {
this.state = initialState;
this.subscriptions = [];
}
update(storeKey, updateFn) {
const nextStoreState = updateFn(this.state[storeKey]);
if (this.state[storeKey] !== nextStoreState) {
this.state[storeKey] = nextStoreState;
this.subscriptions.forEach(f => f(store));
}
}
subscribe = fn => this.subscriptions.push(fn)
getState = () => this.state
}
note the lack of line breaks to preserve my catchy title
Pretty simple right? Now each time the store updates we want to call ReactDOM.render to re-render our app. Later we could add a HOC like redux-connect does to wrap our root component to handle the updates, and to pass store props anywhere in our app (part II anyone?).
Let’s see that working in a simple demo.
You might have noticed the following method:
increaseUserAge = () => {
this.props.store.update('user', state => ({
...state,
age: state.age + 2,
}));
}
Look mom, no actions!
Yet the data still flows one-way. And there’s nothing stopping us from extracting updates out into re-usable and easily testable functions:
// Somewhere else.
function fetchUserDetailsAction(store) {
const { user } = store.getState(); if (!user.fetched) {
$get('/user/123').then(user => store.update('user', user));
}
}// In your Component
componentDidMount() {
fetchUserDetailsAction(this.props.store);
}
“But I like reducers and actions 🤬”
As demonstrated above, complicated state updates can be extracted without needing actions. But I like reducers too. They keep data manipulation for each store node in one place.
There’s nothing stopping us from having a reducers/user.js
file:
const userReducer = {
increaseUserAge: (state, ageIncrease) => ({
...state,
age: state.age + ageIncrease,
}),
setUserEmail: (state, email) => ({
...state,
email,
}),
};
And we could use that instead:
increaseUserAge = () => {
this.props.store.update('user', state =>
userReducer.increaseUserAge(state, 2));
}
Actions also have a purpose beyond logic encapsulation; time travelling. We can log and replay actions, something very handy for debugging! We could achieve something similar by naming our actions
I wanted to demonstrate some basic principles here and show that you can have a very lean store update cycle if you choose to.
Our next step would be adding a connect() function so we can wrap our app in a HOC and so we can pass pieces of state as props to components like redux-connect does.