use Concent, release react hook's maximum energy
幻魂
Posted on December 22, 2019
Hi, my dear friend, I am fantasticsoul, today I want to talk about React's lifecycle method evolution。
If you have any question about Concent
, you can read the articles below to know more about it.
- concent online demo updated, challenge vue3 composition api & setup!
- Concent, born for build large scale and high performance react app
Star Concent if you are interested in it, I will appreciate it greatly.
how we manage our effect code before hooks born
Before hook is born, we usually put our effect code in componentDidMount
,componentDidUpdate
,componentWillUnmount
, a typical example may like this:
class SomePage extends Component{
state = { products: [] }
componentDidMount(){
api.fetchProducts()
.then(products=>this.setState({products}))
.catch(err=> alert(err.message));
}
}
If we have many filter condition to query the product, the code may like this:
class SomePage extends Component{
state = { products: [], type:'', sex:'', addr:'', keyword:'' }
componentDidMount(){
this.fetchProducts();
}
fetchProducts = ()=>{
const {type, sex, addr, keyword} = this.state;
api.fetchProducts({type, sex, addr, keyword})
.then(products=>this.setState({products}))
.catch(err=> alert(err.message));
}
changeType = (e)=> this.setState({type:e.currentTarget.value})
changeSex = (e)=> this.setState({sex:e.currentTarget.value})
changeAddr = (e)=> this.setState({addr:e.currentTarget.value})
changeKeyword = (e)=> this.setState({keyword:e.currentTarget.value})
componentDidUpdate(prevProps, prevState){
const curState = this.state;
if(
curState.type!==prevState.type ||
curState.sex!==prevState.sex ||
curState.addr!==prevState.addr ||
curState.keyword!==prevState.keyword
){
this.fetchProducts();
}
}
componentWillUnmount(){
// here clear up
}
render(){
const { type, sex, addr, keyword } = this.state;
return (
<div className="conditionArea">
<select value={type} onChange={this.changeType} >{/**some options here*/}</select>
<select value={sex} onChange={this.changeSex}>{/**some options here*/}</select>
<input value={addr} onChange={this.changeAddr} />
<input value={keyword} onChange={this.changeKeyword} />
</div>
);
}
}
And some people don't want so many change*** in code, they will write code like this:
class SomePage extends Component{
changeKey = (e)=> this.setState({[e.currentTarget.dataset.key]:e.currentTarget.value})
// ignore other logic......
render(){
const { type, sex, addr, keyword } = this.state;
return (
<div className="conditionArea">
<select data-key="type" value={type} onChange={this.changeKey} >
{/**some options here*/}
</select>
<select data-key="sex" value={sex} onChange={this.changeKey}>
{/**some options here*/}
</select>
<input data-key="addr" value={addr} onChange={this.changeKey} />
<input data-key="keyword" value={keyword} onChange={this.changeKey} />
</div>
);
}
}
And if the component will also been updated while some props changed, code may like this:
class SomePage extends Component{
static getDerivedStateFromProps (props, state) {
if (props.tag !== state.tag) return {tag: props.tag}
return null
}
}
Because
componentWillReceiveProps
is deprecated, here we usegetDerivedStateFromProps
instead ofcomponentWillReceiveProps
As a result, we have quickly completed the use of traditional life cycle method, next, we welcome hook to the stage, to see the revolutionary experience it bring us。
With hook, we can write less code to do more things.
Hook tell us forget the confused this
in class component, it give you a new way to manage your effect logic, now let rewrite our code with function component.
const FnPage = React.memo(function({ tag:propTag }) {
const [products, setProducts] = useState([]);
const [type, setType] = useState("");
const [sex, setSex] = useState("");
const [addr, setAddr] = useState("");
const [keyword, setKeyword] = useState("");
const [tag, setTag] = useState(propTag);//use propTag as tag's initial value
const fetchProducts = (type, sex, addr, keyword) =>
api
.fetchProducts({ type, sex, addr, keyword })
.then(products => setProducts(products))
.catch(err => alert(err.message));
const changeType = e => setType(e.currentTarget.value);
const changeSex = e => setSex(e.currentTarget.value);
const changeAddr = e => setAddr(e.currentTarget.value);
const changeKeyword = e => setKeyword(e.currentTarget.value);
// equal componentDidMount&componentDidUpdate
useEffect(() => {
fetchProducts(type, sex, addr, keyword);
}, [type, sex, addr, keyword]);
// any dependency value change will trigger this callback.
useEffect(()=>{
return ()=>{// clear up
// equal componentWillUnmout
}
}, []);//put an zero length array, to let this callback only execute one time after first rendered.
useEffect(()=>{
// equal getDerivedStateFromProps
if(tag !== propTag)setTag(tag);
}, [propTag, tag]);
return (
<div className="conditionArea">
<select value={type} onChange={changeType}>
{/**some options here*/}
</select>
<select value={sex} onChange={changeSex}>
{/**some options here*/}
</select>
<input value={addr} onChange={changeAddr} />
<input value={keyword} onChange={changeKeyword} />
</div>
);
});
Look at these code above, it is really cool, and the more interesting thing is:a hook can call another hook nested, that means we can put some of our code into a customized hook and reuse it anywhere!
function useMyLogic(propTag){
// we ignore these code
// you just know many useState and useEffect were copied to here
// .......
return {
type, sex, addr, keyword, tag,
changeType,changeSex,changeAddr, changeKeyword,
};
}
const FnPage = React.memo(function({ tag: propTag }) {
const {
type, sex, addr, keyword, tag,
changeType,changeSex,changeAddr, changeKeyword,
} = useMyLogic(propTag);
// return your ui
});
And if state change process have many steps, we can use useReducer
to hold these code, then your customized hook will be more clean.
Here is (Dan Abramov's online example)[https://codesandbox.io/s/xzr480k0np]
const initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { count: count + step, step };
} else if (action.type === 'step') {
return { count, step: action.step };
} else {
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => {
dispatch({
type: 'step',
step: Number(e.target.value)
});
}} />
</>
);
}
Now we see how hook will change our code organization,but is it really enough for us? next let's see how Concent
change your hook using way!
With useConcent, release react hook's maximum energy
Before we talk about useConcent
(a api supplied by Concent), we point out some shortcomings of hooks。
- hook will generate many many temporary closure method no matter you care about it or not, it puts a lot of pressure on GC, and at the same time you should use some method like
useCallback
to avoid some traps or redundant render behavior. -
useReducer
is just a pure function, your async logic code must been placed in your customized hook - the hook way is totally different with class lifecycle way, it means function component can not share logic with class component.
Then let's how Concent
resolve these 3 problems elegantly!
Let's welcome setup
feature to the stage, it will give you a new way to think and write react component.
Here we define a setup function first.
const setup = ctx => {
console.log('setup will only been executed one time before first render');
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));
};
ctx.effect(() => {
fetchProducts();
}, ["type", "sex", "addr", "keyword"]);//here only pass state key
/** equal as:
useEffect(() => {
fetchProducts(type, sex, addr, keyword);
}, [type, sex, addr, keyword]);
*/
ctx.effect(() => {
return () => {// clean up
// equal as componentWillUnmout
};
}, []);
/** Previous writing in function component:
useEffect(()=>{
return ()=>{// clean up
// do some staff
}
}, []);
*/
// attention here, effectProps is reactive to props value change,effect is reactive to state value change
ctx.effectProps(() => {
const curTag = ctx.props.tag;
if (curTag !== ctx.prevProps.tag) ctx.setState({ tag: curTag });
}, ["tag"]);//only need props key
/** Previous writing in function component:
useEffect(()=>{
if(tag !== propTag)setTag(tag);
}, [propTag, tag]);
*/
return {// these return methods will been collected to ctx.settigns
fetchProducts,
changeType: ctx.sync('type'),
};
};
By the way, the more amazing thing about setup is ctx give you another interface like on
,computed
,watch
to enhance your component capability in setup function block, here I give you two online demos.
a standard concent app
about computed&watch
Then we can use the setup function.
import { useConcent } from 'concent';
//state function definition, pass it to useConcent
const iState = () => ({ products:[], type: "", sex: "", addr: "", keyword: "", tag: "" });
const ConcentFnPage = React.memo(function({ tag: propTag }) {
// useConcent returns ctx,here deconstruct ctx directly
const { state, settings, sync } = useConcent({ setup, state: iState });
// attention here we use sync, but if you purchase high performance
// I suggest you use settings.changeType, or write input like this
// <input data-ccsync="addr" value={addr} onChange={sync} />
// because sync('**') will generate a new method in every render period
const { products, type, sex, addr, keyword, tag } = state;
const { fetchProducts } = settings;
// now we can use any method in settings
return (
<div className="conditionArea">
<h1>concent setup compnent</h1>
<select value={type} onChange={sync('type')}>
<option value="1">1</option>
<option value="2">2</option>
</select>
<select value={sex} onChange={sync('sex')}>
<option value="1">male</option>
<option value="0">female</option>
</select>
<input value={addr} onChange={sync('addr')} />
<input value={keyword} onChange={sync('keyword')} />
<button onClick={fetchProducts}>refresh</button>
{products.map((v, idx)=><div key={idx}>name:{v.name} author:{v.author}</div>)}
</div>
);
});
Setup allows you define static method, that means your component's every render period will not generate so many temporary closure function and call some many use***, let's see the effect below:
Up to now we resolve the first problem: many many temporary closure method generated in every render period. how can we resolve the second problem: separate the async logic code to a single file(we can call it logic file)
Use invoke
can easily do this, let's see how it works.
//code in logic.js
export async function complexUpdate(type, moduleState, actionCtx){
await api.updateType(type);
return { type };
}
// code in setup function
import * as lc from './logic';
const setup = ctx=>{
//other code ignored....
return {
upateType: e=> ctx.invoke(lc.complexUpdate, e.currentTarget.value);
}
}
Is this more cute to write and read? you may see the third param actionCtx
in function params list, it can allow you combine other function easily.
//code in logic.js
export async function complexUpdateType(type, moduleState, actionCtx){
await api.updateType(type);
return { type };
}
export async function complexUpdateSex(sex, moduleState, actionCtx){
await api.updateSex(sex);
return { sex };
}
export async function updateTypeAndSex({type, sex}, moduleState, actionCtx){
await actionCtx.invoke(complexUpdateType, type);
await actionCtx.invoke(complexUpdateSex, sex);
}
// code in setup function
import * as lc from './logic';
const setup = ctx=>{
//other code ignored....
return {
upateType: e=> {
// 为了配合这个演示,我们另开两个key存type,sex^_^
const {tmpType, tmpSex} = ctx.state;
ctx.invoke(lc.updateTypeAndSex, {type:tmpType, sex:tmpSex}};
}
}
}
I believe using this way to write code is more readable and maintainable,and you may ask we pass a state definition function to useConcent
, it is a private state for the function Component, how we promote it to a shared state.
Yeah, if you have this question, you asked the right person, Concent can do it very quickly and easily, with a very less code changes。
setup 1, config modules
import { useConcent, run } from "concent";
import * as lc from './logic';
run({
product:{
state: iState(),
// here we can not config lc as reducer
// but I suggest you do it, then you can call method with ctx.moduleReducer.xxx directly
reducer: lc,
}
});
setup 2, pass moudle name to useConcent
const ConcentFnModulePage = React.memo(function({ tag: propTag }) {
// attention, here we don't pass the state to it any more, just flag current component belong to product module, then Concent will inject all product module's state to ctx.state
const { state, settings, sync } = useConcent({ setup, module:'product' });
const { products, type, sex, addr, keyword, tag } = state;
const { fetchProducts } = settings;
// code ignored here, they are 100% the same....
);
});
So now we have 2 components, one is ConcentFnPage
, the other is ConcentFnModulePage
, ConcentFnPage
can still work very well, the little different in code level between these 2 components is ConcentFnPage
have own private state, ConcentFnModulePage
mark module as product
, so all the instances of ConcentFnModulePage
will share the state! let's initialize 2 instance of ConcentFnPage
and 2 instance of ConcentFnModulePage
, and see the effect below:
The problem 2 is resolved, and we only remain the last problem: how can the function component and class component share the logic?
I'm so proud to announce that setup is also can be used on class component, so the last problem is not a problem any more, let me show you the code:
class ConcentFnModuleClass extends React.Component{
render(){
const { state, settings, sync } = this.ctx;
const { products, type, sex, addr, keyword, tag } = state;
const { fetchProducts, fetchByInfoke } = settings;
// code ignored here, they are 100% the same....
}
}
export default register({ setup, module:'product' })(ConcentFnModuleClass);
let's see the effect, be attention that all the instances shared one module's state:
end
I know some of you still don't believe what happed above or want to try it self, so here is the online example link, welcome to fork and change.
https://codesandbox.io/s/nifty-cdn-6g3hh
More details see Concent git repo
or see Concent official doc
Concent is a predictable、zero-cost-use、progressive、high performance's enhanced state management solution, star it if you are interesting in the way I tell you above, thank you so much.
Posted on December 22, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.