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.