1. 程式人生 > >Writing Redux in 15 lines of code

Writing Redux in 15 lines of code

Writing Redux in 15 lines of code

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 ComponentcomponentDidMount() {  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 have 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! This would be the primary reason for keeping named updates for me, but we could simply modify our update signature to update(storeKey, updateFn, actionName) which would enable us save store state at any given time against a name.

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.