Writing Redux in 15 lines of code

Dominic Tobias
ITNEXT
Published in
3 min readOct 11, 2018

--

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:

  1. We mistakenly think verbosity is necessary for a one-way data-flow.
  2. 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.

--

--

Interested in programming and fintech: JS, WASM, Rust, Go, blockchain, L1/2, DApps, trading