How to implement a Redux state management in Vue

Well, we all know that Redux is usually used as a way to manage data in react projects. It is similar to Vuex state management in vUE projects. It has the same function but uses a different method

Recently, I have been studying Redux. In order to better help new students like me to have a deeper understanding of the internal principle of Redux, we will try to manually implement a simple REdux state management.

The difference is that we implemented it in a VUE project

Code not moving, target first

To achieve Redux, we first set a small goal for ourselves, namely a business scenario. We don’t want to be so complicated. We start with the simplest or the most classic counter, as shown in the figure below:

When I click the plus button, I want the data in the warehouse to increase by 1. When I click the minus button, I want the data in the warehouse to decrease by 1. Well, it’s a very clear requirement

The specific implementation

  • Story to create

First, we need to create a redux directory in the SRC directory to hold our core redux code. The core of the redux code is the createStore method, because that’s how we created the store when we initialized it. Redux will return dispatch, subscribe, getState, replaceReducer methods, but in fact we usually use the first three

Ok, let’s implement it slowly

Create an index.js file in the redux directory as follows:

// index.js

function createStore () {
    letstate ; / / distribution of the actionletDispatch = ()=>{} // Subscribe to data updateletSubscribe = ()=> {} // get the state of storelet getState = ()=>{
    
    }
    
    return{
    	dispatch ,
    	subscribe ,
    	getState
    }

}

export {
    createStore
}
Copy the code

Now that we have a prototype of Redux, it’s just a matter of refining the methods

  • Initialize the store

Create a store folder in SRC and create a new index.js folder as follows:

import { createStore } from ".. /redux"

const store = createStore();

export default store;
Copy the code
  • Mount the store

We’ll import it in the import file main.js and mount it:

import store from './store'

Vue.prototype.$store = store;
Copy the code

Then, if you print this.$store in the page, the result is as follows, then the mount is successful ~

However, at this point, there is no actual functionality because we haven’t improved our createStore method

  • Perfect createStore

Those who have used Redux know that we need to pass a reducers parameter when initializing store, that is, calling createStore, to write the processing logic of changing the state. Here we take the counter as an example and take the reducer of the counter as the input parameter ~

Actions, Constants and Reducer are directly coded here, so I won’t bother with this

constants counter.js

// Constant definitionexport const ADD = 'add';
    export const MINUS = 'minus'
Copy the code

actions counter.js

import * as constants from '.. /constants/counter'/ / addexport const add = ()=>({
    type: constants.ADD}) // Reduceexport const minus = ()=>({
    type : constants.MINUS
})

Copy the code

reducers counter.js

import * as constants from '.. /constants/counter'

const defaultState = {
	count: 0
}

export default (state = defaultState, action) => {
    switch ( action.type ) {
    	case constants.ADD :
            return {
            	...state ,
            	count : state.count + 1
            }
    	case constants.MINUS :
            return {
                ...state ,
                count : state.count - 1
            }
    	default :
            returnstate; }}Copy the code

dispatch

We expect that when we click the plus sign, an Add action will be sent, and then the corresponding Reducer receives the action and does corresponding processing. The same is true for the minus sign button ~

The action must be an object. It must contain a type value indicating the type of action that is required. Then we can retrieve the new state from the recuder that we passed. The dispatch method is as follows:

/ / distribution of the actionletDispatch = (action)=>{// Determine the actiontypeif( typeof action ! = ='object') throw Error('Expected the action to be a object.');
    if( action.type === undefined ) throw Error('The action.type is not defined'); State = reducer(state, action); }Copy the code

We print state, and by clicking the plus and minus buttons, we can see that state does change, as we expected

getState

We can get the current state using the getState() method, which is therefore relatively simple, as follows ~

// Get the state of storelet getState = () => state;
Copy the code

In this way, we can obtain the corresponding state ~ through getState method every time we send the action

There is a problem, however, that when we initially get state we return undefined, because we initially defined state but did not assign ~

To solve this problem, we can dispatch an init action at initialization, which returns the default state from the Reducer:

// Initialize state dispatch({type : '@@redux/INIT'}); // Get the state of storelet getState = () => state;
Copy the code

subscribe

Although we now print each count change on the console via the button, it is not reported back to the page because our value has not changed on the page

Redux provides a function called SUBSCRIBE to monitor changes in the data. Its input parameter is a function that subscribes to changes in the data and does the corresponding logical processing. It returns a function to unsubscribe from the data.

// The subscription handlerletlisteners = []; // Subscribe to update datalet subscribe = (fn)=> {
    listeners.push(fn);
    return ()=>{
        listeners = listeners.filter(listener => fn != listener );
    }
}
Copy the code

Note that the above are only subscriptions, and we need to cycle the methods of the listeners as their data changes. Therefore, we need to add a note at the end of the Process:

listeners.forEach(listener => listener());
Copy the code

The effect

Then let’s try ~

The code is as follows:

<template>
    <div class='wrapper'>
        <div class=""> counter: {{number}}</div> <div class="btn-box">
            <button class="btn" @click="handleAddBtnClick">+</button>
            <button class="btn" @click="handleMinusBtnClick">-</button>
        </div>
        <div class="btn-box">
            <button class="btn" @click="handleRemoveListenerBtnClick"</button> </div> </template> <script> import {add, minus} from'.. /actions/counter'
    export default {
        data() {return{
            	number : this.$store.getState().count, consoleHandler: null}}, methods: {// counter increment onehandleAddBtnClick(){
                this.$store.dispatch(add())}, // counter decrement by onehandleMinusBtnClick(){
            	this.$store.dispatch(minus())}, // Cancel print event listeninghandleRemoveListenerBtnClick(){ this.consoleHandler(); }},mounted() {// Data update event this.$store.subscribe(()=>{
            	this.number = this.$store.getState().count; }) // Prints the event this.consoleHandler = this.$store.subscribe(()=>{
            	console.log('My value has changed ~')
            })
    	}
    }
</script>

<style scoped>
    .wrapper{
        padding: 30px 15px;
        text-align: center;
    }
    .btn-box{
        margin-top: 20px;
    }
    .btn{
        display: inline-block;
        font-size: 20px;
        min-width: 50px;
        text-align: center;
        margin: 0 10px;
    }
</style>

Copy the code

We added two event listeners at the initial time, one for updating the value of number and the other for printing. Each time we change the data, these two methods will be triggered. When we click the cancel print listener button, the subsequent data changes will not trigger the printing operation

Try it yourself

combineReducers

So far, we have basically implemented a simple version of Redux, but it is not perfect. The problem is that we still need a function called combineReducers, which is used to merge multiple reducer and return a new reducer, so as to distinguish different reducer states ~

We added this method to index.js in the redux folder as follows:

// Merge the reducer // key is the new state and the value of the namespace is reducer. After this command is executed, a new reducer is returnedfunctionCombineReducers (reducers) {// When the reducer is called the second time, the internal reducer automatically delivers the state of the first time to the reducerreturn(state = {}, action) => {// Reducer returns a state by defaultlet newState = {}
    	for (let key inReducers (reducers) {// By default reducer two parameters are called state and actionlet s = reducers[key](state[key], action);
            newState[key] = s;
    	}
    	returnnewState; }}Copy the code

Then, we can replace the reducer, we will not repeat here ~

conclusion

Through the above study, we implemented a manually redux, although relatively simple, very few core code, but at least is complete, mainly study a kind of coding thought, we can learn some framework or library can pay attention to at the same time, its internal implementation, and then we can start step by step to imitate a simple version, In the back of the continuous expansion, I hope to continue to work with you ~

I also uploaded the code to Github, you can refer to it