โค star me if you like concent ^_^

Managing side effect code

Before hooks were born, we used to write the side effects logic in the built-in life cycle functions componentDidMount, componentDidUpdate, and componentWillUnmount.

There will no longer discuss componentWillUpdate, componentWillReceiveProps, because as the react support asynchronous rendering, these functions have been marked as unsafe, let’s follow the trend of history, thoroughly forget them ๐Ÿ˜€

Here is a typical application scenario:

class SomePage extends Component{
    state = { products: [] }
    componentDidMount(){
        api.fetchProducts()
        .then(products= >this.setState({products}))
        .catch(err= >alert(err.message)); }}Copy the code

Code like this is 100% sure to have been written before, and the meaning is simple: when the component is first mounted, fetch the product list data.

Our page is usually like this, the head is a condition input or select area, the large area is a form, we now give me some requirements on this page, select any value changes in the area, are triggered automatically query update list, component destroyed while they do other things, dear readers must have written a similar to the following code:

class SomePage extends Component{
    state = { products: [].type:' '.sex:' '.addr:' '.keyword:' ' }
    
    componentDidMount(){
        this.fetchProducts();
    }
    
    fetchProducts = (a)= >{
        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(){
        // There is some cleaning up going on
    }
    
    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> ); }}Copy the code

Mark data-*** in the render node to reduce the code, most likely as follows:

class SomePage extends Component{
    changeKey = (e) = > this.setState({[e.currentTarget.dataset.key]:e.currentTarget.value})
    // Other slightly...
    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> ); }}Copy the code

If some condition also need to accept this component from the value of the props to update, then use the new function in the class getDerivedStateFromProps componentWillReceiveProps replaced is not recommended, code written roughly as follows:

class SomePage extends Component{
    static getDerivedStateFromProps (props, state) {
        if(props.tag ! == state.tag)return {tag: props.tag}
        return null}}Copy the code

At this point, we finish the class component’s discussion of side effect code management, and then we let hook powder come on stage โ”(‘ โˆ€ยด) Blue!

Dad Hook teaches how to be human

When Hook was born, we used the similar examples above to rewrite the above examples into more simple and easy to understand examples, and taught the class component to be a new man ๐Ÿ˜€ in minutes

Let’s take a look at the rewritten code

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 the tag from props as the initialization 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);

  ComponentDidMount // componentDidUpdate = componentDidMount // componentDidUpdate = componentDidMount
  useEffect((a)= > {
    fetchProducts(type, sex, addr, keyword);
  }, [type, sex, addr, keyword]);
  // It fills four dependencies, triggering this side effect on the first rendering
  // After the component is in existence, any change will trigger this side effect
  
  useEffect((a)= >{
      return (a)= >{// Returns a cleanup function
          // equivalent to componentWillUnmout, where you clean things up}} []);// The second argument is passed to an empty array, and the second side effect is executed only once after the initial rendering
  
  useEffect((a)= >{
     // For the first rendering, this side effect will still be performed, with a clever internal comparison to avoid an unnecessary UI update
     // equivalent to the logic in getDerivedStateFromProps in the component class above
     if(tag ! == propTag)setTag(tag); }, [propTag, tag]);return( <div className="conditionArea"> <select value={type} onChange={changeType}> {/**some options here*/} </select> <select  data-key="sex" value={sex} onChange={changeSex}> {/**some options here*/} </select> <input data-key="addr" value={addr}  onChange={changeAddr} /> <input data-key="tkeywordype" value={keyword} onChange={changeKeyword} /> </div> ); });Copy the code

It looks so fresh, doesn’t it? Does it look sexy? Cleverly use useEffect to replace each life cycle function in the class component, and there is no confusion in the context of this, true functional programming!

Even better, hooks are freely grouped and nested, so your fat-looking FnPage logic can be slimmed down in seconds

function useMyLogic(propTag){
    // You can copy all the logic here and return the states and methods
    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
});
Copy the code

This useMyLogic function can be reused anywhere else! If status updates are complicated, useReducer is also available to separate the business logic from the hook functions, as Dan Abramov shows: click here for an online example

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((a)= > {
    const id = setInterval((a)= > {
      dispatch({ type: 'tick' });
    }, 1000);
    return (a)= > clearInterval(id);
  }, [dispatch]);

  return( <> <h1>{count}</h1> <input value={step} onChange={e => { dispatch({ type: 'step', step: Number(e.target.value) }); }} / > < / a >); }Copy the code

๐Ÿ˜€ at this point, do you feel no love for class? But is this really perfect for organizing business code using hooks? Are there no weaknesses?

Use Concent’s effect to upgrade the useEffect experience

It is true that useMyLogic can be reused everywhere, and useReducer does re-decouple and decouple state from hook functions, but they have the following problems:

  • Question 1, essentially, hook is to encourage developers to use closures, because every rendering created hook component function corresponding to the scope of the moment is generated within the scope of various states or method will only work in the frame, but we don’t escape is every frame render the very real possibility of the creation of a large number of temporary closure function, It does put some extra pressure on JS to recycle immediately, which we can avoidCreate temporary closure functions repeatedlyWhat about these questions? The answer is yes, for more on whyChanges brought about by SetupHere is the main discussionuseEffectAnd ConcenteffectMake a contrast, forsetupI won’t repeat it again.
  • Question 2,useReducerIt is only to solve the problem of decoupling the update state logic and hook function, but it is only a pure function, asynchronous logic is not written in it, your asynchronous logic will eventually land inside the custom hook function, anduseReducerJust a local state management, we can be happy to achieveStatus updates can be asynchronous, synchronous, freely combined, and easily promoted for global state management purposesWell, the answer is yes, ConcentinvokeThe interface will tell you the final answer!
  • Question 3,useEffectIt does solve the problem of side effect code management, but when we change a class component into a function component, it requires code adjustment and logic transformation, can weUnify side effect code management and allow class and function components to shareAgain, the answer is absolutely yes, based on ConcenteffectInterface, you can achieve uniform side effect management without changing one line of code, which means your component can switch between classes and functions at will!

Let’s summarize the three problems we are going to solve:

  • Avoid creating temporary closure functions repeatedly.
  • 2 Status updates can be asynchronous, synchronous, freely combined, and can be easily promoted for global status management purposes.
  • 3 unified side effect code management, so that classes and functions to achieve zero cost painless sharing.

Let’s start the show

Modify the FnPage function component

Construct the setup function

const setup = ctx= > {
  console.log('The setup function is executed only once before the component is first rendered');
  const fetchProducts = (a)= > {
    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((a)= >{ fetchProducts(); },"type"."sex"."addr"."keyword"]);// Just pass the key name
  UseEffect (() => {fetchProducts(type, sex, addr, keyword); }, [type, sex, addr, keyword]); * /

  ctx.effect((a)= > {
    return (a)= > {
      // Returns a cleanup function
      // equivalent to componentWillUnmout, where you clean things up}; } []);/ * * function components in writing: useEffect (() = > {return () = > {/ / / / returns a cleaning function is equivalent to componentWillUnmout, make clear things here}}, []); // The second argument is passed to an empty array. The second side effect is executed only once after the initial rendering

  ctx.effectProps((a)= > {
    // Write side effects for changes on props. Note that this is different from ctx.effect, which is a side effect for state
    const curTag = ctx.props.tag;
    if(curTag ! == ctx.prevProps.tag) ctx.setState({tag: curTag }); },"tag"]);// Just pass the key name
  /** function (); UseEffect (()=>{// This is done the first time you render it, so you can do a nice internal comparison to avoid an unnecessary UI update. // This is equivalent to if(tag! == propTag)setTag(tag); }, [propTag, tag]); * /

  return {// The return result is collected in ctx. Settings
    fetchProducts,
    // This is recommended, with the method defined in Settings. The following example deliberately uses the sync syntactic sugar function directly
    changeType: ctx.sync('type'),}; };Copy the code

Now that the setup logic is constructed, let’s see what the function component looks like

import { useConcent } from 'concent';

// Define a state constructor that is passed to useConcent
const iState = (a)= > ({ products: [].type: "".sex: "".addr: "".keyword: "".tag: "" });

const ConcentFnPage = React.memo(function(props) {
  // useConcent returns CTX, which is destructed directly to the desired object or method
  const { state, settings, sync } = useConcent({ setup, state: iState, props });
  const { products, type, sex, addr, keyword, tag } = state;
  const { fetchProducts } = settings;

  // The following UI uses the sync syntax sugar function to synchronize the state, if for maximum performance
  // You can define them in the setup result so that you don't have to generate temporary update functions every time you render
  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 data-key="sex" value={sex} onChange={sync('sex')}>
        <option value="1">male</option>
        <option value="0">female</option>
      </select>
      <input data-key="addr" value={addr} onChange={sync('addr')} />
      <input data-key="keyword" 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>
  );
});
Copy the code

The great thing about Setup is that it only executes once before a component is rendered for the first time. The results returned are collected in Settings, which means that your API is statically declared and not created each time you render! In this space you can also define other functions, such as ctx.on to define event listeners, ctx.computed to define calculation functions, ctx.watch to define observation functions, etc. Here we focus on ctx.effect. Codesandbox. IO/s/concent – g… Stackblitz.com/edit/concen…

Now let’s see what happens

Avoid creating temporary closure functions repeatedly

So far, we’ve solved the first problem of avoiding creating temporary closure functions repeatedly

If our state update logic is accompanied by a lot of complicated operations, our setup body will inevitably become bloated. We can of course wrap these functions and abstract them out, return the result and call ctx.state to update them. But Concent provides a more elegant interface, invoke, that lets you do this. We wrap these logic into functions and place them in a file called logic.js, then return the new fragment state and invoke them

//code in logic.js

export function simpleUpdateType(type, moduleState, actionCtx){
    return { type };
}
Copy the code

In your setup body you can create a property that will be collected in Settings to call this function.

import * as lc from './logic';

const setup = ctx= >{
    / / other slightly
    return {
        upateType: e= >ctx.invoke(lc.simpleUpdateType, e.currentTarget.value); }}Copy the code

This might not seem like much, but isn’t it just a call, let’s do it asynchronously

//code in logic.js
export async function complexUpdate(type, moduleState, actionCtx){
    await api.updateType(type);
    return { type };
}

// code in setup
import * as lc from './logic';

const setup = ctx= >{
    / / other slightly
    return {
        upateType: e= >ctx.invoke(lc.complexUpdate, e.currentTarget.value); }}Copy the code

If the caller does not belong to any module, the second argument is an empty object {}. If the caller does not belong to any module, the second argument is an empty object {}. We will analyze it later when it has a value. Here we will focus on the third parameter actionCtx. Isn’t it convenient to use it to concatenate other functions?

//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
import * as lc from './logic';

const setup = ctx= >{
    / / other slightly
    return {
        upateType: e= > {
            // for this demo, let's open two other keys to save type, sex^_^
            const {tmpType, tmpSex} = ctx.state;
            ctx.invoke(lc.updateTypeAndSex, {type:tmpType, sex:tmpSex}}; }}}Copy the code

What if I want to share this state with other components? We just need to configure the state in the run function first. When we use useConcent, we just need to mark the module name

Configure the module first

import { useConcent, run } from "concent";
import * as lc from './logic'; Run ({product: {/ reuse/here just now, the status of the generating function state: IState (), / / just logic function module as reducer configuration / / here, of course, there can be no configuration, but recommended deserve to go up, convenient call place do not need to introduce logic. Js reducer: lc,}});Copy the code

Next, add the module tag to the component and, in contrast to ConcentFnPage, simply change the state property to module and set it to Product

const ConcentFnModulePage = React.memo(function({ tag: PropTag}) {// useConcent returns CTX const {state, Settings, sync} = useConcent({setup, module:'product' });
  const { products, type, sex, addr, keyword, tag } = state; const { fetchProducts } = settings; ConcentFnPage (ConcentFnPage); });Copy the code

Note that the original ConcentFnPage will still work, without changing a single line of code. The new ConcentFnModulePage will simply pass in the module value and remove state when using useConcent. CTX. ConcentFnPage is a module-free component. Instances of ConcentFnPage are isolated from each other. For example, instance 1 changes its state without affecting instance 2. But ConcentFnModulePage is a component that registers the Product module, which means that any instance of it that changes its state will be synchronized to the other instances, making it so easy to upgrade the state to shared! Only one module token is marked.

Let’s see how it works! Note that the state of concent shared COMP2 instances is synchronized.

At this point, we have solved the second problem: status updates can be asynchronous, synchronous, freely combined, and can be easily promoted to global status management, and the promotion process is so smooth and comfortable.

Unified side effect code management

That leaves one final goal: to unify side effect code management so that classes and functions can share it painlessly at zero cost.

This is especially easy for Concent. All in all, The effect that Concent provides in setup automatically ADAPTS intelligently based on the type of component that is registered. For class components, it ADAPTS its various life cycle functions called componentDidMount. ComponentDidMount, componentWillUnmount, useEffect for function components, so the switching cost is the same as 0 cost!

CTX gets the parameters from this and passes them to the Register interface. Note that setup is also reused directly.

class ConcentFnModuleClass extends React.Component{
  render(){
    const { state, settings, sync } = this.ctx;
    const { products, type, sex, addr, keyword, tag } = state;
    const { fetchProducts, fetchByInfoke } = settings;
  
    // The same code is used here}}export default register({ setup, module:'product' })(ConcentFnModuleClass);
Copy the code

Come and see the effect!

Shared comp is a function component, and shared class comp is a class component.

conclusion

This article ends here. I know dear you must be confused or want to give it a try. An online sample of the above code snippet is here

IO /s/concent-g… codesandbox.io/s/concent-g… Ts: codesandbox. IO/s/concent – g…

Middle-aged people, life is not easy, baldness is almost impossible to stop, the code is hard, like the official to come to โœจ star โค star me if you like concent ^_^

We know that hook has improved the react development experience, but For Concent, it does much more than you think. Code splitting and combination, logic separation and reuse, and state definition and sharing can all make your development experience happier again. Because Concent’s slogan is a predictable, zero-intrusion, progressive, high-performance, enhanced state management solution.

As far as I am concerned, although hua fa has begun to fall, but if one can drop the price of more development can retain that head of black and bright and dense hair, instantly feel worth, ha ha ๐Ÿ˜€