阅读 215
React Hooks 事件绑定排坑指南

React Hooks 事件绑定排坑指南

背景

我们项目中有这么一个输入框的组件,会根据输入的关键字去后台获取数据并提供选项,每次关键字变化时都会触发一次请求,势必会对服务器造成不小的压力,当然组件内外对输入事件的触发或多或少都有做防抖的处理。这次问题在于即使在组件内对事件的触发做防抖,在父组件这层如果没有对绑定事件的函数做缓存或者依赖处理,也会在 rerender 时重新渲染子组件,从而导致防抖失效

举个例子,有这么一个组件 SearchInput

const SearchInput: React.FC<Props> = ({ onChange }) => {
  const [params, updateParams] = React.useState('');

  function handleValueChange(value: string) {
    updateParams(value);
    onChange({ keyword: value });
  }

  return (
    <div>
      <input
        type="text"
        value={params}
        onChange={(e) => handleValueChange(e.target.value)}
      />
    </div>
  );
};
复制代码

我们在父组件中这样使用 SearchInput

function App() {
  const [params, updateParams] = React.useState({ keyword: '' });
  const [output, updateOutput] = React.useState('');

  const handleChange = (obj: any) => {
    const newParams = { ...obj };
    updateParams(newParams);
    updateOutput(`keyword: ${newParams.keyword}`);
  };

  return (
    <div className="App">
      <SearchInput onChange={handleChange} />
      <div>{output}</div>
    </div>
  );
}
复制代码

正文

背景介绍中的两段代码中是我们常见的用法,正常情况下无需对代码做任何处理也不会有什么问题,但如果在 SearchInput 中对 props.onChange 做防抖处理,需要注意一下几点

传入的函数会变化

SearchInput 中对传入的 props.onChange 做如下的防抖处理

import debounce from 'lodash/debounce';

const SearchInput: React.FC<Props> = ({ onChange }) => {
  // 省略部分代码 ...
  
  const debounced = React.useRef(debounce(onChange, 200));
  React.useEffect(() => {
    debounced.current = debounce(onChange, 200);
    return debounced.current.cancel;
  }, [onChange]);
  
  function handleValueChange(value: string) {
    // ...
    debounced.current({ keyword: value });
  }
  
  // 省略部分代码 ...
};
复制代码

如果父组件中的 handleChange 足够简单,那么到这一步就算结束了。但我们的业务中往往会在这个函数中处理其他状态或者依赖于其他状态处理一些逻辑,而这些复杂情况又会导致 props.onChange 的变化

缓存事件处理函数

假设父组件的 handleChange 中包含了其他状态,此时 props.onChange 的频繁变化就会导致 SelectInput 组件中 useEffect 返回的回调函数疯狂触发,就会出现防抖失效甚至状态滞留的现象(如:输入了一段文本,按住删除键将文本全部删除,output 中可能会残留最后一个字母)

function App() {
  // 省略部分代码 ...
  const [pager, setPager] = React.useState({});

  const handleChange = (obj: any) => {
    const newPager = { ...pager, current: 1 };
    setPager(newPager);
    // ...
  };

  // 省略部分代码 ...
}
复制代码

可以在父组件中通过 useCallbackhandleChange 做缓存处理,使得传入 SelectInputprops.onChange 不会频繁变化

function App() {
  // 省略部分代码 ...

  const handleChange = React.useCallback((obj: any) => {
    // ...
  }, []);

  // 省略部分代码 ...
}
复制代码

因为 handleChange 中并没有依赖 pager 状态来处理其他逻辑,所以 useCallback 第二个参数可以传入一个空数组,使得该函数只在组件挂载时缓存一次,之后就不再变化了。但如果 handleChange 中需要依赖 pager 状态的话,例如

function App() {
  // 省略部分代码 ...
  
  const handleFetch = (newParams: any, newPager: any) => {
    updateOutput(
      `keyword: ${newParams.keyword}, pager: ${newPager.current}, ${newPager.size}`
    );
    setPager({ ...newPager, size: ~~(100 * Math.random()) });
  };

  const handleChange = React.useCallback((obj: any) => {
    // ...
    handleFetch(newParams, newPager);
  }, []);

  // 省略部分代码 ...
}
复制代码

此时 useCallback 的第二个参数传入空数组,函数中依赖的 pager 状态就不会变化了,后面依赖 pager 状态的 handleFetch 函数也就拿不到最新的参数。如果 useCallback 第二个参数传入 [pager] 的话,那就跟不做缓存处理时一样 props.onChange 会频繁变化。

函数式更新状态

处理这个问题的最简单的方法就是不要在 handleChange 中依赖任何状态处理逻辑,目前 handleChange 中只依赖了 pager 状态,那么我们可以通过 setPager 的函数式更新来获取 pager 状态

function App() {
  // 省略部分代码 ...

  const handleChange = React.useCallback((obj: any) => {
    setPager((pager: any) => {
      const newPager = { ...pager, current: 1 };
      const newParams = { ...obj };
      updateParams(newParams);
      handleFetch(newParams, newPager);
    });
  }, []);

  // 省略部分代码 ...
}
复制代码

用 useReducer 管理多个状态

如果 handleChange 中依赖了多个状态,那么可以使用 useReducer 整合所有状态,使用 dispatch 进行状态的更新,React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。所以可以安全地从 useEffectuseCallback 的依赖列表中省略 dispatch

function App() {
  const [state, dispatch] = React.useReducer(
    (state: any, action: any) => {
      if (action.type === 'params') {
        return { ...state, params: { ...state.params, ...action.params } };
      }
      if (action.type === 'pager') {
        return { ...state, pager: { ...state.pager, ...action.pager } };
      }
      if (action.type === 'filter') {
        return { ...state, filter: { ...state.filter, ...action.filter } };
      }
      if (action.type === 'output') {
        return {
          ...state,
          output: `keyword: ${state.params.keyword}, pager: ${state.pager.current}, ${state.pager.size}, filter: ${state.filter.pid}, ${state.filter.id}`,
        };
      }

      return { ...state };
    },
    {
      params: { keyword: '' },
      pager: {},
      filter: {},
      output: '',
    }
  );

  const handleFetch = (newParams: any, newPager: any, newFilter: any) => {
    dispatch({
      type: 'output',
    });
    dispatch({
      type: 'pager',
      pager: { ...newPager, size: ~~(100 * Math.random()) },
    });
    dispatch({
      type: 'filter',
      filter: { ...newFilter, id: ~~(10000 * Math.random() + 10000) },
    });
  };

  const handleChange = React.useCallback((obj: any) => {
    const newFilter = { pid: 0 };
    const newPager = { current: 1 };
    const newParams = { ...obj };
    dispatch({ type: 'params', params: newParams });
    dispatch({ type: 'pager', pager: newPager });
    dispatch({ type: 'filter', params: newFilter });
    handleFetch(newParams, newPager, newFilter);
  }, []);

  return (
    <div className="App">
      <SearchInput onChange={handleChange} />
      <div>{state.output}</div>
    </div>
  );
}
复制代码

总结

简单总结一下,我们在使用 React Hooks 处理事件绑定时要注意传入的函数是否变化、是否有依赖状态以及是否会根据状态的变化来处理逻辑等,本文以处理防抖为例介绍了几种情况以及解决方案,当然在业务中存在各种各样的其他情况,希望本文能给各位带来一些启发,望各位能在处理问题时保持心态从容不迫

文章分类
前端
文章标签