Using concent, experience a journey of progressively refactoring react applications

Keywords: Javascript React REST Fragment Vue

In the traditional redux project, the state we write in the reducer is bound to get through to the store. We need to plan the definitions of state and reducer at the beginning. Is there any way to enjoy the benefits of the separation of ui and logic quickly without starting from the rules and regulations according to the text? This article starts with the ordinary react writing. When you receive a demand, you have a general definition of the interface of components in your mind. Then you can slip into the concent world and feel the gradual pleasure and the unique charm of the new api.

Demand has come

Last week, the weather was not very good. I remember several rains. But the sound insulation of Beijing Headquarters Building was so good that I didn't feel the wind and rain outside. When I was thinking about sorting out the existing codes, I received an ordinary demand, which was about to realize a bullet window.

  • There is an optional list of fields on the left. Click on any field and you will go to the right.
  • On the right side, there is a list of selected fields, which can be dragged up and down to determine the order of the fields in the table to determine the display order of the column fields, and can also be deleted to restore it to the optional list.
  • Click Save to store the user's field configuration to the back end. The next time the user uses the view form again, the user uses the configured display field to display.

This is a very common requirement. I believe that many code gods have finished reading the code prototype in their minds. Hey hey, but please read this article patiently and have a look at it. concent With the help of our slogan, how will your react application become more flexible and wonderful?

concent, power your react

Preparation

The product students expect to see the general effect prototype quickly, and I hope that the prototype can be the basic code of continuous reconstruction and iteration. Of course, we should take it seriously. We can't scribble a version for the purpose of intersection, so we need to quickly sort out the requirements and start preparing work.

Because a lot of projects write UI based on antd, after listening to the requirements, a shuttle box-like component pops up in my mind, but because the right side is a draggable list, consulting the following no similar components, then realize one by yourself, preliminary sorting out the following ideas.

  • Component named ColumnConfModal, And-based Modal, Card-based layout, And List to achieve the left selection list, react-beautiful-dnd based drag-and-drop api to achieve the right drag-and-drop list.

  • Because this pop-up window component is used by different tables on different pages, the incoming column definition data is different, so we use the way of events to trigger the pop-up window to open and pass the form id, open the pop-up window to get all the field definitions of the form, and the selected field data of the user for the cousin. Initialization of table metadata converges within Column Conf Modal.
  • Based on the interaction between the left and right sides of the table, roughly define the internal interface

1 moveToSelectedList (moved to the selected list)
2 moveToSelectable List (move to optional list)
3 save Selected List (save user's selected list)
4 handleDragEnd (when processing selected list order adjustment is completed)
5 Other slightly...

UI Implementation

Because registered as a concent component has the inherent ability to emit & on, and does not need to manually off, concent automatically helps you remove its event listener before the instance is destroyed, so we can easily listen to the openColumnConf event after the registration is completed.

First, we abandon all kinds of store and reducer definitions, quickly build a prototype based on class es, and register common components as concent components using register interface. Pseudo-code is as follows

import { register } from 'concent';

class ColumnConfModal extends React.Component {
  state = {
    selectedColumnKeys: [],
    selectableColumnKeys: [],
    visible: false,
  };
  componentDidMount(){
    this.ctx.on('openColumnConf', ()=>{
      this.setState({visible:true});
    });
  }
  moveToSelectedList = ()=>{
    //code here
  }
  moveToSelectableList = ()=>{
    //code here
  }
  saveSelectedList = ()=>{
    //code here
  }
  handleDragEnd = ()=>{
    //code here
  }
  render(){
    const {selectedColumnKeys, selectableColumnKeys, visible} = this.state;
    return (
      <Modal title="Setting Display Fields" visible={state._visible} onCancel={settings.closeModal}>
        <Head />
        <Card title="optional field">
          <List dataSource={selectableColumnKeys} render={item=>{
            //...code here
          }}/>
        </Card>
        <Card title="Selected fields">
          <DraggableList dataSource={selectedColumnKeys} onDragEnd={this.handleDragEnd}/>
        </Card>
      </Modal>
    );
  }
}

// The es6 decorator is still in the experimental stage, so it wraps the class directly.
// Equivalent to @register() on a class to decorate a class
export default register( )(ColumnConfModal)

It can be found that there is no difference between the inner of this class and the traditional react class. The only difference is that concent injects a context object ctx into each instance to expose the new feature api that concent brings to react.

Elimination Life Cycle Function

Because event monitoring only needs to be performed once, in our example we completed event openColumnConf listening registration in Compoonent DidMount.

Obviously, according to the requirements, we also need to write business logic here to obtain table column definition metadata and user's personalized column definition data.

  componentDidMount() {
    this.ctx.on('openColumnConf', () => {
      this.setState({ visible: true });
    });

    const tableId = this.props.tid;
    tableService.getColumnMeta(`/getMeta/${tableId}`, (columns) => {
      userService.getUserColumns(`/getUserColumns/${tableId}`, (userColumns) => {
        //Calculate selectedList selectable List based on column user Columns
      });
    });
  }

All concent instances can define setup hook functions, which are called only once before the initial rendering.

Now let's replace this life cycle with setup

  //setup defined in class with a $$prefix
  $$setup(ctx){
    //Here we define on listeners to actually listen on events after the component is mounted
    ctx.on('openColumnConf', () => {
      this.setState({ visible: true });
    });

    //The tag dependency list is an empty array, which is performed only once in the component's initial rendering
    //Simulate componentDidMount
    ctx.effect(()=>{
      //service call balabala.....
    }, []);
  }

If you are familiar with hook, do you see that the effect API grammar in setup is somewhat similar to useEffect?

The execution time of effect and useEffect is the same, that is, after each component is rendered, the effect only needs to be invoked once in setup, which is equivalent to static and has more performance improvement space. Suppose we add a requirement, every time vibible becomes false, we report back-end operation log, which can be written as

    //Fill in the name of the key in the dependency list to indicate that side effects are triggered when the value of the key changes
    ctx.effect( ctx=>{
      if(!ctx.state.visible){
        //The latest visible is now false.
      }
    }, ['visible']);

So far as effect is concerned, there's too much to talk about. Let's go back to the components of this article.

Upgrade status to store

We hope that the state changes of components can be recorded to facilitate the observation of data changes. so, first we define a sub-module of store called ColumnConf.

Define its sate as

// code in ColumnConfModal/model/state.js
export function getInitialState() {
  return {
    selectedColumnKeys: [],
    selectableColumnKeys: [],
    visible: false,
  };
}

export default getInitialState();

Then load the configuration using the configuration interface of concent

// code in ColumnConfModal/model/index.js
import { configure } from 'concent';
import state from './state';

// Configuration module ColumnConf
configure('ColumnConf', {
  state,
});

Notice here that it's easy for us to maintain the business logic in the model by letting the model follow the component definition.

The whole store has been loaded into window.sss by concent. In order to view the store conveniently, Dangdang, you can open the console and view the latest data of each module of the store directly.

Then we registered the class as a component of the'configuration module ColumnConf', and now the state declaration in the class can be eliminated directly by us.

import './model';//Refer to the model file to trigger the model configuration to concent

@register('ColumnConf')
class ColumnConfModal extends React.Component {
  // state = {
  //   selectedColumnKeys: [],
  //   selectableColumnKeys: [],
  //   visible: false,
  // };
  render(){
    const {selectedColumnKeys, selectableColumnKeys, visible} = this.state;
  }
}

You may have noticed that if such violent annotations were removed, would the code in render go wrong? Rest assured, no, concent component state and store are naturally connected, the same setState is also connected to store, let's install a plug-in concent-plugin-redux-devtool first.

import ReduxDevToolPlugin from 'concent-plugin-redux-devtool';
import { run } from 'concent';

// StorConfig configuration is outlined. Details can be found on concent website.
run(storeConfig, {
    plugins: [ ReduxDevToolPlugin ]
});

Note that the concept-driven ui rendering principle is totally different from redux. The core logic part is not wrapped on redux. It has nothing to do with redux. It just bridges the redux-dev-tool plug-in to assist in state change recording. Don't misunderstand it, no redux, concent1. It works well, but because concent provides a perfect plug-in mechanism, why not make use of the community's existing excellent resources? It's hard to duplicate meaningless wheels ()b...

Now let's open chrome's redux plug-in to see how it works.

The above figure contains a lot of ccApi/setState, because there are still many logic not extracted from reducer, dispatch /*** style type is dispatch call, we will mention later.

In this way, the state transition is much better than window.sss, because SSS can only look at the latest state.

Now that redux-dev-tool is mentioned here, let's take a quick look at what the data submitted by concent looks like.

Five fields can be seen in the figure above. renderKey is used to improve performance. We can leave it unknown. Here we will talk about the other four. Module represents the name of the module to which the modified data belongs, committedState represents the status of submission, sharedState represents the status of sharing to store, and UniqueKey represents the status of triggering data modification. Instance id.

Why distinguish committedState from sharedState? Because setState calls allow the submission of its own private key (that is, key not declared in the module), committedState is the entire state to be redistributed to the caller, while sharedState is distributed to other cc component instances that belong to the module value after synchronization to the store.

Here I borrow an illustration from the official website.

So we can declare other non-module key s in the component and get them in this.state.

@register('ColumnConf')
class ColumnConfModal extends React.Component {
   state = {
        _myPrivKey:'i am a private field value, not for store',
   };
  render(){
      //Here, both module data and private data are fetched.
    const {selectedColumnKeys, selectableColumnKeys, visible, _myPrivKey} = this.state;
  }
}

Decoupling Business Logic and UI

Although the code works properly and the state is connected to the store, we find that the class has become bloated. It is fast and convenient to use setState, but the cost of later maintenance and iteration will gradually increase. Let's take the business to reduder.

export function setLoading(loading) {
  return { loading };
};

/** Move to Selected List */
export function moveToSelectedList() {
}

/** Move into the optional list */
export function moveToSelectableList() {
}

/** Initialization list */
export async function initSelectedList(tableId, moduleState, ctx) {
  //You can call it here without using the string ctx.dispatch('setLoading', true), although this is also valid.
  await ctx.dispatch(setLoading, true);
  const columnMeta = await tableService..getColumnMeta(`/getMeta/${tableId}`);
  const userColumsn = await userService.getUserColumns(`/getUserColumns/${tableId}`);
  //Computing selectedColumn Keys selectable Column Keys

  //Just return the fragment state that needs to be set to the module
  return { loading: false, selectedColumnKeys, selectableColumnKeys };
}

/** Save the selected list */
export async function saveSelectedList(tableId, moduleState, ctx) {
}

export function handleDragEnd() {
}

Configuration of reducer with concent's configure interface

// code in ColumnConfModal/model/index.js
import { configure } from 'concent';
import * as reducer from 'reducer';
import state from './state';

// Configuration module ColumnConf
configure('ColumnConf', {
  state,
  reducer,
});

Do you remember the setup above? Setup can return an object. The result will be collected in settings. Now let's make some modifications, and then look at the class. Is the world quieter?

import { register } from 'concent';

class ColumnConfModal extends React.Component {
  $$setup(ctx) {
    //Here we define on listeners to actually listen on events after the component is mounted
    ctx.on('openColumnConf', () => {
      this.setState({ visible: true });
    });

    //The tag dependency list is an empty array, which is performed only once in the component's initial rendering
    //Simulate componentDidMount
    ctx.effect(() => {
      ctx.dispatch('initSelectedList', this.props.tid);
    }, []);

    return {
      moveToSelectedList: (payload) => {
        ctx.dispatch('moveToSelectedList', payload);
      },
      moveToSelectableList: (payload) => {
        ctx.dispatch('moveToSelectableList', payload);
      },
      saveSelectedList: (payload) => {
        ctx.dispatch('saveSelectedList', payload);
      },
      handleDragEnd: (payload) => {
        ctx.dispatch('handleDragEnd', payload);
      }
    }
  }
  render() {
    //Take these methods out of settings
    const { moveToSelectedList, moveToSelectableList, saveSelectedList, handleDragEnd } = this.ctx.settings;
  }
}

Love class, love hook, let the two coexist in harmony

The react community has vigorously promoted the Hook revolution, allowing people to gradually replace class components with Hook components, but essentially Hook escaped from this and streamlined the dom rendering hierarchy, but it also brought a large number of temporary anonymous closures to be created repeatedly during the existence of components.

Let's see how concent can solve this problem. It mentioned above that setup support returns results, which will be collected in settings. Now let's change the class component bar into a Hook component by slightly adjusting the code.

import { useConcent } from 'concent';

const setup = (ctx) => {
  //Here we define on listeners to actually listen on events after the component is mounted
  ctx.on('openColumnConf', (tid) => {
    ctx.setState({ visible: true, tid });
  });

  //The tag dependency list is an empty array, which is performed only once in the component's initial rendering
  //Simulate componentDidMount
  ctx.effect(() => {
    ctx.dispatch('initSelectedList', ctx.state.tid);
  }, []);

  return {
    moveToSelectedList: (payload) => {
      ctx.dispatch('moveToSelectedList', payload);
    },
    moveToSelectableList: (payload) => {
      ctx.dispatch('moveToSelectableList', payload);
    },
    saveSelectedList: (payload) => {
      ctx.dispatch('saveSelectedList', payload);
    },
    handleDragEnd: (payload) => {
      ctx.dispatch('handleDragEnd', payload);
    }
  }
}

const iState = { _myPrivKey: 'myPrivate state', tid:null };

export function ColumnConfModal() {
  const ctx = useConcent({ module: 'ColumnConf', setup, state: iState });
  const { moveToSelectedList, moveToSelectableList, saveSelectedList, handleDragEnd } = ctx.settings;
  const { selectedColumnKeys, selectableColumnKeys, visible, _myPrivKey } = ctx.state;

  // return your ui
}

Here I would like to thank Mr. Youyuxi for this article. Vue Function-based API RFC It gives me great inspiration. Now you can see that all the methods are defined in setup. When you have a lot of components, it is obvious to reduce the pressure on gc.

Since they are written in a highly consistent way, is it very natural to move from class to Hook? We don't really need to argue about who's better, just according to your personal preferences. Even if you don't like classes one day, in concent's code style, the cost of refactoring is almost zero.

Using components

We defined an on event openColumnConf above, so when we refer to the component ColumnConfModal in other pages, of course we need to trigger this event to open its pop-up window.

import { emit } from 'concent';

class Foo extends React.Component {
  openColumnConfModal = () => {
    //If this class is a concent component
    this.ctx.emit('openColumnConfModal', 3);
    //If not, you can call the top-level api emit
    emit('openColumnConfModal', 3);
  }
  render() {
    return (
      <div>
        <button onClick={this.openColumnConfModal}>Configure Visible Fields</button>
        <Table />
          <ColumnConfModal />
      </div>
    );
  }
}

In the above way, if there are many other pages that need to introduce Column Conf Modal, we need to write an openColumn Conf Modal. We can abstract this open logic into modal service, which is used to open all kinds of pop windows, and avoid seeing the constant string openColumn Conf Modal in business.

//code in service/modal.js
import { emit } from 'concent';

export function openColumnConfModal(tid) {
  emit('openColumnConfModal', tid);
}

Now you can use components to trigger event calls like this

import * as modalService from 'service/modal';

class Foo extends React.Component {
  openColumnConfModal = () => {
    modalService.openColumnConfModal(6);
  }
  render() {
    return (
      <div>
        <button onClick={this.openColumnConfModal}>Configure Visible Fields</button>
        <Table />
        <ColumnConfModal />
      </div>
    );
  }
}

epilogue

The above code is valid at any stage. To understand the online demo of incremental refactoring, you can Click here. More online sample lists Click here.

Since this topic is mainly about progressive refactoring components, other features such as sync, computed$watch, renderKey, etc., will not be explained here. Please wait for the next article.

If the inspector likes it, he will come. Stars Well, concent is dedicated to bringing new coding experiences and functional enhancements to react. Please look forward to more features and ecological periphery.

Posted by majocmatt on Sun, 01 Sep 2019 22:06:23 -0700