❤ star me if you like concent ^_^

State management is a front-end industry a cliche topic, all cannot leave the state management in the development of the front frame of iteration and change, for the react, the development of the whole state management followed the react structure changes and the addition of new features and make adjustment, as a grown together with the react fast five years developers, After reflux, Redux, Mobx, and the rest of the redux derivatives dVA, Mirror, Rematch, and much more, I don’t think they are the ultimate state management modality I want, Therefore, in order to create a state management solution that combines react with the most elegant, easy to use, and efficient to run, I embarked on a dream journey.

Why do YOU need state management

There’s pretty much a consensus on why we need to introduce state management in front end references, but I can summarize it in three points:

  • As the application becomes larger and more complex, the granularity of component abstraction becomes smaller and smaller, and the hierarchy becomes deeper and deeper when combined in views, it becomes an urgent requirement to be able to easily share state across components.
  • State also needs to be segmented by modules. Behind the logic of changing state is actually our business logic. Pulling it out can completely decouple UI and business, which is conducive to logic reuse and continuous maintenance and iteration.
  • If state can be centrally managed and distributed properly, components can be updated on demand, reducing rendering scope and improving rendering performance

Status of existing status management schemes

redux

The state management scheme that follows the react immutable approach must be redux, the state management leader in the React world, no matter from git star ranking or community prosperity. It is constrained to use the reducer pure function, the only path, to modify the store data, so as to achieve clear and traceable state flow of the entire application.

mbox

Following the late show of responsive mBox, computed and Reaction, the official slogan is that anything that can be derived from the state of an application should be derived. By turning an ordinary JSON object into an observable, we can modify the state directly. React mBox will automatically drive UI rendering updates, because it is very similar to vUE in its responsiveness concept. Mobx is used in React with Mobx-React, and many people call MOBx a state management solution that turns React into a VUe-like development experience.

Of course, because mbox is very convenient to operate data, it does not meet the demand of large applications for clear traceability of state flow path. In order to restrain users’ updating behavior, a Mobx-state-tree is developed. In a word, MOBx has become a responsive representative.

other

The remaining state management schemes mainly fall into three categories.

One is not satisfied with the shortcomings of redux code redundancy, interface is not friendly enough, and then do two packages on Redux, typical representative of foreign like Rematch, domestic like DVA, mirror, etc., I call them redux derived family works, or read redux source code. Integrate your ideas and redesign a library like final-State, Retalk, Hydux, etc., which I call Redux-like works.

One is the responsive approach, which, like MobX, hijacks ordinary state objects into observables such as DOBs, which I call Mobx-like works.

The rest is to use the React Context API or the latest hook features to focus on lightweight, easy to get started and less conceptual solutions, such as Unstated-Next, Reactn, Smox and React-Model.

The ideal plan in my mind

All the schemes mentioned above can meet our needs to a certain extent, but for the aquarian programmer who pursues perfection, I think they are not my ideal schemes after all. Some of them are small and beautiful, some are large and complete, but they are not strong enough and friendly enough, so I decide to start developing state management schemes by myself.

I know that small is in conflict with beauty, completeness and strength, so I can accept a certain amount of large, and 10KB to 20KB after GZIP is the range I accept. On this basis, I can gradually realize beauty, completeness and strength, so as to achieve the following goals, so as to reflect the difference and superiority of the existing state management framework.

  • There is no need for newcomers to learn about new feature apis, and state management is invisible. They can enjoy the benefits of state management by simply organizing code according to react.
  • This will allow veterans to combine their existing knowledge of state management with the new feature apis, restore a variety of community-recognized best practices, and continue to explore and refine the benefits of state management.
  • inreactThere are thehookAfter feature, class and function components can enjoy the same thinking, the same API access state management, no sense of fragmentation.
  • By keeping the above three points in place, users can write code in a leaner and more intuitive way, while still reaping significant performance gains.

In order to achieve the above goals, Concent has been established to define it as a predictable, zero-intrusion, progressive, high-performance and enhanced state management solution, expecting to polish it into a truly beautiful, comprehensive and powerful framework for users to use.

In human terms, it is simple enough to understand, elegant enough to write code, robust enough to engineering architecture, and excellent enough to use performance…… ^_^

A predictable

React is a UI framework for change detection based on pull. Users need to explicitly call setState to make React aware of state changes, so Concent follows the classic immutable principle of React to reflect predictability. Sense state changes without hijacking objects into observables (or else become a class mobx……) , do not use the global PUB&sub mode to drive the relevant view updates, and configure various resELECT, Redux-Saga and other middleware to solve the calculation cache, asynchronous action, etc. (if so, would not be another redux family bucket wheel no return…..)

Redux’s extensive subscription granularity in the component is more and more, the state is more and more complex, often because the component subscription does not need data and cause redundant updates, and various handwriting mapXXXToYYY is very annoying ah have ah have, can not hurt ah can not hurt……

Zero invasion

As described above, we expect beginners to enjoy the benefits of state management by simply organizing their code according to react. Therefore, setState must be used as an important entry point for rendering instructions (in addition to forceUpdate).

Take a closer look at the above image and see if there are any inaccuracies in the description. Let’s take a look at the official setState signature description:

setState<K extends keyof S>(
    state: ((prevState: Readonly<S>, props: Readonly<P>) = > (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
    callback? :(a)= > void) :void;
Copy the code

From the signature description, we can see that setState is passed a partial state (fragment state). In fact, we often do this when calling setState, passing the corresponding stateKey and value to whoever changes it.

React automatically merges part of the state into the original state object to override the old value, and then drives the corresponding view update.

Therefore, as long as I can get the state submitted by setState to myself, I can also submit it to store and distribute it to other corresponding instances, which is my goal.

Obviously we need to hijack setState, inject some logic of our own, and then call native setState.

// Pseudo-code implementation
class Foo extends Component{
  constructor(props, context){
    this.state = { ... };
    this.reactSetState = this.setState.bind(this);
    this.setState = (partialState, callback){
      //commit partialState to store .....
      this.reactSetState(partialState, callback); }}}Copy the code

Of course, as a framework provider, we would not let the user do the extra injection logic in constructor, so we designed two key interfaces run and Register. Run loads the module configuration, and register registers the module to which the component belongs. The setState of the registered component is enhanced so that its submitted state can not only trigger render updates, but can also be submitted directly to the Store and distributed to other instances of the module.

Although store is a single state tree, the actual business logic consists of many modules, so I use the key of the first layer of store as the module name (similar to the namespace), thus generating the concept of module

//concent
import { run, register } from 'concent';

run({
  foo: {// Foo module definition
    state:{
      name: 'concent',
    }
  }
})

@register('foo')
class Foo extends Component {
  changeName = (a)= > {
    this.setState({ name: e.currentTarget.value });/ / modify the name
  }
  render(){
    const { name } = this.state;/ / read the name
    return <input value={name} onChange={this.changeName} />}}Copy the code

Online sample code is available here

Now look at the code above. Other than declaring state in the Foo component, which is not shown, it looks like a standard way to write the React component. Concent reduces the cost of access state management to almost nothing.

If the setState submission contains both module and non-module states, the module state will be extracted as sharedState and distributed to other instances, and the privName will be submitted only to itself.

@register('foo')
class Foo extends Component {
  state = { privName: 'i am private, not from store' };
  fooMethod = (a)= >{
    // Name is distributed to other instances as sharedState, and privName is submitted only to itself
    this.setState({name: 'newName'.privName: 'vewPrivName' });
  }
  render(){
    const { name, privName } = this.state;// Read name, privName}}Copy the code

Concent allows you to display full states. When your state contains statekeys with the same name as the simulated state, the values of those statekeys will be overwritten by the corresponding values in the module state before the first rendering.

@register('foo')
class Foo extends Component {
  // The value of name is replaced with the value of name in the module state before the first rendering
  state = { name:' '.privName: 'i am private, not from store' };
  render(){
    // name: 'concent', privName: 'i am private, not from store'
    const { name, privName } = this.state; }}Copy the code

In this mode, you can instantiate multiple Foo instances anywhere, any one of them changes the value of name, and the others are updated, and you don’t need to wrap provider-like secondary tags around the top root component to inject store context.

This is possible thanks to Concent’s core working principles, which rely on tags, reference collection, and state distribution, each of which will be covered in the following sections.

progressive

Being able to access state management as an entry point for setState, and also distinguish between shared and private state, does greatly improve our ability to manipulate module data, but is it enough and good enough?

More granular control of data consumption

The granularity of a component’s consumption of module state is not always coarsely proportional to that of a module. That is, a component belonging to module Foo may consume only the values of the f1, F2, and F3 fields in module foo. CompB, a component belonging to module Foo, may consume only the values of the other f4, F5, and f6 fields in module Foo. We would certainly not expect CompA instances to trigger CompB instance renderings when only f2 and F3 are modified.

Most of the time we expect components and modules to maintain a one-to-one relationship, with one component consuming only the data provided by one module, but in reality one component does consume data from multiple modules.

So for the Register interface, we need to pass in more information to satisfy more fine-grained data consumption needs.

  • throughmoduleMarks which specific module the component belongs to

This is optional, so if you don’t specify it, it’s built in, okay? The default module (an empty module) allows Concent to inject the module’s state into the instance’s state after its component is instantiated.

  • throughconnectFlag other modules that are connected

This is an optional option that lets the user mark connected modules with the connect parameter, setting the stateKey range to be observed in other modules.

  • throughccClassKeySets the current component class name

This is optional, and allows you to view the named Concent component node on the React DOM Tree. If not, Concent will automatically calculate a component node based on its module and connect parameters. The react DOM Tree displays the same tag name as the react DOM tree.

Marking components with the key parameters provided by the register completes an important part of Concent’s core working principle: dependency tags, so that when these components are instantiated, they carry enough information with them as data consumers to consume the data they need at a finer granularity.

Look at the relationship between classes and modules from a store perspective

The instance’s state as a data container already holds the state of its module, so how do you inject that data when connecting components to multiple modules using CONNECT? Following this question, we recall that when one instance calls setState, the state submitted by Concent will be extracted from its module state and distributed to other instances as the exact sharedState.

This is possible because when the registered components are instantiated, Concent builds an instance context CTX for them. Each instance corresponds to a unique CTX. Concent then carefully stores these CTX references in the global context ccContext (a singleton object, So component instantiation completes an important part of Concent’s core working principle: reference collection, and of course, the CTX is deleted after the instance is destroyed.

With the CTX object, Concent can naturally implement various functions on it. The module data of the above mentioned component that connects multiple modules will be injected into CTx. connectedState, and the corresponding data will be retrieved by the specific module name.

We can easily build components in code that consume data across multiple modules and control the granularity of consumption by stateKey

//concent
import { run, register, getState } from 'concent';

run({
  foo: {// Foo module definition
    state:{
      name: 'concent'.age: 19.info: { addr: 'bj'.mail: '[email protected]'}}},bar: {... },baz: {... }})// display the ccClassKey name to see which class the reference pool is from
@register({module:'foo' }, 'Foo2')
class Foo2 extends Component {... }// Belongs to module foo and is also connected to module bar and module baz
@register({
  module:'foo'.connect: ['bar'.'baz']},'Foo2')
class Foo2 extends Component {
  render(){
    // Obtain the bar and baz module data
    const { bar, baz } = this.ctx.connectedState;
    ['barKey1', 'barKey2']
    // Only when 'barKey1', 'barKey2' in the module changes will the current instance render be triggered
    const{ barKey1, barKey2 } = bar; }}Copy the code

As mentioned above, precise distribution can be achieved because the state of all instances is a proxy object, which assists Concent to implement precise updates for all the data dependencies required by these instances at runtime.

Decouple UI and business

As mentioned in the introduction of why we need state management, the logic behind the change of state is actually our business logic. Pulling it out can completely decouple UI and business, which is conducive to logic reuse and continuous maintenance and iteration.

Therefore, we use setState to depress business logic all over the place. The interweaving of business code and rendering code will inevitably cause our components to become more and more bloated, which is not conducive to logic reuse. However, in many cases, the division of functional boundaries and the establishment of data model of modules can not be clearly defined at the beginning. Is in the process of continuous iteration and abstraction gradually precipitation down.

Therefore, Concent allows such a variety of development modes. We can plan the Reducer of the store according to modules and functions from top to bottom at the beginning, and then gradually code relevant components. We can also develop and iterate from bottom to top, and do not abstract reducer when requirements or functions are not clear, but just write the business into the components. Then, they are separated one by one. Instead of forcing the centralized configuration module store, they can freely configure the decentralized configuration module store, and then easily adjust the configuration of store according to the subsequent iteration plan.

Add Reducer Definition

import { run } from 'concent';
run({
  counter: {// Define the counter module
    state: { count: 1 },//state definition, required
    reducer: {// Reducer function definition, optional
      inc(payload, moduleState) {
        return { count: moduleState.count + 1 }
      },
      dec(payload, moduleState) {
        return { count: moduleState.count - 1}}},},})Copy the code

Change the status through Dispatch

import { register } from 'concent';
// Register as a Concent Class component and specify that it belongs to the Counter module
@register('counter')
class CounterComp extends Component {
  render() {
    // CTX is a context object that Concent inject for all components, carrying various new feature apis for the React component
    return (
      <div>
        count: {this.state.count}
        <button onClick={()= > this.ctx.dispatch('inc')}>inc</button>
        <button onClick={()= > this.ctx.dispatch('dec')}>dec</button>
      </div>); }}Copy the code

Concent’s modules include state, Reducer, watch, computed, and init, so you can define them as needed.

So it is recommended to further write models as folders, whether globally consumed business Models, or component models and Page models maintained by components or pages themselves. State, Reducer, computed, watch, and init were defined internally, and then exported and synthesized together to form a complete Model definition.

The SRC ├ ─... └ ─ page │ ├ ─ login │ │ ├ ─ model / / write for folder │ │ │ ├ ─ state. The js │ │ │ ├ ─ reducer. Js │ │ │ ├ ─ computed. The js │ │ │ ├ ─ watch. Js │ │ │ ├ ─ init. Js │ │ │ └ ─ index. The js │ │ └ ─ the Login. The js │ └ ─ the product... │ └─ component ├─ model ├─ index.jsCopy the code

In this way, not only their responsibilities are clearly defined, preventing code swelling into a huge model object, but also internal functions dispatch calls can be directly based on references instead of strings after reducer independent definition.

// code in models/foo/reducer.js
export function changeName(name) {
  return { name };
}

export async function  changeNameAsync(name) {
  await api.track(name);
  return { name };
}

export async function changeNameCompose(name, moduleState, actionCtx) {
  await actionCtx.setState({ loading: true });
  await actionCtx.dispatch(changeNameAsync, name);// Call based on function reference
  return { loading: false };
}
Copy the code

A high performance

The existing state management scheme is based on reducing the rendering scope to improve performance. Only render the rendered area, which can help improve the performance of react application. Meanwhile, it also avoids writing shouldComponentUpdate function artificially.

Then compare the story, because support key levels of consumption granularity control, from the moment of state to submit know update which instances, so able to give you enough to ensure performance, especially for many components, data model of complex scenes, cocent will give you enough confidence to calmly deal with, let’s look at the contrast mbox, Concent has explored more scenarios.

RenderKey, more precise control of render range

Each component’s instance context CTX has a unique index corresponding to it, called ccUniqueKey. Each component’s renderKey defaults to ccUniqueKey if it is not overridden by the passing renderKey when instantiated. When we encountered that a stateKey of the module was a list or map, the same reducer was called for all subitems in the view it produced, and only their own data was modified through ID, but they shared a stateKey. So it is inevitable to observe that other children of the stateKey will also trigger redundant rendering, and the desired result is that only those who modify their data will trigger rendering.

For example, the list of the store is a long list, and each item will be rendered into an ItemView, and each ItemView will modify its own data by following the same reducer function. However, we expect that only ourselves can be rendered after modification, so as to achieve more accurate control of the rendering range.

With renderKey, Concent can do this easily. When you mark the renderKey at the status distribution entry, Concent will hit the renderKey instance directly to trigger the render update.

Both setState, Dispatch, and Invoke support passing renderKey.

The React key is used for diff v-dom-tree, and concent’s renderKey is used for instance location. There are some essential differences between the react key and the renderKey

// a submodule description of the store
{
  book: {
    state: {
      list: [{name: 'xx'.age: 19 },
        { name: 'xx'.age: 19}].bookId_book_: {... },//map from bookId to book
    },
    reducer: {
      changeName(payload, moduleState) {
        const { id, name } = payload;
        const bookId_book_ = moduleState.bookId_book_;
        const book = bookId_book_[id];
        book.name = name;//change name

        // Just modify the data of a book
        return { bookId_book_ };
      }
    }
  }
}

@register('book')
class ItemView extends Component {
  changeName = (e) = >{
    this.props.dispatch('changeName', e.currentTarget.value);
  }
  changeNameFast = (e) = >{
    // Each cc instance has a ccUniqueKey
    const ccUniqueKey = this.ctx.ccUniqueKey;
    When I change the name, I really just need to refresh myself
    this.props.dispatch('changeName', e.currentTarget.value, ccUniqueKey);
  }
  render() {
    const book = this.state.bookId_book_[this.props.id];
    // Although I consume bookId_book_ data for subModuleFoo, the id allows me to consume only one subitem in the list

    // Replace changeName with changeNameFast for our purpose
    return<input value={ book.name } onChange = { changeName } /> } } @register('book') class BookItemContainer extends Component { render() { const books = this.state.list; Return (< div > {/ * * traversal generates ItemView * /} {books. The map (= (v, independence idx) > < ItemView key = {} v.i d id = {} v.i d / >)} < / div >)}}Copy the code

Because Concent’s hoc for class components is wrapped in reverse inheritance by default, it will have fewer DOM hierarchies in addition to reduced render scope and rendering time.

LazyDispatch, more granular control of render times

In Concent, the Reducer function, like setState, advocates returning what is changed, and the writing format is diverse.

  • It could be a normal pure function
  • Can begeneratorGenerator function
  • Can beasync & awaitThe reducer function can return a partial state, it can call another Reducer function and return a partial state, or it can return nothing but a combination of other Reducer functions. contrastreduxorreduxFamily, always synthesis is a new state to save a lot of, and pure functions and side effects will no longer be differently defined in different places, just make an issue of function declaration is ok, you want to be pure functions, is declared as the normal function, function you want to side effects, is declared as asynchronous function, simple and clear, thinking in reading.

Based on this mechanism, the granularity of our Reducer function is very fine and atomic. Each reducer function is responsible for independently updating the values of a certain key and several keys, so that they can be combined flexibly to achieve the purpose of high reuse, make the code structure more elegant, and make the responsibilities of each reducer function smaller.

//reducer fns
export async function updateAge(id){
  / /...
  return {age: 100};
}

export async function trackUpdate(id){
  / /...
  return {trackResult: {}};
}

export async function fetchStatData(id){
  / /...
  return {statData: {}};
}

// compose other reducer fns
export async function complexUpdate(id, moduleState, actionCtx) {
  await actionCtx.dispatch(updateAge, id);
  await actionCtx.dispatch(trackUpdate, id);
  await actionCtx.dispatch(fetchStatData, id);
}
Copy the code

Although the code structure is more elegant and each Reducer function has smaller responsibilities, each reducer function actually triggers an update.

The source trigger of the Reducer function starts from the instance context ctx.dispatch or the global context cc.dispatch (or cc.reducer) and calls a reducer function of a module. Then, other Reducer functions triggered by the Reducer function inside the reducer function have actually formed a call chain, and each Reducer function that returns a state value on the link will trigger a render update. If there are many reducer functions on the reducer function, there will be many redundant updates to the same view as usual.

The source code triggered by the Reducer

// In your view <button onClick={()=> ctx.dispatch('complexUpdate', 2)}>Copy the code

The update process is as follows

The lazy feature is provided for this call chain so that the granularity of the reducer function update state can be split satisfactorily and the number of renders can be minimized.

Mbox users are thinking about transaction. Yes, you’re right. In some ways they serve the same purpose, but in Concent it’s much easier and more elegant to use.

Now you only need to make small changes to the trigger source and replace the Dispatch with lazyDispatch. There is no need to make any changes to the reducer code. Concent will delay the time when all the Reducer functions in the reducer call chain trigger UI updates. The new partial state they return is only temporarily stored after being merged by module category, and the last source function call is committed to store once and triggers the related instance rendering.

// In your view <button onClick={()=> ctx.lazyDispatch('complexUpdate', 2)}>Copy the code

See online sample code

Now the new update process is shown below

Of course, lazyScope can also be customized, and lazy does not have to be enabled on the source function.

// in your view
const a=  <button onClick={()= >Ctx.dispatch ('complexUpdateWithLoading', 2)}> Complex updates</button>

// in your reducer
export async function complexUpdateWithLoading(id, moduleState, actionCtx) {
  // Updates will be triggered in real time
  await actionCtx.setState({ loading: true });

  // Enable lazy from here, and the internal chain of calls does not trigger updates until the complexUpdate function terminates
  await actionCtx.lazyDispatch(complexUpdate, id);

  // A new partial state is returned and updates are triggered in real time
  return { loading: false };
}
Copy the code

DelayBroadcast, more actively reduce the render frequency

For some shared states, when an instance changes it frequently, delayBroadcast is used to proactively control the delayed distribution of the state to other instances, so as to proactively reduce the frequency of rendering times

function ImputComp() {
  const ctx = useConcent('foo');
  const { name } = ctx.state;
  const changeName = e= > ctx.setState({name: e.currentTarget.value});
  //setState the fourth parameter is the delay distribution time
  const changeNameDelay = e= > ctx.setState({name: e.currentTarget.value}, null.null.1000);
  return (
    <div>
      <input  value={name} onChange={changeName} />
      <input  value={name} onChange={changeName} />
    </div>
  );
}

function App(){
  return (
    <>
      <ImputComp />
      <ImputComp />
      <ImputComp />
    </>
  );
}
Copy the code

See online sample code

Enhance the react

React has a CTX object that Concent builds for every instance of react, so it’s easy to add new features to it.

New features added

For example, keywords such as computed and watch are mentioned in the module above. Readers must have some doubts about them. In fact, the motivation and experience of using them are the same as that of VUE.

  • computedDefine eachstateKeyWhen the value changes, the calculation function is triggered and its result is cached only whenstateKeyThe meter is triggered only when the value of.Learn more about computed Tomography
  • watchDefine eachstateKeyThe callback function to be triggered only when the value ofstateKeyWhen the value of is changed again, usually for asynchronous task processing.Learn more about Watch. If I fromsetStateYou can see that these features are naturally available to the user.

The parameter passed in by setState is partialState, so concent knows which stateKey has changed from the beginning. Naturally, we only need to expose a place with computed and watch, so when the instance submits a new partialState, Enhanced setState will naturally trigger related callbacks.

Setup gives components more power

The values of computed and watch mentioned above are for modules. What if we need to customize computed and watch separately for instances?

Setup is a very important feature for component instances. It can be used in both class components and function components. It is triggered once before the component is first rendered, and its return result is collected in ctx. Settings. Therefore, you can define hook functions such as instance computed, instance watch, and instance Effect, as well as customize other business logic functions and return them to facilitate component use.

The setup execution timing feature gives the component an extra space to define the relevant personalization configuration for the component at once, giving the component more capabilities, especially for function components, by providing useConcent to replicate all the capabilities of the Register interface. The ctx. Settings feature allows function components to define all methods once in setup, avoiding the vulnerability of repeatedly generating temporary closure functions during repeated rendering of function components and reducing gc pressure.

UseConcent is only used so that you can write core business logic using classic Dispatch && Reducer mode. It does not exclude mixing with other utility hook functions (useWindowSize, etc.).

Let’s setup!! Look at the magic of Setup, where the Effect hook function is a perfect substitute for useEffect. Learn more about Setup

const setup = ctx= > {
  // The side effect function when count changes. The second argument can pass multiple values, indicating that any change will trigger the side effect
  ctx.effect((a)= > {
    console.log('count changed'); },'count']);
  // Each render round is executed
  ctx.effect((a)= > {
    console.log('trigger every render');
  });
  // A side effect function executed only for the first rendering
  ctx.effect((a)= > {
    console.log('trigger only first render'); } []);// Define instance computed, since each instance can be triggered, take precedence over module computed
  ctx.computed('count', (newVal, oldVal, fnCtx)=>{
    return newVal*2;
  });

 // Define an instance of watch, which, unlike Effect, executes before the component renders
 // Since each instance may trigger, the watch module is preferred
  ctx.watch('count', (newVal, oldVal, fnCtx)=>{
    // Launch event
    ctx.emit('countChanged', newVal);
    api.track(`count changed to ${newVal}`);
  });

  // Define event listening. Concent will automatically disable the instance after it is destroyed
  ctx.on('changeCount', count=>{
    ctx.setState({count});
  });

  return {
    inc: (a)= > setCount({ count: ctx.state.count + 1 }),
    dec: (a)= > setCount({ count: ctx.state.count - 1})}; }Copy the code

Thanks to the Setup feature and the fact that all Concent instances hold the live text object CTX, class components and function components achieve 100% API call uniformity, which means they have a highly consistent coding style and zero conversion cost.

Access the function components of Setup

import { useConcent } from 'concent';

function HooklFnComp() {
  Setup is called only once before the first rendering
  const ctx = useConcent({ setup, module:'foo' });
  const { state , settings: { inc, dec }  } = ctx;

  return (
    <div>
      count: {state.count}
      <button onClick={inc}>+</button>
      <button onClick={dec}>-</button>
    </div>
  );
}
Copy the code

Access the Class components of Setup

@register('foo')
class ClassComp extends React.Component(a){
  ?setup(ctx){
    // Repeat the setup definition function you just did, but remember to return the result
    return setup(ctx);
  }

  render(){
    const ctx = this.ctx;
    //ctx.state equals this.state
    const { state , settings: { inc, dec }  } = ctx;

    return (
      <div>
        count: {state.count}
        <button onClick={inc}>+</button>
        <button onClick={dec}>-</button>
      </div>); }}Copy the code

See online sample code

After the ability is enhanced, you can freely choose the appropriate way to update the status according to the scene

@register("foo")
class HocClassComp extends Component {
  render() {
    const { greeting } = this.state; // or this.ctx.state
    const {invoke, sync, set, dispatch} = this.ctx;

    // dispatch will find reducer method to change state
    const changeByDispatch = e= > dispatch("changeGreeting", evValue(e));
    // invoke cutomized method to change state
    const changeByInvoke = e= > invoke(changeGreeting, evValue(e));
    // classical way to change state, this.setState equals this.ctx.setState
    const changeBySetState = e= > this.setState({ greeting: evValue(e) });
    // make a method to extract event value automatically
    const changeBySync = sync('greeting');
    // similar to setState by give path and value
    const changeBySet = e= > set('greeting', evValue(e));

    return( <> <h1>{greeting}</h1> <input value={greeting} onChange={changeByDispatch} /><br /> <input value={greeting} onChange={changeByInvoke} /><br /> <input value={greeting} onChange={changeBySetState} /><br /> <input value={greeting} onChange={changeBySync} /><br /> <input value={greeting} onChange={changeBySet} /> </> ); }}Copy the code

See online sample code

Here is a complete concent component lifecycle diagram:

Support middleware and plug-ins

A good framework should provide some mechanisms that can plug and plug other libraries to flexibly expand additional capabilities, which is conducive to users’ additional customization of some individual needs, and thus promote the ecological development around the framework. Therefore, the middleware and plug-in mechanisms were retained at the beginning of the design of Concent. Allows defining middleware to intercept all data change submission records for additional processing, and supports custom plug-ins to receive various signals at runtime, enhancing Concent capabilities.

Define middleware and use it

A middle is a normal function

import { run } from 'concent'; const myMiddleware = (stateInfo, next)=>{ console.log(stateInfo); next(); //next must not forget} run({... }, //store config { middlewares: [ myMiddleware ] } );Copy the code

Define the plug-in and use it

A plug-in is a plain object that must contain the install method

import { cst, run } from 'concent';

const myPlugin = {
  install: ( on ) = >{
    // Listen for signals from the Concent runtime and personalize them
    on(cst.SIG_FN_START, (data)=>{
      const { payload, sig } = data;
      //code here})}return { name: 'myPlugin' }// The plug-in name must be returned
}
Copy the code

The following plug-ins are provided based on the plug-in mechanism

  • Concent-plugin-loading, a plug-in that can easily control the loading state of Concent applications
  • Concent-plugin-redux -devtool enables Concent applications to access the redux-dev-tool debugging tool, which facilitates the clear tracing of status change history.

Embrace the existing React ecosystem

Of course, Concent will not build meaningless wheels. It still embraces various excellent resources of the react ecosystem, such as the react-Router-Concent, which Bridges the React-Router and ADAPTS it to The Concent application.

Globally expose the history object to enjoy programmatic navigation jumps.

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import { ConnectRouter, history, Link } from 'react-router-concent';
import { run, register } from 'concent';

run();

class Layout extends Component {
  render() {
    console.log('Layout Layout');
    return( <div> <div onClick={() => history.push('/user')}>go to user page</div> <div onClick={() => Push ('/user/55')}>go to userDetail page</div> {/** Link */} <Link to="/user" onClick={to => alert(to)}>to user</Link> <div onClick={() => history.push('/wow')}>fragment</div> <Route path="/user" component={User_} /> <Route path="/user/:id" component={UserDetail_} /> <Route path="/wow" component={F} /> </div> ) } } const App = () => ( <BrowserRouter> <div id="app-root-node"> <ConnectRouter /> <Route path="/" component={Layout} /> </div> </BrowserRouter> ) ReactDOM.render(<App />, document.getElementById('root'));Copy the code

Click on me to see online examples

Conclusion & reflection

At the core of Concent’s working mechanism are dependency tags, reference collection, and state distribution. By building global and instance contexts and interacting with each other, Concent achieves state management and further enhances component capabilities.

Theoretically, based on this principle, state management can be implemented for other UI frameworks that are also based on pull Based update mechanism, and they can maintain consistent API calling ability and code writing style, such as this.setData of small programs and this.update of Omi.

And since Concent provides the instance context object CTX to upgrade component capabilities, if we propose a goal: It is possible to make the coexistence of responsive and immutable appear to be feasible by attaching an observable equivalent to state to CTX, assuming that this.ctx.data is the observable we built, and then the mentioned responsive needs to be handled according to different strategies for different platforms to achieve the purpose of coexistence.

  • To itself isresponsiveSuch as the framework ofangualrandvueTo providethis.ctx.dataTo modify the state directly is equivalent to bridging the original update mechanism, whilereducerThe returned state eventually fallsthis.ctx.dataTo modify to drive view rendering.
  • forpull basedSuch as the framework ofreactTo providethis.ctx.dataIt’s just a pseudo-response form inthis.ctx.dataThe collected changes eventually fallthis.setStateTo drive view updates, but it does give users the illusion that they are driving the view by directly manipulating the data. So if you can unify this layer, rightconcentCan we write all UI frameworks in the same way?

Of course, the desire for greater unity is wonderful, but is it really necessary to achieve it? Each frame of the state management solutions are already very mature, personal limited energy to do is to realizing this vision will chose the most difficult road, so here just write a personal response to make type and immutable coexistence of thinking, give readers some reference opinions of state management thinking and the development trend between the UI framework.

If you use a poem to describe state management and UI framework, I think it is

Jinfeng Yulu a meet, they win but countless world.

The two have achieved each other, supported and developed each other, witnessing the replacement of various state libraries over the years.

For now, Concent is only considering the integration with React to improve the tacit understanding between them, hoping to gradually occupy a small base under the territory of brother Redux and brother Mobx. If you like this article and have interest in Concent, welcome to Star. I believe that the spark of revolution will continue and Concent’s ideas will go further.