Skip to content
forked from heluxjs/helux

Build-in dependency collection, a predictable、zero-cost-use、progressive、high performance's react develop framework

License

Notifications You must be signed in to change notification settings

yifan-blog/concent

 
 

Repository files navigation

English | 简体中文

a predictable、zero-cost-use、progressive、high performance's enhanced state management solution,work based on dependency collection&markref collection and state broadcast,power you react!

Docs

visit official website https://concentjs.github.io/concent-doc to learn more.


hello-concent

review this gif

🎇Enjoy composition api & dependency collection🎊 🎉

with composition api, user can easily separate ui and logic. view this demo

import { run, useConcent } from "concent";

run();// startup concent

const setup = ctx => {
  const { initState, computed, watch, setState, sync } = ctx;
  
  initState({ greeting: 'hello concent' });
  computed("reversedGreeting", n => n.greeting.split('').reverse());
  watch("greeting", (n, o) => alert(`from ${o.greeting} to ${n.greeting}`));
  
  return {
    changeGreeting: (e) => setState({ greeting: e.target.value }),
    changeGreeting2: sync('greeting'),
  };
};

function HelloConcent(){
  const { state, refComputed, settings } = useConcent({ setup });
  return (
    <>
      <h1>{state.greeting}</h1>
      <h1>{refComputed.reversedGreeting}</h1>
      <input value={state.greeting} onChange={settings.changeGreeting}/>
      <input value={state.greeting} onChange={settings.changeGreeting2}/>
    </>
  );
}

with dependency collection, exact update become true.

run({
  counter: {
    state: { count: 1 }
  }
});

const Counter = React.memo(function Counter() {
  const { state, ccUniqueKey } = useConcent("counter");
  const [showCount, setShow] = useState(true);
  const toggleShow = () => setShow(!showCount);

  return (
    <div>
      <button onClick={toggleShow}>toggle</button>
      {/*when showCount is false, current instance will have no re-render behavior any more*/}
      {/*no matter the module state count value changed or not*/}
      <span>{showCount ? state.count : "hide count"}</span>
    </div>
  );
});

simple demo 1 simple demo 2

🎆Unified coding of class components and function components

setup can be used in both class and function component, that means user can easily share logic (even including life cycle logic) between the 2 kinds of component.

// for class
@register({setup})
class ClsComp extends React.Component{...}

// for function
function FnComp(){
  useConcent({setup});
}

view demo

🖥Online experience

  • simple quick start project:
    here you can have a quick look on concent's powerful features!!

Edit

  • todo app:
    written with concent, there is another one written with hook&redux, you can visit it and compare how different they are

Edit

todo app with hook&redux (author:Gábor Soós@blacksonic)

  • standard enterprise project with concent eco-lib(js):

Edit

  • standard enterprise project with concent eco-lib(ts):

Edit

source code see here:https://github.com/fantasticsoul/concent-guid-ts

✨Fetures

  • support dependency collection,use Proxy&defineProperty in v2.3+ to support dependency collection
  • simple core api,use run to load model configuration, use register to decorate class component, or use useConcent in function component。
  • zero-cost-use,no Provider any more, the decorated component can be interactive with store by setState directly.;hello-concent
  • friendly model configuration,except state, you can also define reducer、computed、watch and init optionally to cover all your scene。
  • flexible data consumption granularity,your can consume multi model data with state key level dependency.。
  • progressive,except setState, you can also use dispatch or invoke to change state, separate your business logic and ui completely.。from class to function
  • enhance component ability,support ref level computed 、watch、emit&on、setup etc(setup is is inspired by vue3)。
  • highly consistent coding experience,no matter class component or function component, they can enjoy the same api calling。multi ways to define component
  • high performance rendering mechanism,working based on dependency mark、ref collection and state broadcast,built-in renderKey、lazyDispatch、delayBroadcast feature.。long list exact upatestate batch commithigh frequency input&delay broadcast
  • clean dom hierarchy,use reverse inheritance strategy for class component by default, to let your react dom tree keep clean。
  • middleware and plugin is supported,allow user customize middleware to intercept data changing behavior to do something else, allow user customize plugin to enhance concent ability.。
  • de-centralization model configuration,except for configuring models with run, user can also call configure api to configure you model definition near your component, that means you can publish your component to npm with your component model。
  • model clone,allow user clone new model by existed model, to meet the abstract factory need.。
  • fully typescript support,writing elegant ts code with concent is easy.。

Use with react router

Details see here react-router-concent,expose history,you can call it anywhere in your app to enjoy the imperative navigation jump.

react-router-concent online demo

Use with redux-dev-tool

Details see here concent-plugin-redux-devtool,track your state changing history。 redux-dev-tool

Use with plugin-loading

Details see here concent-plugin-loading,control all your reducer function's loading status easily。

concent-plugin-loading online demo


📦Quick start

Make sure you have installed nodejs

Create an app

In your computer,use create-react-app to create an app

$ npm i -g create-react-app
$ create-react-app cc-app

Install concent

After app created,go to the app's root directory,install concent with npm command.

$ cd cc-app
$ npm i --save concent

or yarn command

$ yarn add concent

Replace App.js file content with the code below

you can also review the online example's file App1-module-state.js content, and copy it to file App.js to see the effect.

import React, { Component } from 'react';
import { register, run, useConcent } from 'concent';

// run concent with a module named counter
run({
  counter:{
    state:{count:1}
  }
})

// define a class component that belong to 'counter' module
@register('counter')
class Counter extends Component{
  render(){
    // now setState can commit state to store and broadcast state to other refs which also belong to counter module
    const add = ()=>this.setState({count:this.state.count+1});
    return (
      <div>
        {this.state.count}
        <button onClick={add}>add</button>
      </div>
    )
  }
}

// define a function component that belong to 'counter' module
function FnCounter(){
  const ctx = useConcent('counter');
  const add = ()=>ctx.setState({count:ctx.state.count+1});
  return (
    <div>
      {ctx.state.count}
      <button onClick={add}>add</button>
    </div>
  )
}

export default function App() {
  return (
    <div className="App">
      <Counter />
      <FnCounter />
    </div>
  );
}

❤️Dependency collection

concent use Proxy&defineProperty in v2.3+ to support dependency collection

module level computed dependency collection

run({
  counter:{
    state:{
      modCount: 10,
      modCountBak: 100,
      factor: 1,
    },
    computed:{
      xxx(n){
        return n.modCount + n.modCountBak;
      },// for xxx computed retKey, the depKeys is ['modCount', 'modCountBak']
      yyy(n){
        return n.modCountBak;
      },// for yyy computed retKey, the depKeys is ['modCountBak']
      zzz(n, o, f){// n means newState, o means oldState, f means fnCtx
        return f.cuVal.xxx + n.factor;
      },// for zzz computed retKey, the depKeys is ['factor', 'modCount', 'modCountBak']
    },
    watch:{
      xxx:{
        fn(n){
          console.log('---> trigger watch xxx', n.modCount);
        },// for xxx watch retKey, the depKeys is ['modCount']
        immediate: true,
      },
    }
  }
});

ref level computed dependency collection

const setup = ctx => {
  ctx.computed('show', (n)=>{
    return n.show + n.cool + n.good;
  });// for show retKey, the depKeys is ['show', 'cool', 'good']

  ctx.computed('show2', (n)=>{
    return n.show + '2222' + n.cool;
  });// for show2 retKey, the depKeys is ['show', 'cool', 'good']
};

state dependency collection

import {register, useConcent} from 'concent';

const iState = ()=>({show:true});
function FnComp(){
  const {state, syncBool} = useConcent({module:'counter', state:iState, setup});
  return (
    <div>
      {/** if show is true, current ins's dependency is ['modCount']*/}
      {state.show? <span>{state.modCount}</span> : ''}
      <button onClick={syncBool('show')}>toggle</button>
    </div>
  );
}

@register({module:'counter', state:iState, setup})
class ClassComp extends React.Component{
  // state = iState(); //or write private state here
  render(){
     const {state, syncBool} = this.ctx;
    return (
      <div>
        {/** if show is true, current ins's dependency is ['modCount']*/}
        {state.show? <span>{state.modCount}</span> : ''}
        <button onClick={syncBool('show')}>toggle</button>
      </div>
  }
}

edit this demo on CodeSandbox

🔨Examples with some advanced features

  • run concent,load model configuration
import React, { Component, Fragment } from 'react';
import { register, run } from 'concent';

run({
  counter: {// define counter module
    state: {// 【necessary】,define state
      count: 0,
      products: [],
      type: '',
    },
    reducer: {// 【optional】define reducer,write logic code to change the state
      inc(payload=1, moduleState) {
        return { count: moduleState.count + payload };
      },
      dec(payload=1, moduleState) {
        return { count: moduleState.count - payload };
      },
      async inc2ThenDec3(payload, moduleState, actionCtx){
        await actionCtx.dispatch('inc', 2);
        await actionCtx.dispatch('dec', 3);
      }
    },
    computed:{// 【optional】define computed,the function will be triggered when stateKey changed,and the return result will be cached.
      count(newState, oldState){
        return newState.count * 2;
      }
    },
    watch:{// 【optional】define watch,the function will be triggered when stateKey changed,usually for some async tasks
      count(newState, oldState){
        console.log(`count changed from ${oldState.count} to ${newState.count}`);
      }
    },
    init: async ()=>{//【optional】async state init process, attention this process has nothing to do with whether the component is mounted or not, but the result can effect all the components belong to this module.
      const state = await api.fetchState();
      return state;
    }
  }
})

recommend user put every part of model configure to separate files,because they have clear responsibilities.

|____models             # business models
| |____index.js
| |____counter
| | |____index.js
| | |____reducer.js     # change state methods(optional)
| | |____computed.js    # computed methods(optional)
| | |____watch.js       # watch methods(optional)
| | |____init.js        # async state initialization function(optional)
| | |____state.js       # module init state(required)

now reducer functions can call each other with function ref directly(not only string)

export function inc(payload=1, moduleState) {
  return { count: moduleState.count + payload };
}

export function dec(payload=1, moduleState) {
  return { count: moduleState.count - payload };
}

// combine other reducer functions to complete a logic
export async function inc2ThenDec3(payload, moduleState, actionCtx){
  await actionCtx.dispatch(inc, 2);
  await actionCtx.dispatch(dec, 3);
}

you can also call setState in reducer function block, it is a promisified api.

export updateLoading(loading){
  return { loading }
}

export async function inc2ThenDec3(payload, moduleState, actionCtx){
  await actionCtx.dispatch(inc, 2);
  //equivalent actionCtx.dispatch(updateLoading, true);
  await actionCtx.setState({loading: true});
  await actionCtx.dispatch(dec, 3);
  //equivalent actionCtx.dispatch(updateLoading, false);
  await actionCtx.setState({loading: false});
  
  //if you return a new partial state here, it will trigger view updated also, but this is optional.
  return { tip: 'you can return some new value in current reducer fn ot not' };
}
  • define setup
    setup will only been executed before first render, usually for defining some effects or return methods that user can get them from ctx.settings later, so there is no temporary closure method any more in your render function block, and setup can pass to class and function both, that means you can switch your component definition way between class and function as you like,reuse business logic elegantly。
const setup = ctx => {
  console.log('setup only execute one time before first render period');
  
  ctx.on('someEvent', (p1, p2)=> console.log('receive ', p1, p2));
  
  ctx.effect(() => {
    fetchProducts();
  }, ["type", "sex", "addr", "keyword"]);//only pass state key
  /** equivalent code below in function component
    useEffect(() => {
      fetchProducts(type, sex, addr, keyword);
    }, [type, sex, addr, keyword]);
  */

  ctx.effect(() => {
    return () => {
      // clear up
      // equivalent componentWillUnmout
    };
  }, []);// pass zero length array, to let effect only execute one time after first render period
  /** equivalent code below in function component
    useEffect(()=>{
      return ()=>{
        // clear up
      }
    }, []);
  */

  ctx.effectProps(() => {
    // write effect handler for props value change,it is different with ctx.effect which works for state value changing
    const curTag = ctx.props.tag;
    if (curTag !== ctx.prevProps.tag) ctx.setState({ tag: curTag });
  }, ["tag"]);//only pass props key
  /**  equivalent code below in function component
    useEffect(()=>{
      if(tag !== propTag)setTag(tag);
    }, [propTag, tag]);
  */


  // define ref computed, the function will been triggered when count changed, user can get the function result from ctx.refComputed.doubleTen later in render block
  ctx.computed('doubleTen', (newState, oldState)=>{
    return newState.count * 10;
  }, ['count']);
  // but mostly you should think about module computed first if you want to share the computed logic between all refs and only want the computed function only been triggered one time when state state changed, cause every ref will trigger its own computed function;


  // if retKey is equal to stateKey, you can write like below
  ctx.computed('count', ({count})=>count*2);

  // define ref watch, and just like reason of module computed, you should think about module watch first
  ctx.watch('retKey', ()=>{}, ['count']);

  const fetchProducts = () => {
    const { type, sex, addr, keyword } = ctx.state;
    api.fetchProducts({ type, sex, addr, keyword })
      .then(products => ctx.setState({ products }))
      .catch(err => alert(err.message));
  };

  const inc = () => {
    ctx.setState({ count: this.state.count + 1 });
  }
  const dec = () => {
    ctx.setState({ count: this.state.count - 1 });
  }
  //dispatch can commit state to store, and broadcast state to other refs(which belongs to module counter) also
  const incD = () => {
    ctx.dispatch('inc');// or better way: this.ctx.moduleReducer.inc()
  }
  const decD = () => {
    ctx.dispatch('dec');// or better way: this.ctx.moduleReducer.dec()
  }

  // return result will been collected to ctx.settings
  return {
    inc,
    dec,
    incD,
    decD,
    fetchProducts,
    //sync type value, sync method can extract value from event automatically
    changeType: ctx.sync('type'),
  };
};
  • register as a concent component base on class、renderProps, hook
// base on class
@register({module:'counter', setup})
class Counter extends Component {
  constructor(props, context){
    super(props, context);
    this.state = {tag: props.tag};// private state
  }
  render() {
    // now the state is a combination of private state and module state
    const { count, products, tag } = this.state;
    // this.state can replace with this.ctx.state 
    //const { count, products, tag } = this.ctx.state;

    const {inc, dec, indD, decD, fetchProducts, changeType} = this.ctx.settings;    

    return 'your ui xml...';
  }
}

// base on renderProps
const PropsCounter = registerDumb({module:'counter', setup})(ctx=>{
  const { count, products, tag } = ctx.state;
  const {inc, dec, indD, decD, fetchProducts, changeType} = ctx.settings;    
  return 'your ui xml...';
});

// base on hook
function HookCounter(){
  const ctx = useConcent({module:'counter', setup});
  const { count, products, tag } = ctx.state;
  const {inc, dec, indD, decD, fetchProducts, changeType} = ctx.settings;    

  return 'your ui xml...';
}

⚖️Some online comparative examples

💻Some online examples

⌨️Some git repo

📰Some articles


Pic introduction

cc state broadcast process

cc component working process

About

Build-in dependency collection, a predictable、zero-cost-use、progressive、high performance's react develop framework

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 100.0%