preface

Recently refactoring the company system using React hooks, using the useEffect feature, when there are multiple dependencies, if multiple dependencies are modified at the same time, useEffect will be called multiple times, which may cause unexpected calls.

An 🌰

We have the following interface to implement the following requirements

  • When changing the status, pull the latest data.
  • When the type is changed, the status is reset to All, and the latest data is pulled.

Let’s use the code to simple implementation:

import { useMemo } from 'react';
import { useEffect, useState } from 'react';

export default function Records() {
  const types = useMemo(() = > {
    return ['Personal Records'.'Family Records']; } []);const statusList = useMemo(() = > {
    return ['Full appointment'.'To be paid'.'Accepted']; } []);const [type, setType] = useState('Personal Records');
  const [status, setStatus] = useState('Full appointment');

  const getData = () = > {
    console.log('Get the latest list, type:The ${type}, state:${status}.The ${Date.now()}`);
  };

  // The type changes to reset the state
  useEffect(() = > {
    setStatus('Full appointment'); },type]);

  // Pull the latest data when the state or type changes
  useEffect(() = >{ getData(); },type, status]);

  return (
    <div>
      <div>
        {types.map((val) => {
          return (
            <button style={{ color: val= =type ? 'red' : 'black'}}key={val} onClick={()= > setType(val)}>
              {val}
            </button>
          );
        })}
      </div>
      <div>
        {statusList.map((val) => {
          return (
            <button style={{ color: val= =status ? 'red' : 'black'}}key={val} onClick={()= > setStatus(val)}>
              {val}
            </button>
          );
        })}
      </div>
    </div>
  );
}
Copy the code

As a result, both requirements are fulfilled, but there is a problem with obtaining the latest data twice when the state and record type change simultaneously.

To solve this problem, we have several ways:

Method 1 (personally not recommended)

Instead of using useEffect to get the latest data, put the getData call mode on the button click event.

Cons: Developers need to focus on more points, not conducive to late maintenance. The getData function needs to add two more parameters. The code complexity increases as follows:

import { useMemo } from 'react';
import { useEffect, useState } from 'react';

export default function Records() {
  const types = useMemo(() = > {
    return ['Personal Records'.'Family Records']; } []);const statusList = useMemo(() = > {
    return ['Full appointment'.'To be paid'.'Accepted']; } []);const [type, setType] = useState(' ');
  const [status, setStatus] = useState(' ');

  const getData = (type: string, status: string) = > {
    setType(type);
    setStatus(status);
    console.log('Get the latest list, type:The ${type}, state:${status}.The ${Date.now()}`);
  };

  // Initialize for the first time to get the data
  useEffect(() = > {
    getData('Personal Records'.'Full appointment'); } []);return (
    <div>
      <div>
        {types.map((val) => {
          return (
            <button style={{ color: val= =type ? 'red' : 'black'}}key={val} onClick={()= >GetData (val, 'all reservations ')}> {val}</button>
          );
        })}
      </div>
      <div>
        {statusList.map((val) => {
          return (
            <button style={{ color: val= =status ? 'red' : 'black'}}key={val} onClick={()= > getData(type, val)}>
              {val}
            </button>
          );
        })}
      </div>
    </div>
  );
}
Copy the code

Method 2 (personally not recommended)

Merge type and status into one state so that the dependency becomes one and the useEffect side effect does not start twice.

Disadvantages: In fact, the relationship between the two states is not very strong, readability is not very good, call and update one state to pay attention to the value of the other state, if the number of dependencies continues to increase, the complexity increases. The code is as follows:

import { useMemo } from 'react';
import { useEffect, useState } from 'react';

export default function Records() {
  const types = useMemo(() = > {
    return ['Personal Records'.'Family Records']; } []);const statusList = useMemo(() = > {
    return ['Full appointment'.'To be paid'.'Accepted']; } []);const [state, setState] = useState({
    type: 'Personal Records'.status: 'Full appointment'});const getData = () = > {
    console.log('Get the latest list, type:${state.type}, state:${state.status}.The ${Date.now()}`);
  };

  // Pull the latest data when the state or type changes
  useEffect(() = > {
    getData();
  }, [state]);

  return (
    <div>
      <div>
        {types.map((val) => {
          return (
            <button style={{ color: val= =state.type ? 'red' : 'black'}}key={val} onClick={()= >SetState ({status:" all reserved ", type: val})}> {val}</button>
          );
        })}
      </div>
      <div>
        {statusList.map((val) => {
          return (
            <button style={{ color: val= =state.status ? 'red' : 'black'}}key={val} onClick={()= >setState({... state, status: val})}> {val}</button>
          );
        })}
      </div>
    </div>
  );
}
Copy the code

Method 3 (Personal recommendation)

What we really want is for the callback function to execute only the latest one when multiple dependencies change at the same time. This is very similar to the idea of stabilization, so we can use the idea of stabilization to getData first. Because react hooks+ closures have a value capture feature, useRef is required to record the latest callbacks in real time, but I won’t go into details. Let’s start by manually implementing an antishake Hooks, using the lodash-es antishake implementation

import { useRef } from 'react';
import { debounce } from 'lodash-es';

export function useDebounce<T extends Function> (fn: T, wait = 1000) {
  const func = useRef(fn);
  func.current = fn
  const debounceWrapper = useRef(debounce((args) = >func.current? .(args), wait));return (debounceWrapper.current as unknown) as T;
}
Copy the code

Full code:

import { useDebounce } from '@/hooks/useInit';
import { useMemo } from 'react';
import { useEffect, useState } from 'react';

export default function Records() {
  const types = useMemo(() = > {
    return ['Personal Records'.'Family Records']; } []);const statusList = useMemo(() = > {
    return ['Full appointment'.'To be paid'.'Accepted']; } []);const [type, setType] = useState('Personal Records');
  const [status, setStatus] = useState('Full appointment');

  const getData = useDebounce(() = > {
    console.log('Get the latest list, type:The ${type}, state:${status}.The ${Date.now()}`);
  }, 0);

  useEffect(() = > {
    setStatus('Full appointment'); },type]);

  useEffect(() = >{ getData(); },type, status]);

  return (
    <div>
      <div>
        {types.map((val) => {
          return (
            <button style={{ color: val= =type ? 'red' : 'black'}}key={val} onClick={()= > setType(val)}>
              {val}
            </button>
          );
        })}
      </div>
      <div>
        {statusList.map((val) => {
          return (
            <button style={{ color: val= =status ? 'red' : 'black'}}key={val} onClick={()= > setStatus(val)}>
              {val}
            </button>
          );
        })}
      </div>
    </div>
  );
}
Copy the code

Execution result:Although this achieved our requirements, but always feel not very elegant, need to be specific to a function, and modify the implementation of the original function, if inuseEffectOther operations, more cumbersome, semantically is not intuitive, other developers may not understand why to use tremble. Such as:

  useEffect(() = > {
    getData();
    // Other operations 1
    // Other operations 2},type, status]);
Copy the code

To solve the above problem, we can further encapsulate and implement a useBatchEffect. UseBatchEffect:

/** * Batch set update */
export function useBatchEffect(effect: EffectCallback, deps? : DependencyList, wait =0) {
  const fn = useDebounce(effect, wait);
  useEffect(fn, deps);
}

Copy the code

In this case, we use the antiskid hooks implemented above to antiskid all operations done in useEffect, thereby implementing useEffect that has multiple operations that depend on a single batch operation that changes simultaneously. Full code:

import { useBatchEffect } from '@/hooks/useInit';
import { useMemo } from 'react';
import { useEffect, useState } from 'react';

export default function Records() {
  const types = useMemo(() = > {
    return ['Personal Records'.'Family Records']; } []);const statusList = useMemo(() = > {
    return ['Full appointment'.'To be paid'.'Accepted']; } []);const [type, setType] = useState('Personal Records');
  const [status, setStatus] = useState('Full appointment');

  const getData = () = > {
    console.log('Get the latest list, type:The ${type}, state:${status}.The ${Date.now()}`);
  }

  useEffect(() = > {
    setStatus('Full appointment'); },type]);

  // Batch operation
  useBatchEffect(() = >{ getData(); },type, status]);

  return (
    <div>
      <div>
        {types.map((val) => {
          return (
            <button style={{ color: val= =type ? 'red' : 'black'}}key={val} onClick={()= > setType(val)}>
              {val}
            </button>
          );
        })}
      </div>
      <div>
        {statusList.map((val) => {
          return (
            <button style={{ color: val= =status ? 'red' : 'black'}}key={val} onClick={()= > setStatus(val)}>
              {val}
            </button>
          );
        })}
      </div>
    </div>
  );
}
Copy the code

conclusion

Compared with the implementation of the above three schemes, I recommend the third method. Compared with the other two methods, the code changes are minimal and the semantics are more intuitive. If you have any better suggestions, please leave them in the comments section.