welcome star

Your star will be my greatest spiritual encouragement. Welcome to Star 🥺🥺🥺

sequence

Concent is a framework designed to provide state management services for React. It extracts the essence of existing frameworks and best practices recognized by the community. With good module design, It not only ensures the best performance of React but also allows users to decouple the relationship between UI logic and business logic very flexibly. Improve code readability, maintainability, and extensibility as a whole.

Concent carries the following features

  • Core API few and simple, powerful, easy to use, small invasion, easy to debug
  • A single data source that provides global modularity
  • Support for 0 intrusion, incremental refactoring of existing React code
  • Features such as event bus, computed, Watch, and bidirectional binding are extended to components
  • Perfect support for the Function component
  • Fine-grained state subscriptions are supported based on reference location and state broadcasting, and render efficiency is outstanding
  • Support for middleware that extends your personalized plug-in to handle data changes
  • React 0.10+ Any version;

React provides users with a more comfortable and simple encoding experience

Elaborate module design concept

state

First of all, data is the soul of a module, carrying the most basic character description of your function module. Without data, all upper-level business functions are empty talk, so state is the definition that must be included in a module.

reducer

The flexibility of data modification is one of the surprises concent provides to users. Because the core of Concent is to take over setState for state management, users can enjoy the benefits of state management without immediate modification of existing code when they connect to Concent. Similarly, Concent also supports user-defined reducer function to modify state, which is a recommended best practice and can completely decouple UI rendering from business logic. Because reducer function is essentially a variant of setState, it is important to always return the fragment state that needs to be updated. Since Concent supports the reducer functions to call each other and combine them arbitrarily, it allows users to cut the update granularity of the Reducer function to the state as needed, form a chain call relationship, and trigger the Reducer function through the dispatch handle

If the chain call level is too deep, it will cause many renders. As shown in the figure above, three functions return new fragment states, resulting in three renders. Therefore, Concent also provides a lazyDispatch handle so that users can have one more choice to trigger the call to the Reducer function. Concent automatically caches and merges all states belonging to the same module in the current call chain during the transfer process, and does not commit once until the call chain ends

computed

Computed provides an entry point to define functions that need to perform calculations on keys that change. In general, most state data is not directly needed for UI rendering, and it is usually formatted or converted, but it is not necessary to perform these operations every time before rendering. Computed only computes on keys that have changed and caches the results.

watch

The biggest difference between watch and computed is that you do not need to return a specific result, but you can define watch functions for keys that you care about changing asynchronously

init

If an instance of the module exists, Concent will automatically broadcast the state to the corresponding instance. Similarly, if it does not, concent will automatically broadcast the state to the corresponding instance. When some instances of the module are generated, those instances are synchronized to the latest state of the module, so when we have some state that is not dependent on the instance being mounted and trigger componentDidMount to get it, we can promote the initialization of the state to the module init

Flexible module and component mapping

The React component is registered as a Concent component (also known as the Concent class) through the register function.

Registration, you can specify the exclusive module, theory, we should keep clean component and module corresponding relation, that is, a component to belong to a module, consumption is the module of data, operation of the module’s reducer function, but the actual scene may have many components are spending and modify data across multiple modules, So Concent also allows you to specify other modules to which the component is connected via the Connect definition. The only difference is that the context of the call handle by default points to your own module. If you need to call methods of other modules, you need to display the specified module name

@register('Foo', {module:'foo', connect:{bar:The '*'}})
class Foo extends Component(){
  onNameChange = (name)=>{
    this.$$dispatch('changeName', name); // changeName (this.$) from reducer foo is called by default$dispatch('bar/changeName', name); Call the changeName method in the Reducer of the bar module to modify the data of the bar module}Copy the code

CcClass
setState
state
connectedState

Therefore, on the whole, components and stores will form a clear and clear structure network, which is conducive to users’ orderly division of service modules in the early stage of a large React project and flexible adjustment of module definitions in the middle stage

Friendlier function support

After the hook proposal was implemented, the existing React community has gradually shifted from class Component to function Component. However, as Vue function-based API RFC says, Hook obviously needs to create a lot of temporary functions and generate a large number of closures, and provides auxiliary functions such as useMemo/useCallback to solve the problem of excessive updates or capture of expired values, etc. The setup scheme is proposed, each component instance will only be called once during initialization. The state is stored in the closure of setup() by reference.

The setup mechanism is called only once during component instantiation. Methods can be defined and returned. These methods are collected in the Settings object of the context. Additionally, it allows you to define effect, computed, and watch functions in setup (of course, these are instantiation-level computed and watch)

Online sample

The UI definition

const AwardPanelUI = (props) => {
  returnDisplayBonus: {props. DisplayBonus}</div> </div>); };Copy the code

The setup is defined

Const setup = CTX => {ctx.defineEffect(CTX => {ctx.dispatch(CTX => {ctx.dispatch(CTX => {ctx.dispatch('init'); // Returns the cleanup function that is triggered when the component is unloadedreturn () => ctx.dispatch('track'.'user close award panel')}, []); /** Also supports function ctx.defineWatch(CTX =>{return{... }}); */ ctx.definewatch ({// This observation function is triggered when the value of key inputCode changes'inputCode':(nevVal)=> ctx.setState({msg:'inputCode into'+nevVal }) }); Ctx.definecomputed ({// This calculation function is triggered when the value of key inputCode changes'inputCode':(newVal)=>`${newVal}_${Date.now()}`}); Const handleStrChange = (e) => {const inputCode = e.currenttarget.value; // Ctx. dispatch('handleInputCodeChange', inputCode); // ctx.reducer.award.handleInputCodeChange(inputCode); } // define init function const init = ctx.reducer.award. //const init = ()=> ctx.dispatch('init'); //setup will place the result returned to Settingsreturn { handleStrChange, init };
}
Copy the code

MapProps definition

// mapProps is called every time the function component is rendered, Const mapProps = CTX => {// Remove bonus calculation results const displayBonus = ctx.modulecomputed.bonus; Const {handleStrChange, init} = ctx. Settings; Const {inputCode, awardList, mask, MSG} = ctx.moduleState; // Get the computed value of the module key from refConnectedComputed const {inputCode:cuInputCode} = ctx.refcomputed. // The result is mapped to the props of the componentreturn { msg, cuInputCode, init, mask, inputCode, awardList, displayBonus, handleStrChange }
}
Copy the code

Join function component

const AwardPanel = connectDumb({setup, mapProps, module:'award'})(AwardPanelUI);
Copy the code

Is hook really the answer

Have the support of the setup, which can be promoted to static context API will use method, without the need to redefine repeatedly, also does not exist a large number of the temporary closure of problem, at the same time, based on the functional method and can be more flexible separation and combination you U code with the business, the setup function at the same time, after further abstraction, It can also be reused elsewhere.

Functional programming is also better for typescript type derivation. Concent supports functional components that allow users to choose between classes and functions on demand. Concent also allows you to define state for local state management. So a connectDumb wrapped function component can read and write both local and Store state, so why not hook?

Const AwardPanel = connectDumb(const AwardPanel = connectDumb(const AwardPanel = connectDumb(const AwardPanel = connectDumb(const AwardPanel = connectDumb(const AwardPanel = connectDumb()))localName:1});
  state:{localName:1},
  setup, 
  mapProps, 
  connect:{award:The '*'}
})(AwardPanelUI);

//code in setup
const setup = ctx =>{
  const changeLocalName = name => ctx.setState({localName});
  return {changeLocalName};
}

//code in mapProps
const mapProps = ctx =>{
  const localName = ctx.state.localName;
  return {localName}; 
}
Copy the code

Focus more on the architecture of the use experience

You don’t need to modify any of the existing React application code. You just need to start Concent and use it. You don’t need to wrap providers and other components in the top layer to provide global context. Since Concent automatically maintains its own global context, you can understand that The Relationship between Concent and React is parallel, not nested or wrapped. The only thing to note is that concent should be started first before rendering the React application.

Separate module configuration

Concent does not require you to configure module definitions at startup. It allows you to configure modules by calling configure when you define components, which greatly improves the coding experience of defining page models or Component Models.

. | ____page | | ____Group | | | ____index. Js | | | ____model/page/definition model | | | ____reducer. Js / / optional | | | ____index. Js | | | ____computed. Js / / optional | | | ____state. Js / / will contain | | | ____watch. Js / / optional | | | ____init. Js / / optional | | | | ____... / / all kinds of page component definition | | ____App. CSS | ____index. Js | ____utils | | ____... | | ____index. CSS ____models / / all kinds of business model definition | | ____home | | | ____reducer. Js | | | ____index. Js | | | ____computed. Js | | |____state.js | |____components | |____Nav.js | |____router.js |____logo.png |____assets | |____... | ____run - cc. Js / / start the concent, the entrance to the index. The first line in the js calls | ____App. Js | ____index. Js / / project entry documents | ____services | | ____...Copy the code

As an example, the page component Group contains its own model, which defines the module to concent in model/index.js.

// code in page/Group/model/index.js
import state form './state';
import * as reducer form './reducer';
import * as computed form './computed';
import * as watch form './watch';
import init form './init';
import {configure} from 'concent'; // configure the module into 'concent', named as'group'
configure('group', {state, reducer, computed, watch, init});
Copy the code

Before the Group component is exposed to the outside world, introduce the Model

import './model';

@register('GroupUI', {module:'group'})
export default class extends Component {

}
Copy the code

This way of organizing the code makes it possible for users to publish a concent component with a complete Model definition to NPM. Other users can simply install the component in the Concent application and use the component directly. They don’t even use the component’s UI logic, they just register their newly written component into the module that the component carries. Completely reuse all definitions of modules except UI.

Module cloning

For existing modules, sometimes we want to completely reuse all the definitions in the module, but the runtime is completely isolated. The dumbest way is to copy all the code in the target module, give it a new name, and configure it into Concent. But what if you have 10, 20, or more components that want to reuse logic but maintain runtime isolation? Concent provides the cloneModule function to help you do this. The cloneModule function actually makes a deep copy of state. The rest is just function definitions, so the new module points to references to those functions.

With the feature that cloneModule can call at any time at run time, you can even write a factory function that dynamically creates and unbinds components of a new module!

//makeComp.js
import existingModule from './demoModel';
import { register, cloneModule } from 'concent'; const module_comp_= {}; // Records whether a module has a corresponding component class Comp extendsComponent() {/ /... }export makeComp(module, CompCcClassName){
  let TargetComp = module_comp_[module];
  if(TargetComp) returnTargetComp; // Clone a new module based on the existing modulecloneModule(module, existingModule); // Because module is not repeatable, ccClassName is also not repeatable, // if the user does not explicitly specify ccClassName, Can default ccClassName is equal to the module value const ccClassName = CompCcClassName | | module; TargetComp = register(ccClassName, {module})(Comp); Module_comp_ [module] = TargetComp;return TargetComp;
}

Copy the code

Concent component workflow

ConcentDumb abstracts the life cycle of the React component in a clever way. You can use defineEffect, defineWatch, defineComputed and other interesting features without having to pay attention to the life cycle of the class component. You don’t have to deal with this anymore, so the function component and the class component have exactly the same functionality.

Compare mainstream state management solutions

As we know, there are two main types of existing state frameworks. One is the global state management represented by Redux based on the mode of data subscription, and the other is the active change interception and state synchronization represented by MOBx by transforming data into observable objects.

vs redux

Let’s talk about Redux, the current react state manager.

Redux unspeakable reducer

Users who have written redux, or redux Wrapper (dVA, rematch, etc.), should be well aware of one of redux’s conventions: Reducer must be a pure function, and if the state changes, the original state must be deconstructed to return a new state

// fooReducer.js
export default (state, action)=>{
  switch(action.type){
    case 'FETCH_BOOKS':
      return{... state, ... action.payload}; default:returnstate; }}Copy the code

The pure function has no side effects and is easy to be tested, which has been mentioned many times. We wrote and wrote, and there are two kinds of writing methods for actionCreator and Reducer.

  • One is to put both asynchronous request logic and post-request data processing logic inactionCreatorWrite, and then encapsulate the data aspayload, the calldispatchAnd send the data to the correspondingreducer.

This type of code is gradually reduced to the reducer, which is full of operations to deconstruct the payload, synthesize the new state and return it. The business logic is all in actionCreator, which has a serious drawback, because the business logic is all in actionCreator. All the type values in the Reducer function have become a bunch of names like CURD, saveXXModel, updateXXModelXXField, setXXX, deleteXXX and other names that seem to have no relationship with business logic at all. The reducer function is flooded with a large number of types, and the type values recorded by our state debugging tool are exactly named in these ways. When you see the list of types corresponding to the change process in the debugging tool, you only get information about which data has been changed, but you have no idea where these states sent payloads and caused the changes. Even if we want to know what interaction of those UI views caused the state change, we can only start from the reducer type keyword of the code as the search criteria and reverse consult other code files.

  • There is another way to letactionCreatorAs thin as possible, send synchronous actions to return directly, and asynchronous actions use the thunk function orredux-sagaAnd so on the third party library to do processing, get the data as early as possibleactionObject, sent toreducerThe function,

In this mode, our actionCreator is thinner and does the same thing as its name, except generating action objects. At the same time, because our data processing logic is in the reducer, our type value can be named according to the caller’s motivation or scenario. For example, formatTimestamp, handleNameChanged, handelFetchedBasicData, etc., but due to the architecture of Redux, your UI will inevitably trigger actions that go through two steps, In the first step, action is generated by actionCreator. In the second step, new state is synthesized by reducer processing payload. Therefore, the naming of actionCreator and reducerType will probably be changed into the same name, such as fetchProductList, which appears once in actionCreator, in order to facilitate the introduction of contextual information in future reading. And then again in reducerType

Concent Reduced complexity to a reducer

The reducer’s role in Concent is to return a new fragment view, so you can think of it as a partialStateGenerator function that you can declare as a normal function

//code in fooReducer.js
function fetchProductList(){
}
Copy the code

It can also be async or generator functions

async function fetchProductList(){
}
Copy the code

If your function needs several requests to complete the rendering, but each step needs to trigger a view update in time, Concent allows you to combine functions freely. If the reducer functions belong to the same module, they can be called directly based on the function signature

function _setProductList(dataList){
  return {dataList};
}

// Get the basic data of the product list
async function fetchProductBasicData(payload, moduleState, ctx){
  const dataList = await api.fetchProductBasicData();
  return {dataList};// Return data to trigger rendering
  // or ctx.dispatch(_setProductList, dataList);
}

// Get product list count statistics, statistics is slow, batch pull (pseudocode)
async function fetchProductStatData(payload, moduleState, ctx){
  const dataList = moduleState.dataList;
  // do batch pull statistics ids, logic slightly......
  const batchIdsList = [];
  const len = batchIds.length;

  for(let i=0; i<len; i++){
    const ids = batchIdsList[i];
    const statDataList = await api.fetchProductBasicData(ids);

    // the logic is...... Cursor start and end, change the corresponding data statistics
    let len = statDataList.length;
    for(let j=0; j<len; j++){
      dataList[j+cursor].stat = statDataList[j];// Assign statistics
    }
    await ctx.dispatch(_setProductList, dataList);// Modify dataList data to trigger rendering}}// A complete product list contains both basic data and statistics, which are pulled in batches
async function fetchProductList(payload, moduleState, ctx){
  await ctx.dispatch(fetchProductBasicData);
  await ctx.dispatch(fetchProductStatData);
}
Copy the code

Now you just need to call it in the view instance, okay? Dispatch triggers the update

// An instance belonging to the product module calls this.$$dispatch('fetchProductList'); // Instances belonging to other modules call this.$$dispatch('product/fetchProductList');
Copy the code

It can be seen that such code organization is more in line with the caller’s use intuition. There are no redundant operations, mutual call or multi-level call. The code can be organized according to the most intuitive ideas of the developer, and it is very convenient to constantly adjust the reducer in the latter reconstruction module in the later period.

Concent emphasizes the return of the fragment state to be updated rather than the return of a new state. From the working principle, because the concent class marks the observation key information, the smaller and more accurate the state submitted by reducer will be, the more helpful it is to speed up the search for instances concerned about the change of these key values. Concent also allows you to define watch and computed functions on the key, so keeping the commit state minimal doesn’t trigger redundant watch and computed logic. From a business perspective, the new state you return must conform to the description of the function name. When we visually interpret a function, we generally know what processing is done and what new fragment state is returned to Concent. It is linear thinking and concent can update the rest of the UI logic.

Perhaps readers have noticed that Redux advocates the advantages of pure functions that are easy to test and have no side effects? Can it be reflected in concent? In fact, there is no need to worry about this. As you can see from the reducer example above, whether the reducer function has any side effects depends entirely on how you declare the function. You can call either pure functions or side effects directly in your UI. Depending on your usage scenario, the function name is Type. Is the world much quieter without actionCreator?

Further explore the essence of reducer. As mentioned above, reducer is the partialStateGenerator function for Concent, so if you don’t like the dispatch school, you can directly define a function and call it. Do not need to be placed under the Reducer definition of the module.

function inc(payload, moduleState, ctx){
  ctx.dispatch('bar/recordLog');// The recordLog method in the Reducer of the bar module is triggered asynchronously without await
  return {count: moduleState.count +1 };
}

@register('Counter'.'counter')(Counter)
class Counter extends Component{
  render(){
    return <div onClick={()= >this.? invoke(inc}>count: {this.state.count}</div>}}Copy the code

Not only is writing experience friendly, but concent manages state based on reference collection, so the state debugging tool provided by Concent can precisely locate what state is submitted, which methods are invoked, and which instances are triggered by each state change.

Redux’s complex user experience

Although the redux core code is simple, providing helper functions such as Twitter creators and bindActionCreators, and providing the Connect function as the react-Redux bridge, The whole process had already made the code look bloated, so dVA, Rematch and other Redux wrapper were used to simplify the complexity. However, no matter how it was packaged, From the bottom, for the update process of REdux, any action dispatch must go through all the reducer, and the state returned by reducer must go through all the components that connect to the corresponding state of this Reducer. After a shallow comparison (which is why Redux must return a new state with the help of destruct syntax), it decides whether to update its wrapped child components.

const increaseAction = {
  type: 'increase'
};

const mapStateToProps = state => {
  return {value: state.count}
};

const mapDispatchToProps = dispatch => {
  return{ onIncreaseClick: () => dispatch(increaseAction); }}; const App = connect( mapStateToProps, mapDispatchToProps )(Counter);Copy the code

Concent’s straightforward hands-on experience

Registered as a component of the Concent class, it has the inherent ability to manipulate stores, and the data will be injected directly into state

// we can use this.$directly in Counter$dispatch('increase')
class Counter extends Component{
  render() {return <div onClick={()=> this.$$dispatch('increase')}>count: {this.state.count}</div>
  }
}

const App = register('Counter'.'counter')(Counter);
Copy the code

You’ll notice that Concent directly connects? The dispatch method, bound to this, because Concent uses a reverse inheritance strategy by default to wrap your components, resulting in fewer nested components and thus fewer Dom levels.

Store state is also injected directly into this, because the transition is synchronized to store from the start of the setState call, so injecting state is a natural thing to do.

Of course, Concent also allows users to declare other non-store keys on instance state, so that their values are private. If users don’t like state contamination and don’t like reverse inheritance, they can also write as

class Counter extends Component{
  constructor(props, context){
    super(props, context);
    this.props.$$attach(this);
  }
  render() {return(
      <div onClick={()=> this.props.$$dispatch('increase')}>
        count: {this.props.$$connectedState.counter.count}
      </div>
    )
  }
}

const App = register('Counter', {connect:{counter:The '*'}, isPropsProxy:true} )(Counter);
Copy the code

vs mobx

Mobx is a function responsive programming library. Mobx-react bridge library turns React into a fully responsive programming mode. Mobx converts defined states into observables, so users only need to modify data, and Mobx automatically updates corresponding views. Mobx makes React a vUe-like writing experience, with automatic data mapping views and no need for explicit setState calls.

In essence, all MVVM frameworks are built around data and views. React takes single data streams to the extreme. Mobx has implemented a patch for React auto-mapping views. Mobx uses the same approach as VUE. It uses push mode for change detection, which intercepts data getters and setters. When users modify data, the framework knows that the data has changed. The setState and setData interfaces are exposed to users so that users can actively submit change data before they know that data has changed.

The only difference is that the pull process is smarter. When a component instance is created, Concent already knows the following information:

  • Which module the instance belongs to
  • Example to observe which key values change in this module
  • What additional modules are connected to the instance

References to instances are also collected and held until uninstalled.

So you can plug your React code directly into Concent at zero cost, and then support users to gradually separate your UI from your business logic.

Do you need automatic mapping

So let’s get the question out of the way, do we really need auto-mapping?

As the application becomes larger and larger with more and more modules, direct modification of data leads to many additional uncertain factors that cannot be traced. Therefore, VUE provides VUEX to guide and regulate users’ modification state in large applications, while MOBx also provides Mobx-State-tree to constrain users’ data modification behavior. Through the unified action to make the whole modification process can be traced, debugging.

Modification cost

So in the application of large, we all want to standardize the user to modify data, then the concent to react and make advantage in his bones will show up, setState can start to enjoy the benefits of state management, without user more access to the auxiliary functions and a lot of decorators (on the definition of the field level), Perfect support for user incremental refactoring, elegant decoupling and separation of business logic from UI views, and code that always smells like React.

conclusion

Concent provides a more comfortable and intuitive coding experience around React, while adding more features to make writing react components more interesting and practical. Both the traditional class school and the emerging function school, Can enjoy a unified coding experience in Concent.

Based on concent’s three core principles:

  • Reference to collect
  • Observe key tags
  • State radio

Based on reference collection and observation of the key tag, you can do hot update path cache, in theory, a reducer if returned to update clip object shape is the same, for the first time trigger rendering and a lookup process (although has very fast), behind the words of the same reducer call can be a direct hit and update, It is similar to the hot code cache in V8, but the reducer of Concent cache returns the relationship between data shapes and references, so the application can run faster, especially the application with hundreds of components and N modules in one interface. This is the optimization that Concent is doing in the next version. Faster performance and a better writing experience are concent’s goals.

eggsAnt Design Pro powered by concent🎉 🎉 🎉

Although Concent has its own standard development approach, its flexible architecture is very easy to integrate with existing projects. This case integrates Concent into ANTD-Pro (the last VERSION of JS version 2.2.0). The business logic of the source code is not changed, only the following changes are made. Lint-staged acceptance passed:

  • Add the runConcent script to the SRC directory
  • Models were replaced with those defined in Concent format. Because UMI would automatically read model folder definitions and inject them into DVA, all Concent-related models were placed in model-CC folder
  • Component layer decorators, all usedconcentTo replace thedva, and made a few grammatical modifications
  • The introduction ofconcent-plugin-loadingPlug-in for automatic setupreducerThe start and end states of a function
  • The introduction ofreact-router-concent, for connectingreact-routerandconcent
  • The introduction ofconcent-middleware-web-devtool(First available version, more crude) for viewing statusconcentState transition process

Note that after running the project, you can open console, input SSS, view store, and input cc.dispatch or cc.reducer.** to directly trigger the call. For more API, please go to concent’s official website to view

How to run

  • Download the source code
git clone [email protected]:concentjs/antd-pro-concent.git
Copy the code
  • Go to the root directory and install the dependency packages
npm i
Copy the code
  • Run and debug projects
npm start
Copy the code

By default, the SRC directory stores the source code for the concent version. To run the DVA version, run NPM run start:old, switch to concent, and run NPM run start:cc

other

Happy coding, enjoy concent ^_^ welcome star


An out-of-box UI solution for enterprise applications as a React boilerplate.