DEV Community

loading...

learning composition api to improve your react development experience

fantasticsoul profile image 幻魂 ・9 min read

Open source is not easy, thank you for your support, ❤ star me if you like concent ^_^

Here is a list of status management in the collection, welcome friends who are interested to know ^_^
awesome-state

In this article, we will see the big difference between composition style and hook style in react code.

Preface

composition api (combination api) and optional api (optional api) are two ways to organize code. I believe you have learned a lot in the various related introduction articles of vue3, they can exist at the same time, It is not mandatory that you can only use which one, but the two major advantages of combined api do make developers more inclined to use it instead of optional api.

  • Package the reusable logic based on the function and inject it into any component, making the decoupling of the view and business more elegant
  • Let the businesses of the same function be placed more closely together without being separated, improving the development and maintenance experience

The above two points are elegantly solved by hook in React, so what are the advantages of combined api compared to hook? I’m not going to sell it here. I believe that some friends have already known when Youda introduced the combined api. The combined api is statically defined, which solves the performance problem that the hook must regenerate the temporary closure function every time it is rendered. In the hook, the old value trap, manual detection dependence and other coding experience problems are solved.

However, react is the coding method of all in js, so as long as we dare to think and do, all excellent programming models can be absorbed. Next, we use native hook and concentrated setup and pass Examples and explanations, to completely solve the pain point of hook mentioned by You Da^_^

react hook

We first design a traditional counter, the requirements are as follows

  • There is a decimal and a large number
  • There are two groups of plus and minus buttons, which operate on decimal and large numbers respectively, the decimal button adds and subtracts 1, and the large number button adds and subtracts 100
  • Pull the welcome greeting when the counter is first mounted
  • When the decimal reaches 100, the button turns red, otherwise it turns green
  • When the large number reaches 1000, the button turns purple, otherwise it turns green
  • When the big number reaches 10,000, the number of the big number is reported
  • When the calculator is uninstalled, report the current number

In order to complete this requirement, we need to use the following 5 hooks

useState

After the requirement, we need to use the first hook useState to initialize the state of the component's first rendering

function Counter() {
  const [num, setNum] = useState(6);
  const [bigNum, setBigNum] = useState(120);
}

useCallback

If you need to use the cache function, you need to use the second hook useCallback, here we use this hook to define the addition and subtraction functions

  const addNum = useCallback(() => setNum(num + 1), [num]);
  const addNumBig = useCallback(() => setBigNum(bigNum + 100), [bigNum]);

useMemo

If you need to use the cached calculation results, you need to use the third hook useMemo, here we use this hook to calculate the button color

 const numBtnColor = useMemo(() => {
    return num> 100?'red':'green';
  }, [num]);
  const bigNumBtnColor = useMemo(() => {
    return bigNum> 1000?'purple':'green';
  }, [bigNum]);

useEffect

To handle the side effects of the function, the fourth hook useEffect is needed. Here we are used to deal with the two requirements

  • When the big number reaches 10,000, the number of the big number is reported
  • When the calculator is uninstalled, report the current number
  useEffect(() => {
    if (bigNum> 10000) api.report('reach 10000')
  }, [bigNum])
  useEffect(() => {
    return ()=>{
      api.reportStat(num, bigNum)
    }
  }, [])

useRef

The writing of useEffect using the cleanup function above will be warned in the IDE, because of the internal use of num, bigNum variables (not writing dependencies will fall into the trap of the old value of the closure), so we are required to declare dependencies

However, if we change to the following method to avoid IDE warnings, obviously it is not our intention. We just want to report the number when the component is uninstalled, instead of triggering the cleanup function every round of rendering

  useEffect(() => {
    return ()=>{
      api.reportStat(num, bigNum)
    }
  }, [num, bigNum])

At this time we need the fifth hook useRef to help us fix our dependencies, so the correct way of writing is

  const ref = useRef();// ref is a fixed variable, and each round of rendering points to the same value
  ref.current = {num, bigNum};// Help us remember the latest value
  useEffect(() => {
    return () => {
      const {num, bigNum} = ref.current;
      reportStat(num, bigNum);
    };
  }, [ref]);

Complete counter

After using 5 hooks, our complete component is as follows

function Counter() {
  const [num, setNum] = useState(88);
  const [bigNum, setBigNum] = useState(120);
  const addNum = useCallback(() => setNum(num + 1), [num]);
  const addNumBig = useCallback(() => setBigNum(bigNum + 100), [bigNum]);
  const numBtnColor = useMemo(() => {
    return num> 100? "red": "green";
  }, [num]);
  const bigNumBtnColor = useMemo(() => {
    return bigNum> 1000? "purple": "green";
  }, [bigNum]);
  useEffect(() => {
    if (bigNum> 10000) report("reach 10000");
  }, [bigNum]);

  const ref = useRef();
  ref.current = {num, bigNum};
  useEffect(() => {
    return () => {
      const {num, bigNum} = ref.current;
      reportStat(num, bigNum);
    };
  }, [ref]);

  // render ui ...
}

Of course, we can abstract this code separately as a hook based on the customizable characteristics of hook. In this case, we only need to export the data and methods, so that the Counter components expressed by multiple uis can be reused, while also achieving ui It is isolated from the business and facilitates maintenance.

function useMyCounter(){
  // .... slightly
  return {num, bigNum. addNum, addNumBig, numBtnColor, bigNumBtnColor}
}

concent setup

The hook function must be re-executed during each round of rendering, so it is inevitable that a large number of temporary closure functions will be generated during each round of rendering. If we can omit them, it can indeed help reduce gc Some recovery pressure, now let's take a look at what the Counter will look like after using setup.

Using concent is very simple, just use the runapi to start it before the root component, so we don't have a module definition, just call it directly.

import {run} from'concent';

run();// Start first, in render
ReactDOM.render(<App />, rootEl)

Then we slightly modify the above logic, and wrap it all inside setup. The logic inside the setup function will only be executed once. The APIs provided by the rendering context ctx that need to be used include initState, computed, effect, setState, and the state state that needs to be read when calling setState are also obtained by ctx.

function setup(ctx) {// rendering context
  const {initState, computed, effect, state, setState} = ctx;
  // setup is executed only once before the component is rendered for the first time, we can write related business logic internally
}

initState

initState is used to initialize the state, instead of useState, when our component state is large, we still don't need to consider how to divide the state granularity.

initState({ num: 6, bigNum: 120 });

The function initialization state is also supported here

initState(()=>({ num: 6, bigNum: 120 }));

computed

computed is used to define the calculation function. When deconstructing from the parameter list, the input dependency of the calculation is determined. Compared with useMemo, it is more direct and elegant.

// This calculation function is triggered only when num changes
computed('numBtnColor', ({ num }) => (num> 100?'red':'green'));

Here we need to define two calculation functions. The calculation function can be configured with the description body of the calculation object, so that you only need to call computed once.

computed({
  numBtnColor: ({ num }) => num> 100?'red':'green',
  bigNumBtnColor: ({ bigNum }) => bigNum> 1000?'purple':'green',
});

effect

The usage of effect is exactly the same as useEffect, the difference is that only the key name is passed in the dependent array. At the same time, effect internally encapsulates the life cycle of function components and class components. Make any changes and migrate to class components

effect(() => {
  if (state.bigNum> 10000) api.report('reach 10000')
}, ['bigNum'])
effect(() => {
  // Here you can write what needs to be done when the first rendering is completed
  return () => {
  // Cleanup function triggered when uninstalling
    api.reportStat(state.num, state.bigNum)
  }
}, []);

setState

It is used to modify the state. After we define the method in setup based on setState, then return. Then we can get these method handles through ctx.settings in any component that uses this setup Can call

function setup(ctx) {// rendering context
  const {state, setState} = ctx;
  return {// export method
    addNum: () => setState({ num: state.num + 1 }),
    addNumBig: () => setState({ bigNum: state.bigNum + 100 }),
  }
}

Complete Setup Counter

Based on the above apis, the logic code of our final Counter is as follows

function setup(ctx) {// rendering context
  const {initState, computed, effect, state, setState} = ctx;
  // Initialization data
  initState({ num: 6, bigNum: 120 });
  // Define calculation function
  computed({
    // When the parameter list is deconstructed, the input dependency of the calculation is determined
    numBtnColor: ({ num }) => num> 100?'red':'green',
    bigNumBtnColor: ({ bigNum }) => bigNum> 1000?'purple':'green',
  });
  // define side effects
  effect(() => {
    if (state.bigNum> 10000) api.report('reach 10000')
  }, ['bigNum'])
  effect(() => {
    return () => {
      api.reportStat(state.num, state.bigNum)
    }
  }, []);

  return {// export method
    addNum: () => setState({ num: state.num + 1 }),
    addNumBig: () => setState({ bigNum: state.bigNum + 100 }),
  }
}

After defining the core business logic, we can use useConcent to assemble our defined setup inside any function component to use it. useConcent will return a rendering context (and the parameter list of the setup function refers to Is the same object reference, sometimes we also call the instance context), we can get the target data and methods from ctx on demand, for this example, we can export
You can use the three keys of state (data), settings (the method returned by the setup package), and refComputed (container of the calculation function result of the instance).

import {useConcent} from'concent';

function NewCounter() {
  const {state, settings, refComputed} = useConcent(setup);
  // const {num, bigNum} = state;
  // const {addNum, addNumBig} = settings;
  // const {numBtnColor, bigNumBtnColor} = refComputed;
}

We mentioned above that setup can also be assembled to class components, just use register. Note that the assembled class components can be directly obtained from this.ctx to the rendering context generated by concent At the same time, this.state and this.ctx.state are equivalent, and this.setState and this.ctx.setState are also equivalent, which is convenient for users to change the code 0 to access concentUse.

import {register} from'concent';

@register(setup)
class NewClsCounter extends Component{
  render(){
   const {state, settings, refComputed} = this.ctx;
  }
}

Conclusion

Compared with native hooks, setup fixes the business logic in a function that will only be executed once, provides a more friendly api, and is perfectly compatible with class components and function components, allowing users to escape the troubles of the use rules of hook ( Think about useEffect and useRef, is there a significant cognitive cost?), instead of passing these constraints on learning barriers to users, it is also more friendly to gc. I believe everyone has already defaulted to hook yesAn important invention of react, but in fact, it is not for users, but for frameworks. Users do not need to understand the details and rules of brain-burning. For concent users, they only need one hook to open one. The portal can implement all business logic in another space, and these logics can also be reused on class components.

My dear guest official has seen so much, don’t hurry up and try it out. The following links are provided in two ways for you to play with😀

one more thing

If you want to share the state of the two hook counters, we need to modify the code to connect to redux or build a self-built Context, but in the development mode of concent, setup does not require any modification, just declare one in advance Module, and then register the module that belongs to the component. This silky migration process allows users to flexibly deal with various complex scenarios.

import {run} from'concent';

run({
  counter:{
    state: {num:88, bigNum: 120 },
  },
  //reducer: {...}, // If the operation data process is complicated, the business can be promoted here
})

// For function components
useConcent({setup});
// ---> Change to
useConcent({setup, module:'counter'})

// For function components
@register({setup});
// ---> Change to
@register({setup, module:'counter'});

-shared Counter

Finish

Previous articles

❤ star me if you like concent ^_^

Edit on CodeSandbox
https://codesandbox.io/s/concent-guide-xvcej

Edit on StackBlitz
https://stackblitz.com/edit/cc-multi-ways-to-wirte-code

If you have any questions about concent, you can scan the QR code and join the group consultation or send email to me( zhongzhengkai@gmail.com ). We will try our best to answer questions and help you learn more 😀.

Discussion (0)

Forem Open with the Forem app