TNTWeb – The full name of Tencent News Zhongtai front-end team, partners in the group have practiced and accumulated experience in Web front-end, NodeJS development, UI design, mobile APP and other large front-end fields.

At present, the team mainly supports the front-end development of Tencent news business. Besides business development, some front-end infrastructure has been accumulated to enable business efficiency improvement and product innovation.

The team advocates open source construction, has a variety of technical masters, the team Github address: github.com/tnfe

The author of this article leng Ye project address: github.com/tnfe/clean-…

One, foreword

React has gone through nearly a hundred iterations from its inception to its latest v17 release. Around the design philosophy of one-way data flow, Redux state management based on Flux thought and Mobx based on responsive monitoring emerged. One emphasizes the unity of concept and the other emphasizes the perfection of performance experience. But through materialist dialectics we know that opposition and unity are the final form of the development of all things. Therefore, since React@v16.8.0, Hooks functions have been developed to complete the shortcomings of logical abstraction without changing their mental model, with which we can open up a whole new horizon of state management.

Second, the background

In the current MVVVM-centric software development model, we know that the essence of a view is the expression of data, and any data mutation will bring feedback on the view. When facing a large project development, in order to improve the efficiency of subsequent maintenance iterations, the first thing we need to do is to disassemble modules and make each part as fragmented and reusable as possible, which is also the primary concept of microcomponents.

What we’re actually fragmenting is the UI layer during the whole debunking process. For example, in a popover, there will be uniform design standards for a particular business, and only the copy changes; Or a large list, each time updated with metadata, the contours of the cards remain consistent. Then how to deal with the data? Just imagine if we follow the components, when a project becomes larger and larger, the scattered data and logic in various places will dramatically increase the entropy of the software, resulting in the subsequent requirements iteration, error detection, debugging and maintenance of exponentially more difficult. Therefore, centralization of data to a certain extent is the right development concept for the front end.

Three,

In React, we call the data corresponding to the view state. Schemes related to state management also experienced a slash-and-burn era. The best known is Redux, which has been criticized in terms of performance but has been used to the greatest extent in terms of correct thinking. It stores the data center as State in the store and issues an action triggering the Reducer to update through dispatch.

The design concept is great, but when we actually apply it to the project we find several problems:

  1. How is the architecture level organized? We had to introduce a lot of third party libraries, such as React-Redux, Redux-Thunk, Redux-Saga, etc., which added a lot of learning costs, as well as a lot of packages on mobile that cost a lot of space.
  2. How to avoid invalid rendering performance? After we bridge through react-Redux, those who have paid attention to the source code will find that the essence of redux update in React is variable promotion. The setState at the top level will be triggered after each dispatch by upgrading state. According to React’s update mechanism, this triggers the Render function on all child nodes.
/ / the Provider injection
import React from 'react'
import ReactDOM from 'react-dom'

import { Provider } from 'react-redux'
import store from './store'
import App from './App'

const rootElement = document.getElementById('root')
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)

/ / connect to use
import { connect } from 'react-redux'
import { increment, decrement, reset } from './actionCreators'

// const Counter = ...
const mapStateToProps = (state /*, ownProps*/) = > {
  return {
    counter: state.counter,
  }
}

const mapDispatchToProps = { increment, decrement, reset }
export default connect(mapStateToProps, mapDispatchToProps)(Counter)
Copy the code

The second solution is Mobx, which does an accurate update of the target component, but it’s a different genre and has a lot of fans but there are plenty of people who don’t like it. His core idea is that anything that comes from the application state should be automatically acquired. React doesn’t allow the parent to update the component, but rather the data bound to it. This responsive listening approach contradicts the React concept of single data flow.

// Declare observable status
import { decorate, observable } from "mobx";

class TodoList {
    @observable todos = [];
    @computed get unfinishedTodoCount() {
        return this.todos.filter(todo= > !todo.finished).length;
    }
}

// Declare the observing component
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import {observer} from 'mobx-react';

@observer
class TodoListView extends Component {
    render() {
        return <div>
            <ul>
                {this.props.todoList.todos.map(todo =>
                    <TodoView todo={todo} key={todo.id} />
                )}
            </ul>
            Tasks left: {this.props.todoList.unfinishedTodoCount}
        </div>}}const TodoView = observer(({todo}) = >
    <li>
        <input
            type="checkbox"
            checked={todo.finished}
            onClick={()= >todo.finished = ! todo.finished} />{todo.title}</li>
)

const store = new TodoList();
ReactDOM.render(<TodoListView todoList={store} />.document.getElementById('mount'));
Copy the code

Four, light and flexible scheme: clean-state

Maybe you can do it differently. First let’s look at what Hooks are doing:

  1. Solve the difficult problem of reusing logical state between components.
  2. Too many life cycles make components hard to understand.
  3. Eliminate the division between class and function components and simplify module definitions.

That hooks are essentially simplifying the mental curve of learning to use React, and taking logical abstraction one step further. Clean-state stands on the shoulder of this idea, saying goodbye to the concept of ReactContext and proposing a new way of managing State in an extremely streamlined way. With CS we have no more learning burden, no artificial organizational structure, a unified solution, no variable improvement in performance, and no Provider injection method so we can achieve precise module-level updates. Some of its features are listed below.

At CS, we embrace the principle of minimalism as much as possible, allowing development to build product buildings in the simplest way possible.

1. How to divide modules

In module division, it is recommended to distinguish by routing entry or data model, which is in line with the natural way of thinking.

Each module of state management is called module, managed in a single directory, and finally exported by index file.

|--modules
|   |-- user.js
|   |-- project.js
|   |-- index.js
Copy the code

2. How to define a module

In terms of definition, we didn’t do any more concepts and followed the most sensible approach to daily development.

State is the module state; Effect c. Reducer Returns the updated status.

// modules/user.js
const state = {
  name: 'test'
}

const user = {
  state,
  reducers: {
    setName({payload, state}) {
      return{... state, ... payload} } },effects: {
    async fetchNameAndSet({dispatch}) {
      const name = await Promise.resolve('fetch_name')
      dispatch.user.setName({name})
    }
  }
}

export default user;
Copy the code

3. How to register the module

All you need to do is call bootstrap in the module entry file. It will automatically concatenate multiple modules and return the useModule and Dispatch methods.

// modules/index.js
import user from './user'
import bootstrap from 'clean-state'

const modules = { user }
export const {useModule, dispatch}  = bootstrap(modules);
Copy the code

4. How to use modules

We useModule state or trigger execution methods via useModule and dispatch exported from the modules entry file.

// page.js
import {useCallback} from 'react'
import { useModule, dispatch } from './modules'

function App() {
  Const {user, project} = useModule(['user', 'project']) */
  const { user } = useModule('user')
  const onChange = useCallback((e) = > {
    const { target } = e
    dispatch.user.setName({name: target.value})
  }, [])

  const onClick = useCallback(() = > {
    dispatch.user.fetchNameAndSet()
  }, [])

  return (
    <div className="App">
      <div>
        <div>
          name: {user.name}
        </div>
        <div>Change the user name:<input onChange={onChange}></input>
        </div>
        <button onClick={onClick}>Obtaining a user name</button>
      </div>
    </div>
  );
}

export default App; 
Copy the code

5. How to access across modules

We injected rootState parameters into each Reducer and effect, allowing access to other module attributes. Effect also injected the Dispatch method, which can be called across modules.

 async fetchNameAndSet({dispatch, rootState, state, payload}) {
      const name = await Promise.resolve('fetch_name')
      dispatch.user.setName({name})
 }
Copy the code

6. Mix in mechanics

In many cases, there are common state, reducer or effect between multiple modules. Here, in order to prevent users from making repeated declarations in each module, we exposed the mixed methods externally.

// common.js
const common = {
  reducers: {
    setValue<State>({payload, state}: {payload: Record<string, any>, state: State}): State {
      return{... state, ... payload} } } }export default common;

// modules/index.js
import commont from './common'
import user from './user'
import { mixin } from 'clean-state';

// Mix Common's setValue method into the User module
const modules = mixin(common, { user })

// You can now call the dispatch.user.setValue method on other pages
export const {useModule, dispatch}  = bootstrap(modules);
Copy the code

7. How to debug

How to debug during development? CS provides a plug-in mechanism to support debugging of redux-devtool in a friendly way.

NPM install cs-redux-devtool */

// modules/index.js
import user from './user'
import bootstrap from 'clean-state'
import devTool from 'cs-redux-devtool'

bootstrapfrom.addPlugin(devTool)

...
Copy the code

After this brief configuration, you can track state changes using the Redux DevTool!

Five, technical implementation

Without further discussion, first of all, let’s take a look at the overall architecture of CS:

Module layer is divided into State, Reducer and Effect. We provide mixing mechanism for the public part. After the project is started, a Store is generated and a Container is initialized to synchronize data with the Store.

When we call useModule from page, Component, or hooks, we associate the corresponding module state with the object method and add the update function to the Container. So when the A page triggers the B module method, we can execute exactly B’s dependency render function.

Below we show the code execution of redux and CS without any optimization logic, and you can see that we have reduced all the useless component rendering.

The following figure is the packaging dependency diagram of my actual project. It can be seen that after Gzip compression, the overall size of CS is less than 1KB. I hope it can help the user experience and performance of C-side project development to be extreme ~

So how does all this work? I’m going to go through it step by step.

1, entry

// index.js
import bootstrap from './bootstrap';

export { default as mixin } from './mixin';
export default bootstrap;

Copy the code

First of all, let’s take a look at the entry file code. We only exported two apis, the first one is mixin for handling module mixing of public properties, and the second one is bootstrap for starting the state manager. Let’s take a look at the main process implementation of starting.

2, the bootstrap

// bootstrap.js
const bootstrap: Bootstrap = <Modules>(modules: Modules) = > {
  const container = new Container(modules);
  const pluginEmitter = newEventEmitter(); .return { useModule: useModule as any, dispatch };
};
Copy the code

The bootstrap parameter is a collection of modules, followed by the initialization of a container for caching and updating data states. PluginEmitter is part of the CS plugin mechanism and tracks the execution of all functions. We ended up exporting two methods, useModule to read module state, and Dispatch to distribute events.

These two methods are essentially exported uniformly in the index, and the reason for doing so is that we have multi-data center support here. Let’s look at the implementation in detail around these two apis.

3, useModule

// bootstrap.js
const bootstrap: Bootstrap = <Modules>(modules: Modules) = > {
  const container = new Container(modules);
  const pluginEmitter = newEventEmitter(); .return { useModule: useModule as any, dispatch };
};
Copy the code

The first is the implementation of the useModule. We see that the input parameter namespace is a string or an array of strings, and then we declare an empty state and provide the setState proxy for the assignment of the new object, which triggers the update of the associated component.

Finally, we bind the method and state to the Container object to implement the update in observer mode. The data that is returned is actually from the Container’s cache object. This logic is very simple and clear, so let’s look at the implementation of Dispatch.

4, dispatch

// bootstrap
const bootstrap: Bootstrap = <Modules>(modules: Modules) = >{...// The only module method call that is exposed to the outside world
  const dispatch: any = (nameAndMethod: string, payload: Record
       
        ,
       ,>) = > {
    const [namespace, methodName] = nameAndMethod.split('/');
    const combineModule = container.getModule(namespace);

    const { state, reducers, effects } = combineModule[namespace];
    const rootState = container.getRootState();

    // The side effects take precedence over the reducer execution
    if (effects[methodName]) {
      return effects[methodName]({ state, payload, rootState, dispatch });
    } else if (reducers[methodName]) {
      constnewState = reducers[methodName]({ state, rootState, payload, }); container.setState(namespace, newState); }};return { useModule: useModule as any, dispatch };
};
Copy the code

The Dispatch method takes two parameters, the first is the module and method name string of the call in a specific format similar to moduleName/function, and the second is the load object. According to nameAndMethod, we will fetch the corresponding module and method from the Container to call and execute.

In the execution process, effect takes priority over reducer and the required parameters are passed in. In the actual project development, considering the development efficiency and usage habits, we implemented a layer of encapsulation for dispatch, supporting the form of dispatch.module.fun.

5. Dispatch chain call

// bootstrap
const bootstrap: Bootstrap = <Modules>(modules: Modules) = > {
 
  const injectFns = (reducersOrEffects) = > {
    Object.keys(reducersOrEffects).forEach((key) = > {
      if(! dispatch[key]) dispatch[key] = {};const originFns = reducersOrEffects[key];
      const fns = {};
      Object.keys(originFns).forEach((fnKey) = > {
        fns[fnKey] = (payload: Record<string, any>) = >
          dispatch(`${key}/${fnKey}`, payload);
      });
      Object.assign(dispatch[key], fns);
    });
  };

  // Inject each module's reducer and effect method into the Dispatch
  const rootReducers = container.getRootReducers();
  constrootEffects = container.getRootEffects(); injectFns(rootReducers); injectFns(rootEffects); . };Copy the code

At the end of the method, we take out the rootReducers and rootEffects collection from the Container, encapsulate them twice according to the module through injectFns method, and proxy the wrapped method to dispatch itself, realizing the cascading invocation. The wrapped method takes only payload as an input parameter, which greatly improves user development efficiency and provides complete code hints in TS syntax.

6. Combine redux-devTool

The core of using Redux debugging tools on the PC side is to create a virtual Redux-store to synchronize data with our state management library. Here I developed a library such as CS-Redux-devTool separately. Let’s see how it works.

First, we instantiate a Redux store in the Install method, which automatically generates a reducer based on the modules we passed in. We then call the window.__redux_devtools_extension__ method to open the Chrome plugin, which is automatically injected into the current page context when our browser installs Redux-DevTools. Finally, we use PluginEmitter passed in to listen for status update events and synchronize them to the virtual Redux-Store.

import { createStore, combineReducers } from 'redux'

var reduxStore = null;
var actionLen = 0

function createReducer(moduleName, initState) {
  return function (state, action) {
    if (state === undefined) state = initState;

    const {newState, type = ' '} = action
    const [disPatchModule] = type.split('/')
    if (moduleName === disPatchModule && newState) {
      return newState
    } else {
      returnstate; }}; }function createReducers(modules) {
  var moduleKeys = Object.keys(modules);
  var reducers = {};
  moduleKeys.forEach(function (key) {
    const {state} = modules[key]
    reducers[key] = createReducer(key, state);
  });
  return reducers;
}

function injectReduxDevTool(reducers) {
  reduxStore = createStore(
    combineReducers(reducers),
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  );
}

function dispatchAction(actionForRedux) {
  if(reduxStore) { actionLen++; reduxStore.dispatch(actionForRedux); }}function install(modules, pluginEmitter) {
  const reducers = createReducers(modules)

  injectReduxDevTool(reducers)
  pluginEmitter.on('CS_DISPATCH_TYPE'.(action) = > {
    dispatchAction(action)
  })
}

export default install
Copy the code

Then, in clean-state, we will add the registered plug-ins to the plugins array. When the effect or reducer of the corresponding module is triggered, we will send the processed results to the public distributor to realize listening synchronization.

const bootstrap: Bootstrap = <Modules>(modules: Modules) = > {
  const container = new Container(modules);
  const pluginEmitter = new EventEmitter();

  // The only module method call that is exposed to the outside world
  const dispatch: any = (nameAndMethod: string, payload: Record
       
        ,
       ,>) = >{...// The side effects take precedence over the reducer execution
    if (effects[methodName]) {
      pluginEmitter.emit(DISPATCH_TYPE, {
        type: nameAndMethod,
        payload,
      });
      return effects[methodName]({ state, payload, rootState, dispatch });
    } else if (reducers[methodName]) {
      const newState = reducers[methodName]({
        state,
        rootState,
        payload,
      });
      container.setState(namespace, newState);

      // Sync state to plugin
      pluginEmitter.emit(DISPATCH_TYPE, {
        type: nameAndMethod, payload, newState, }); }}; . plugins.forEach((plugin) = >plugin(modules, pluginEmitter)); . };Copy the code

Six, the last

Clean-state embraces the correct design patterns and ideas of React, and achieves architectural design and view-level optimization through streamlined code. If you are a new React project, it is highly recommended that you use hooks pure functions to write and build your app. You will experience faster React development posture. You can learn about using CS whether it’s a logically complex project on the toB side or a high-performance project on the toC side.