During my time with React development, my biggest feeling was “It’s the best of React times, it’s the worst of React times”! Hooks are “good” because they enable a different mode of development, requiring more focus on data dependencies in the way of thinking, simpler writing, and generally improved development efficiency; The “bad” part is that there are often class components and function components in a project, while class components are dominated by class programming, and the development process is more focused on the rendering cycle of the entire component, and maintenance projects often need to jump between the two mindsets, which is not the worst part.

The other day, Wang asked me, “You’ve been getting a weekly peek at the hooks article. What do you think are the hooks that cause memory leaks?” It set me thinking (because my mind went blank). We’ve been talking about hooks. What are we talking about? While there has been a lot of discussion about hooks in the community, there has been a lot of discussion about how the Popular Hooks API is used or compared to the class component lifecycle or Redux, and a lack of discussion or consensus about hooks best practices, which I think is the “worst” part. Today, let’s talk about the changes brought about by hooks, and how we can embrace them.

Note “Weekly Glance” is a column translated and shared by the team.

Since React 16.8 was released, Hooks have brought about three major changes: mindset changes, scope changes during rendering, and data flow changes.

Thinking mode

React.hooks are designed to simplify the reuse of state logic between components, allow developers to abstract associated logic into smaller functions, and reduce the cognitive cost of not having to understand the suffocating this in JS Class. Because of this motivation, hooks weaken the concept of the component lifecycle and reinforce the dependency between state and behavior, which tends to lead us to focus more on “what” than “how” [1].

Given a scenario in which the Detail of a component relies on the query parameter passed in by the parent component for data requests, we need to define an asynchronous request method getData, whether based on class components or Hooks. The difference is that in the development mode of class components, we think more in the “how” direction: request data when the component is mounted, compare the new and old Query values when the component is updated, and re-call the getData function if necessary.

class Detail extends React.Component {
  state = {
    keyword: ' ',}componentDidMount() {
    this.getData();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    if (this.props.query ! == prevProps.query) {return true;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot) {
      this.getData(); }}async getData() {
    // This is a code that requests data asynchronously
    console.log('Data requested with the following parameters:The ${this.props.query}`);
    this.setState({
      keyword: this.props.query
    })
  }

  render() {
    return (
      <div>
        <p>{this.state.keyword}</p>
      </div>); }}Copy the code

In the function component with Hooks applied, we think “what to do” : different query values, different data to display.

function Detail({ query }) {
  const [keyword, setKeyword] = useState(' ');

  useEffect(() = > {
    const getData = async() = > {console.log('Data requested with the following parameters:${query}`);
      setKeyword(query);
    }

    getData();
  }, [query]);

  return (
    <div>
      <p>{keyword}</p>
    </div>
  );
}
Copy the code

As a result, the mindset of developers in the coding process should also change, considering the synchronous relationship between data and data and data and behavior. This pattern allows related code to be grouped together more succinctly, even abstracting into custom hooks that implement shared logic and seem to smell of pluggable programming 🤔.

Although Dan Abramov writes on his blog that thinking in terms of the lifecycle and deciding when to implement side effects is counter-intuitive [2], knowing when hooks are implemented during component rendering helps us keep our understanding of React in alignment and focus on “what to do.” Donavon charts and contrasts the Hooks paradigm with the lifecycle paradigm [3] to help us understand how hooks work in components. Each time a component is updated, the component function is re-called to generate a new scope, and this change also creates new coding requirements for us developers.

scope

In a class component, once the component is instantiated, it has its own scope, which remains the same from creation to destruction. As a result, the internal variable points to the same reference every time it is rendered throughout the life of the component, and we can easily get the latest state value in each render using this.state or the same internal variable using this.xx.

class Timer extends React.Component {
  state = {
    count: 0.interval: null,}componentDidMount() {
    const interval = setInterval(() = > {
      this.setState({
        count: this.state.count + 1,}}),1000);

    this.setState({
      interval
    });
  }

  componentDidUnMount() {
    if (this.state.interval) {
      clearInterval(this.state.interval); }}render() {
    return (
      <div>The counter is: {this.state.count}</div>); }}Copy the code

In Hooks, the relationship between Render and state is more like closure and local variables. At each render, a new state variable is generated, to which React writes the state value of the next render and remains unchanged during the next render. That is, each render is independent of each other and has its own state value. Similarly, functions, timers, side effects, etc. within a component are also independent and accessed internally by the state values of the current render, so it is common to encounter situations where the latest values cannot be read from the timer/subscriber.

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() = > {
    const interval = setInterval(() = > {
      setCount(count + 1);    // Always only 1
    }, 1000);

    return () = > {
      clearInterval(interval); }} []);return (
    <div>The counter is: {count}</div>
  );
}
Copy the code

If we want to get the latest value, there are two solutions: one is to use the lambada form of setCount and pass in a function that takes the last state value as an argument; The other is to store the latest value in its current property with the useRef hook.

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() = > {
    const interval = setInterval(() = > {
      setCount(c= > c + 1);
    }, 1000);

    return () = > {
      clearInterval(interval); }} []);return (
    <div>The counter is: {count}</div>
  );
}
Copy the code

In the hook-flow diagram, we can see that when the parent component is re-rendered, all of its states (state, local variables, etc.) are new. If a child depends on an object variable of the parent, the child gets the new object regardless of whether the object changes, invalidating the diff corresponding to the child and reexecuting that part of the logic. In the example below, our side effects dependency contains object parameters passed in by the parent component, which triggers a data request each time the parent component is updated.

function Info({ style, }) {
  console.log('Info renders');

  useEffect(() = > {
    console.log('Reload data'); // Data is reloaded every time a rerender occurs
  }, [style]);

  return (
    <p style={style}>This is the text in Info</p>
  );
}

function Page() {
  console.log('Page rendered ');

  const [count, setCount] = useState(0);
  const style = { color: 'red' };

  // count +1 causes Page to be re-rendered, which in turn causes Info to be re-rendered
  return (
    <div>
      <h4>Count: {count}</h4>
      <button onClick={()= > setCount(count + 1)}> +1 </button>
      <Info style={style} />
    </div>
  );
}
Copy the code

React Hooks provide a solution. UseMemo allows us to cache incoming objects and recalculate and update them only when dependencies change.

function Page() {
  console.log('Page rendered ');

  const [color] = useState('red');
  const [count, setCount] = useState(0);
  const style = useMemo(() = > ({ color }), [color]); // Style changes only when color changes substantially

  // count +1 causes Page to be re-rendered, which in turn causes Info to be re-rendered
  // However, since style is cached, reloading of data in Info will not be triggered
  return (
    <div>
      <h4>Count: {count}</h4>
      <button onClick={()= > setCount(count + 1)}> +1 </button>
      <Info style={style} />
    </div>
  );
}
Copy the code

The data flow

React Hooks support friendlier use of context for state management, avoid loading irrelevant parameters to the middle layer when there are too many layers; The other is to allow functions to participate in the data flow and avoid passing redundant parameters to the underlying components.

UseContext is one of the core modules of hooks that retrieve the current value of the context passed to them for cross-layer communication. React: When the context value changes, all components that use the context will be rerendered. In order to avoid redrawing irrelevant components, we need to construct the context properly. For example, starting from the new mindset mentioned in section 1, we need to organize the context by the degree of relevence of the states and store the related states in the same context.

In the past, if a parent component used the same data request method, getData, and that method depended on a query value passed in from above, the query and getData methods were passed together to the child component, which used the query value to decide whether to re-execute getData.

class Parent extends React.Component {
   state = {
    query: 'keyword',}getData() {
    const url = `https://mocks.alibaba-inc.com/mock/fO87jdfKqX/demo/queryData.json?query=The ${this.state.query}`;
    // Request data...
    console.log('Request path is:${url}`);
  }

  render() {
    return (
      // Pass a query value that the child component does not render
      <Child getData={this.getData} query={this.state.query} />); }}class Child extends React.Component {
  componentDidMount() {
    this.props.getData();
  }

  componentDidUpdate(prevProps) {
    // if (prevProps.getData ! == this.props. GetData) {// This condition is always true
    // this.props.getData();
    // }
    if(prevProps.query ! = =this.props.query) { // Only the query value can be used to determine
      this.props.getData(); }}render() {
    return (
      // ...); }}Copy the code

In React Hooks useCallback allows us to cache a function and update the function if and only if the dependency changes. This enables on-demand loading with useEffect in the child components. With hooks, functions are no longer just a method, but can participate as a value in the application’s data flow.

function Parent() {
  const [count, setCount] = useState(0);
  const [query, setQuery] = useState('keyword');

  const getData = useCallback(() = > {
    const url = `https://mocks.alibaba-inc.com/mock/fO87jdfKqX/demo/queryData.json?query=${query}`;
    // Request data...
    console.log('Request path is:${url}`);
  }, [query]);  GetData is updated if and only if query changes

  // A change in the count does not cause the Child to rerequest the data
  return (
    <>
      <h4>Count: {count}</h4>
      <button onClick={()= > setCount(count + 1)}> +1 </button>
      <input onChange={(e)= > {setQuery(e.target.value)}} />
      <Child getData={getData} />
    </>
  );
}

function Child({ getData }) {
  useEffect(() = > {
    getData();
  }, [getData]);	// Functions can participate in the data flow as dependencies

  return (
    // ...
  );
}


Copy the code

conclusion

Back to the original question: “What are the points in which hooks cause memory leaks?” I understand that the risk of memory leaks is caused by the scope changes brought about by hooks. Since each rendering is independent, a side effect that references local variables and is not released when the component is destroyed can easily cause a memory leak. Sandro Dolidze’s blog has a checkList[4] for how best to use hooks, which I think is a good suggestion for writing the right hooks application.

  1. Follow the Hooks rule;
  2. Do not use any side effects in the function body; instead, place them inuseEffectIn the implementation;
  3. Unsubscribe/process/destroy all used resources;
  4. The preferreduseReduceruseStateFunction update to prevent reading and writing the same value in the hook;
  5. Don’t inrenderFunctions use mutable variables instead of usinguseRef;
  6. If theuseRefDo not release this value when processing resources, since the life cycle of the content stored in the
  7. Beware of dead loops and memory leaks;
  8. When you need to improve performance, memoize functions and objects;
  9. Setting up dependencies correctly (undefined=> each render;[a, b]= > whenabWhen change;[]=> Run this command only once.
  10. Use custom hooks in reusable use cases.

This article compares and summarizes the changes brought about by hooks in the development process and how to deal with them. Welcome to correct the misunderstanding

reference

[1] React is becoming a black box

[2] A Complete Guide to useEffect

[3] hook-flow

[4] The Iceberg of React Hooks

The article can be reproduced at will, but please keep this link to the original text.

You are welcome to join ES2049 Studio. Please send your resume to [email protected].