Implement Redux architecture from zero to one with native JS

Preface

Recently, I used my spare time to read Daika's "React Books", which benefits a lot from explaining React, Redux, etc. from the basic principles.It's better to write it once in a thousand times than to follow the author's thoughts and reference code to implement the basic Demo. Here's a Redux architecture from scratch using native JS based on your own understanding and reference data.

I. Redux Basic Concepts

Friends who often use React may be familiar with Redux,React-Redux. Here's to tell you that Redux and React-Redux are not one thing. Redux is an architecture pattern. In 2015, Redux appeared, combining Flux with functional programming, and in a short time became the most popular front-end architecture.It doesn't care what library you use, it can be combined with React,Vue, or JQuery.

2. Start with a simple example

Starting with a simple example, let's create a new html page with the following code:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Make-Redux</title>
</head>
<body>
<div id="app">
  <div id="title"></div>
  <div id="content"></div>
</div>
<script>
  // Status of application
  const appState = {
    title: {
      text: 'This is a title',
      color: 'Red'
    },
    content: {
      text: 'This is a paragraph',
      color: 'blue'
    }
  };

  // renderer
  function renderApp(appState) {
    renderTitle(appState.title);
    renderContent(appState.content);
  }

  function renderTitle(title) {
    const titleDOM = document.getElementById('title');
    titleDOM.innerHTML = title.text;
    titleDOM.style.color = title.color;
  }

  function renderContent(content) {
    const contentDOM = document.getElementById('content');
    contentDOM.innerHTML = content.text;
    contentDOM.style.color = content.color;
  }

  // Render data to page
  renderApp(appState);
</script>
</body>
</html>

HTML content is simple. We define an appState data object, including the title and content properties, each with text and color. Then we define renderApp,renderTitle, renderContent rendering methods, and finally execute renderApp(appState) to open the page:

Although there is no problem with these writes, there is a big hidden danger that everyone can modify the shared state appState. In normal business development, a common problem is that a global variable is defined, and other colleagues may be overwritten and deleted without knowing. The problem is that the result of function execution is often unpredictable., it is very difficult to debug when there is a problem.

So how do we solve this problem? We can raise the threshold for modifying shared data, but we can't modify it directly. We can only modify some modifications that I allow.Thus, a dispatch method is defined, which is responsible for modifying the data.

function dispatch (action) {
    switch (action.type) {
      case 'UPDATE_TITLE_TEXT':
        appState.title.text = action.text;
        break;
      case 'UPDATE_TITLE_COLOR':
        appState.title.color = action.color;
        break;
      default:
        break;
    }
  }

In this way, we stipulate that all age data must be manipulated through the dispatch method.It accepts an object, temporarily called an action, and specifies that only the text and color of the title can be modified.In order to know which function modified the data, we can debug the breakpoint directly in the dispatch method.Greatly improve the efficiency of problem solving.

3. Separate store s and monitor data changes

Our appStore and dispatch are separated above. To make this pattern more versatile, we build a function, createStore, in one place that produces a store object, including state and dispatch.

function createStore (state, stateChanger) {
    const getState = () => state;
    const dispatch = (action) => stateChanger(state, action);
    return { getState, dispatch }
  }

We modified the previous code as follows:

let appState = {
    title: {
      text: 'This is a title',
      color: 'red',
    },
    content: {
      text: 'This is a paragraph',
      color: 'blue'
    }
  }

  function stateChanger (state, action) {
    switch (action.type) {
      case 'UPDATE_TITLE_TEXT':
        state.title.text = action.text
        break
      case 'UPDATE_TITLE_COLOR':
        state.title.color = action.color
        break
      default:
        break
    }
  }

  const store = createStore(appState, stateChanger)
  // First Render Page
  renderApp(store.getState());
  // Modify title text
  store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: 'Change the title' });
  // Modify Title Color
  store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'grey' });
   // Render the modified data to the page again
  renderApp(store.getState());

The code above is not difficult to understand: we generated a store with createStore, and you can see that the first parameter state is the shared data we previously declared, and the second stateChanger method is the dispatch method we previously declared to modify the data.

We then called the store.dispatch method twice, and finally called renderApp again to retrieve the new data to render the page, as follows: You can see that the title text and title have changed.

The problem is that every time dispatch modifies the data, we have to manually call the renderApp method to make the page change.We can put renderApp at the end of the dispatch method, so our createStore isn't generic enough because other Apps don't necessarily need to execute the renderApp method. Here we use a method that monitors data changes and then re-renders the page, termed the observer mode.

We modified the createStore as follows.

function createStore (state, stateChanger) {
    const listeners = []; // Empty Method Array
    // store Call once subscribe Just pass in the listener Method push To Method Array
    const subscribe = (listener) => listeners.push(listener); 
    const getState = () => state;
    // When store call dispatch Traverse when changing data listeners Array, which executes each of these methods, to achieve the effect of listening data re-rendering the page
    const dispatch = (action) => {
      stateChanger(state, action);
      listeners.forEach((listener) => listener())
    };
    return { getState, dispatch, subscribe }
  }

 

Modify the code in the previous section again as follows:

// First Render Page
  renderApp(store.getState());
  // Rerender page by monitoring data changes
  store.subscribe(()=>{
    renderApp(store.getState());
  });
  // Modify title text
  store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: 'Change the title' });
  // Modify Title Color
  store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'grey' });

We only need subscribe once after rendering the page for the first time, then dispatch modifies the data, and renderApp method is called again to achieve the effect of listening for data to automatically render the data.

3. Generate a shared deconstructed object to improve page performance

In the last section, each time we called the renderApp method, we actually executed the renderTitle and renderContent methods. We both modified the title data by dispatch, but the renderContent methods were also executed together, which executed unnecessary functions. There are serious performance problems. We can add some Log s to several rendering functions to see if they are actually notThat's true

 

function renderApp (appState) {
  console.log('render app...')
  ...
}
function renderTitle (title) {
  console.log('render title...')
  ...
}
function renderContent (content) {
  console.log('render content...')
 ...
}

The browser console prints as follows:

  

 

The solution is that before each rendering function is executed, we make a judgment about the data it passed in, determine whether the new data and the old data are the same, and return the same without rendering, otherwise render.

  // renderer
  function renderApp (newAppState, oldAppState = {}) { // Prevent oldAppState No incoming, so default parameters are added oldAppState = {}
    if (newAppState === oldAppState) return; // Data will not render without change
    console.log('render app...');
    renderTitle(newAppState.title, oldAppState.title);
    renderContent(newAppState.content, oldAppState.content);
  }
  function renderTitle (newTitle, oldTitle = {}) {
    if (newTitle === oldTitle) return; // Data will not render without change
    console.log('render title...');
    const titleDOM = document.getElementById('title');
    titleDOM.innerHTML = newTitle.text;
    titleDOM.style.color = newTitle.color;
  }
  function renderContent (newContent, oldContent = {}) {
    if (newContent === oldContent) return; // Data will not render without change
    console.log('render content...');
    const contentDOM = document.getElementById('content')
    contentDOM.innerHTML = newContent.text;
    contentDOM.style.color = newContent.color;
  }
  ...
  let oldState = store.getState(); // Cache Old state
  store.subscribe(() => {
    const newState = store.getState(); // Data may change, get new state
    renderApp(newState, oldState); // Put old and new state Inbound to Render
    oldState = newState // After rendering, NEW newState Become old oldState,Wait for next data change to render again
  })
...

The above code first caches the old state with oldState when subscribe, executes the method inside after dispatch to retrieve the new state again, then oldState and newState are passed into renderApp, and then saves the new state with oldState.

Okay, let's open the browser to see the effect:

 

The console prints only a few lines of logs for the first rendering, and the rendering function does not execute after the last two dispatch data.This means that oldState and newState are equal.

 

Through breakpoint debugging, newAppState and oldAppState are found to be equal.

The reason for this is that because objects and arrays are reference types, newState,oldState points to the same state object address and is always equal in each rendering function judgement, it return s.

Solution: AppState and newState are actually two different objects. We use ES6 syntax to copy appState objects shallowly. When dispatch method is executed, one new object overwrites the contents of the original title, and the rest of the property values remain unchanged.To form a shared data object, you can refer to one of the following demo s:

When we modify the stateChanger so that it modifies the data, instead of directly modifying the original data state, we create objects with the above shared structure:

function stateChanger (state, action) {
    switch (action.type) {
      case 'UPDATE_TITLE_TEXT':
        return { // Build a new object and return
          ...state,
          title: {
            ...state.title,
            text: action.text
          }
        }
      case 'UPDATE_TITLE_COLOR':
        return { // Build a new object and return
          ...state,
          title: {
            ...state.title,
            color: action.color
          }
        }
      default:
        return state // No modification, return original object
    }
  }

Because stateChanger does not modify the original object but returns an object, modify the dispatch method inside createStore to overwrite the original state by executing the return value of stateChanger(state,action), so that when subscribe executes the incoming method in the dispatch call, newState is the result of stateChanger().

function createStore (state, stateChanger) {
    ...
    const dispatch = (action) => {
      state=stateChanger(state, action);
      listeners.forEach((listener) => listener())
    };
    return { getState, dispatch, subscribe }
  }

Run the code again to open the browser:

Rerendering of content caused by store.dispatch for the last two times was found to be absent, optimizing performance.

4. Generalized Reducer

appState can be merged together

function stateChanger (state, action) {
    if(state){
      return {
        title: {
          text: 'This is a title',
          color: 'Red'
        },
        content: {
          text: 'This is a paragraph',
          color: 'blue'
        }
      }
    }
    switch (action.type) {
      case 'UPDATE_TITLE_TEXT':
        return { // Build a new object and return
          ...state,
          title: {
            ...state.title,
            text: action.text
          }
        }
      case 'UPDATE_TITLE_COLOR':
        return { // Build a new object and return
          ...state,
          title: {
            ...state.title,
            color: action.color
          }
        }
      default:
        return state // No modification, return original object
    }
  }

Modify the createStore method again:

function createStore (stateChanger) {
    let state = null;
    const listeners = []; // Empty Method Array
    // store Call once subscribe Just pass in the listener Method push To Method Array
    const subscribe = (listener) => listeners.push(listener);
    const getState = () => state;
    // When store call dispatch Traverse when changing data listeners Array, which executes each of these methods, to achieve the effect of listening data re-rendering the page
    const dispatch = (action) => {
      state=stateChanger(state, action);
      listeners.forEach((listener) => listener())
    };
    dispatch({}); //Initialization state
    return { getState, dispatch, subscribe }
  }

Initialize a local variable state=null, and finally manually call dispatch({}) once to initialize the data.

The stateChanger function can also be called a generic name: reducer.Why is it called reducer? Ruan Yifeng's Basic Usage of redux Inside is the explanation of reducder;

V: Redux Summary

The above is based on reading the "React.js Books" re-disc, through the above we introduced a simple example using native JS can roughly complete the Redux from zero to the following steps:

// Set one reducer
function reducer (state, action) {
/* Initialize state and switch case */
}
// generate store
const store = createStore(reducer)
// Rerender page by monitoring data changes
store.subscribe(() => renderApp(store.getState()))
// First Render Page
renderApp(store.getState())
// Freedom to follow dispatch Yes, the page is updated automatically
store.dispatch(...)

Follow the definition reducer->Generate store->Monitor data changes->dispatch page for automatic updates.

The two figures below also illustrate Redux's workflow very well

There are three main principles to follow when using Redux:

1. Unique data source store

2. store is read-only and cannot modify application state directly

3. Modification of application state is accomplished by pure function Reducer

Of course, not every project should use Redux. Some carefully share less data and it is unnecessary to use Redux, depending on the size and complexity of the project. When do you use Redux?To quote one sentence: When you are not sure whether to use Redux, do not use Redux.

Project Complete Code Address make-redux

6. Write at the end

Each tool or framework is created to solve a certain problem under certain conditions. After reading React.js several times, I finally get some understanding of some basic principles such as React and Redux. As a coder, I feel deeply that as a coder, I can not just CV, but remember that some framework API s will be used, whether they are used or not, let's know why, so that we can finish the project.Better optimization and more elegance in code writing.What are the errors? Please correct them. If you want to make a qualitative leap in technology, you need to learn more, think more, practice more and share your favor with you.

 

 

Reference material:

1.React.js Books - Beard Daha

2.React's Way to Advance - Xu Chao

3. Introduction to Redux (1): Basic Usage - Ruan Yifeng

Keywords: Javascript React Programming Vue JQuery

Added by jasraj on Wed, 15 May 2019 05:38:07 +0300