English | 简体中文
a predictable、zero-cost-use、progressive、high performance's enhanced state management solution,work based on dependency collection&mark、ref collection and state broadcast,power you react!
visit official website https://concentjs.github.io/concent-doc to learn more.
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>
);
});
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});
}
- simple quick start project:
here you can have a quick look on concent's powerful features!!
- todo app:
written with concent, there is another one written with hook&redux, you can visit it and compare how different they are
- standard enterprise project with concent eco-lib(js):
- standard enterprise project with concent eco-lib(ts):
- support dependency collection,use
Proxy
&defineProperty
in v2.3+ to support dependency collection - simple core api,use
run
to load model configuration, useregister
to decorate class component, or useuseConcent
in function component。 - zero-cost-use,no
Provider
any more, the decorated component can be interactive with store bysetState
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 usedispatch
orinvoke
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 upate、state batch commit、high 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 callconfigure
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.。
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
Details see here concent-plugin-redux-devtool,track your state changing history。
Details see here concent-plugin-loading,control all your reducer function's loading status easily。
concent-plugin-loading online demo
Make sure you have installed nodejs。
In your computer,use create-react-app to create an app
$ npm i -g create-react-app
$ create-react-app cc-app
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
you can also review the online example's file
App1-module-state.js
content, and copy it to fileApp.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>
);
}
concent use Proxy
&defineProperty
in v2.3+
to support 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,
},
}
}
});
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']
};
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>
}
}
- 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...';
}
- concent todo mvc vs redux&hook todo mvc
- concent calculator vs traditional hook calculator
- concent query list& concent shared query list vs traditional hook query list
- redux、mobx、concent特性大比拼, 看后生如何对局前辈
- 聊一聊状态管理&Concent设计理念
- 应战Vue3 setup,Concent携手React出招了!
- 深度挖掘Concent的effect,全面提升useEffect的开发体验
- concent 骚操作之组件创建&状态更新
- 使用concent,体验一把渐进式地重构react应用之旅