Implementation of Redux like state manager with native JavaScript

Time:2021-1-25

React and Vue divide an application into different components. The state of one component may affect another component. With the increasing complexity of the project, the communication between components is a headache

Me: Qiao to Ma TTE! Is the increasing complexity of the project making state management difficult?

  • There may be many factors causing a model change: server response, cache data, local data, UI state, user events… How do we deal with these States?
  • Multiple components depend on the same state. How to update and synchronize the state of each component?

That’s redux.

When it comes to Redux, it’s amazing. She’s here to solve this problem, so students who don’t know need to study Redux carefully.

It’s on. It’s on

In order to follow the new front-end technology and enhance personal competitiveness, we must understand some principles and be a smart coder who knows why. In the future, soft girls will show off their skills and hard core will be confident.

State management in my mind

In fact, the young brick movers at the bottom of the front-end society don’t have the state in mind. I think Redux is very good and I appreciate it very much

First of all, flattery on the surface: the concept and the move of “lying trough” are powerful; in fact, they really feel powerful.

If I can achieve one by myself, am I just the same? yes!

Now, suppose I have implemented this state manager, how should I use it, what posture is cool, it’s worth thinking about

Create a module first

const app = {
  namespace: 'app',
  state: {
    num: 0
  },
  actions: {
    change (state, value) {
      this.setState({
        num: value
      })
    }
  }
}

Then create a store

const store = createStore({
  modules: [app]
})

This store is our overall state. You can compare it to a Redux store, but please don’t say it’s copied.

Then the store should also expose the methods of obtaining and modifying data

API that store should contain

const actions = store.mapActions({
  change: 'app/change'
})
const states = store.mapStates({
  num: 'app/num'
})

Here, everything is taken for granted, the usage is simple and clear, once suspected that he was a genius.

const app = {
  namespace: 'app',
  state: {
    num: 0
  },
  actions: {
    change (state, value) {
      this.setState({
        num: value
      })
    }
  }
}
const { mapActions, mapStates } = createStore({
  modules: [app]
})
const actions = store.mapActions({
  change: 'app/change'
})
const states = store.mapStates({
  num: 'app/num'
})
actions.change(1)
states.num // 1

Create logic

All of a sudden, it seems that there is no direction. Faced with the fake code examples, I feel frustrated that I can no longer use CC and CV. This kind of feeling is like being lovelorn.

No way, life will continue, vaguely remember the teachers said: in the face of suffering, simplify, one by one break

So you list a few implementation steps:

  1. Store is a class
  2. You need to install the app modules. Note that there may be multiple modules
  3. Modules are divided by a namespace
  4. Implement mapactions
  5. Implementing mapstates
  6. Drink a glass of water and greet the goddess you have admired for a long time on wechat to live a delicate life

The implementation of store

class Store {
  constructor (options) {
    this._state = {}
    this._modules = {}
    this.setModules(options.modules)
  }
  
  setModules (modules) {
    [].concat(modules).forEach(m => {
      let ns = m.namespace
      let _m = new Module(m, this)
      
      this._module[ns] = _m
      this._state[ns] = _m.state
    })
  }
}
function createStore (options) {
  return new Store(options)
}

You find that the logic of store can be simplified by simple combination, so you plan to implement oneModuleClass to facilitate expansion

Implementation of module class

class Module {
  constructor (m, _store) {
    this._store = _store
    this.state = m.state
    this.actions = m.actions
    this.namespace = m.namespace
  }

  setState (state) {
    Object.assign(this.state, state)
  }
}

Now, the structure of the store can be sorted out, but the methods (actions) and states (States) have not been exposed, so now we need to implement mapactions and mapstate.

The necessity of dispatch

Think of the previous mapactions call:

const actions = store.mapActions({
  change: 'app/change'
})

Is it OK to just find and return the corresponding actions of the module? It’s too low, and the middleware of Redux is enhanced and expanded by modifying dispath. It’s too powerful to use for reference.

Therefore, mapactions should be the encapsulation implementation of dispatch;

Implementation of dispatch

class Store {
  // ...
  dispatch = (path, ...args) => {
    let { ns, key } = normalizePath(path);
    ns = this._module[ns]

    if (!ns) { return }

    let action = ns.actions[key]

    return action.call(ns, ns.state,...args)
  }
}
function normalizePath (path) {
  const [ns, key] = path.split('/')
  return { ns, key }
}

Implementation of mapactions and mapstates

All the methods returned by mapactions should encapsulate the dispatch, so that all the methods go through the dispatch, which makes it extremely convenient for us to add Middleware in the future.

class {
  // ...
  mapActions = map => {
    let res = {}
    forEachValue(map, (path, fkey) => {
      let fn = (...args) => {
        this.dispatch(path, ...args)
      }
      
      res[fkey] = fn
    })
    return res
  }
  mapStates = map => {
    let res= {}
    
    forEachValue(map, (path, fkey) => {
      const { ns, key } = normalizePath(path);
      const m = this._module[ns]

      res[fkey] = m.state[key]
    })
    return res
  }
}
function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

Mapstates is implemented conveniently

Here, you look at the last evil spirit on the list, smile and think: technology changes the world, changes yourself, and you feel like a winner in life.

Just the great initiative to give you full courage, you happy to open wechat, skilled to open the dialog with the goddess, carefully sent a sentence: in?

Review

Goddess may be busy again. You are going to test your code

const actions = mapActions({
  change: 'app/change'
})
const states = mapStates({
  num: 'app/num'
})

change(1)
console.log(states.num) // 0
console.log(store._store._state.app.state.num) // 1

const add = val => actions.change(states.num + 1)
const reudce = val => actions.change(states.mum - 1)

Problem with code found:

  1. States do not respond to changes
  2. Mapactions are too limited; the above actions should support the following optimizations
const actions = store.mapActions({
  change: 'app/change',
  add (dispatch) {
    dispatch('app/change', states.val + 1)
  },
  reduce (dispatch) {
    dispatch('app/change', states.val - 1)
  }
})

Proxy the value returned by mapstates to the state of the module

class Store {
  // ...
  mapStates = map => {
    let res= {}
    
    forEachValue(map, (path, fkey) => {
      const { ns, key } = normalizePath(path);
      const m = this._module[ns]

      if (!m) { return }
      
      proxyGetter(res, fkey, m.state, key)
    })
    return res
  }
}
function proxyGetter (target, key, source, sourceKey) {
  sharedPropertyDefinition.get = function () {
    return source[sourceKey]
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

Modify mapactions to support expansion

class Store {
  // ...
  mapActions = map => {
    let res = {}
    forEachValue(map, (path, fkey) => {
      let fn
      
      if (typeof path === 'function') {
        fn = (...args) => {
          path(this.dispatch, ...args)
        }
      } else {
        fn = (...args) => {
          this.dispatch(path, ...args)
        }
      }
      
      res[fkey] = fn
    })
    return res
  }
}

So far, the front-end state manager function has been basically realized. You can

Go here to see her simple usage and source code;
See simple examples of undo and redo

She has been able to meet her own use, but she still has limitations and shortcomings

  • The unpredictability of dispath to setstate

For example:

const app = {
  namespace: 'app',
  state: { num: 0 },
  actions: {
    async getNumer () {
      // waiting...
      this.setState({ num: 'xxx' })
    }
  }
}

As you can see, if action is an asynchronous method, then we don’t knowsetStateWhen it will be called.

  • There is no error handling logic in the code
  • Module does not support multi-level modules, like vuex;

However, you can define it by using a namespace

const app = { namespace: 'app' }
const app = { namespace: 'app:user' }
const app = { namespace: 'app:system' }

It also flattens the data, doesn’t it?

  • Calling other action in action of module
const other = {
  namespace: 'other',
  actions: {
    foo () {
      this.dispatch('app/getNumber')
    }
  }
}