javascript - 使用钩子(Hook)在 React 中消除/限制回调,而无需等待用户停止输入来获取更新

标签 javascript reactjs react-hooks throttling debouncing

我使用 React 16.8.6 以及 Hooks 和 Formik 1.5.7 来构建一个表单,其中包含稍后将使用该数据生成的内容的预览在。该表单本身运行良好,但只要我也渲染预览,一切都会变得有点缓慢和迟缓。

我已经使用 setTimeout 修复了表单的 onChange 去抖动问题,但我希望即使用户不断输入也能定期调用它:

const Preview = ({ values: { name = '', message = '' } }) => (<div className="preview">
  <p><strong>{ name }</strong></p>
  { message.split(/\r?\n\r?\n/g).filter(Boolean).map((text, i) => (<p key={ i }>{ text }</p>)) }
</div>);

const Form = ({ onChange }) => {
  const [values, setValues] = React.useState({});
  
  // Let's assume this is internal code from Formik...:
  
  const handleChange = React.useCallback(({ target }) => {
    setValues(values => {
      const nextValues = ({ ...values, [target.name]: target.value });
      
      onChange(nextValues);
    
      return nextValues;
    });
  }, []);
  
  return (<form>
    <input type="text" value={ values.name || '' } name="name" onChange={ handleChange } />
    <textarea value={ values.message || '' } name="message" onChange={ handleChange } />
  </form>);
};

const App = () => {
  const [formValues, setFormValues] = React.useState({});
  const timeoutRef = React.useRef();
  
  React.useEffect(() => window.clearTimeout(timeoutRef.current), []);
  
  const handleFormChange = React.useCallback((values) => {
    window.clearTimeout(timeoutRef.current);
    
    timeoutRef.current = window.setTimeout(() => setFormValues(values), 500);
  }, []);
  
  return (<div className="editor">
    <Form onChange={ handleFormChange } />
    <Preview values={ formValues } />
  </div>);
};

ReactDOM.render(<App />, document.querySelector('#app'));
body {
  margin: 0;
}

body,
input,
textarea {
  font-family: monospace;
}

.editor {
  display: flex;
}

form,
.preview {
  position: relative;
  max-width: 480px;
  width: 100%;
  margin: 0 auto;
  padding: 8px;
  
}

input,
textarea {
  border: 2px solid black;
  border-radius: 2px;
  display: flex;
  padding: 8px;
  margin: 0 auto 8px;
  width: 100%;
  box-sizing: border-box;
}

.preview {
  border-left: 2px solid black;
}
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

最佳答案

您可以定义一个自定义 withThrottledCallback Hook 来处理该问题并替换/组合这些行:

const timeoutRef = React.useRef();

React.useEffect(() => window.clearTimeout(timeoutRef.current), []);

const handleFormChange = React.useCallback((values) => {
    window.clearTimeout(timeoutRef.current);

    timeoutRef.current = window.setTimeout(() => setFormValues(values), 500);
}, []);

像这样:

const throttledHandleFormChange = useThrottledCallback((values) => {
    setFormValues(values);
}, 500, []);

即使用户不断输入,也会每 500 毫秒定期触发一次。

这样,您就可以以声明方式重用此功能,而不是直接使用 setTimeout,就像 Dan Abramov 在 Making setInterval Declarative with React Hooks 中建议的 setInterval 一样。 .

它看起来像这样:

function useThrottledCallback(callback, delay, deps) {
  const timeoutRef = React.useRef();
  const callbackRef = React.useRef(callback);
  const lastCalledRef = React.useRef(0);

  // Remember the latest callback:
  //
  // Without this, if you change the callback, when setTimeout kicks in, it
  // will still call your old callback.
  //
  // If you add `callback` to useCallback's deps, it will also update, but it
  // might be called twice if the timeout had already been set.

  React.useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Clear timeout if the components is unmounted or the delay changes:
  React.useEffect(() => window.clearTimeout(timeoutRef.current), [delay]);

  return React.useCallback((...args) => {
    // Clear previous timer:
    window.clearTimeout(timeoutRef.current);

    function invoke() {
      callbackRef.current(...args);
      lastCalledRef.current = Date.now();
    }

    // Calculate elapsed time:
    const elapsed = Date.now() - lastCalledRef.current;

    if (elapsed >= delay) {
      // If already waited enough, call callback:
      invoke();
    } else {
      // Otherwise, we need to wait a bit more:
      timeoutRef.current = window.setTimeout(invoke, delay - elapsed);
    }
  }, deps);
}

const Preview = ({ values: { name = '', message = '' } }) => (<div className="preview">
  <p><strong>{ name }</strong></p>
  { message.split(/\r?\n\r?\n/g).filter(Boolean).map((text, i) => (<p key={ i }>{ text }</p>)) }
</div>);

const Form = ({ onChange }) => {
  const [values, setValues] = React.useState({});
  
  // Let's assume this is internal code from Formik...:
  
  const handleChange = React.useCallback(({ target }) => {
    setValues(values => {
      const nextValues = ({ ...values, [target.name]: target.value });
      
      onChange(nextValues);
    
      return nextValues;
    });
  }, []);
  
  return (<form>
    <input type="text" value={ values.name || '' } name="name" onChange={ handleChange } />
    <textarea value={ values.message || '' } name="message" onChange={ handleChange } />
  </form>);
};

const App = () => {
  const [formValues, setFormValues] = React.useState({});
    
  const throttledHandleFormChange = useThrottledCallback((values) => {
    setFormValues(values);
  }, 500, []);
  
  return (<div className="editor">
    <Form onChange={ throttledHandleFormChange } />
    <Preview values={ formValues } />
  </div>);
};

ReactDOM.render(<App />, document.querySelector('#app'));
body {
  margin: 0;
}

body,
input,
textarea {
  font-family: monospace;
}

.editor {
  display: flex;
}

form,
.preview {
  position: relative;
  max-width: 480px;
  width: 100%;
  margin: 0 auto;
  padding: 8px;
  
}

input,
textarea {
  border: 2px solid black;
  border-radius: 2px;
  display: flex;
  padding: 8px;
  margin: 0 auto 8px;
  width: 100%;
  box-sizing: border-box;
}

textarea {
  resize: vertical;
}

.preview {
  border-left: 2px solid black;
}
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

您还可以找到 setTimeoutsetIntervaluseTimeoutuseInterval 的声明式版本,以及自定义 useThrottledCallback 钩子(Hook)在 https://www.npmjs.com/package/@swyg/corre 中用 TypeScript 编写.

关于javascript - 使用钩子(Hook)在 React 中消除/限制回调,而无需等待用户停止输入来获取更新,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59277957/

相关文章:

javascript - 如何让所有子文本用 ; 分隔在 JavaScript 中?

javascript - jQuery UI 模态对话框 : Close and Resize Icons in Wrong Place

javascript - Pixi.js - 窗口大小调整后重新定位元素

javascript - 动态启用/禁用表单提交按钮的规范 jQuery 方法

reactjs - Service Worker 范围不匹配

javascript - Antd 4.0.0 : How to use form. getFieldValue() 和所有表单的 API 为 `const [form] = Form.useForm` ?

node.js - Material-ui AppBar。滚动时更改颜色。 react

reactjs - 在 React Hooks 中输入时,如何阻止 Editor draftJS 光标跳转到文本开头?

graphql - useQuery 错误对象为 graphQLErrors 属性返回空数组

javascript - 当 reducer 函数依赖于组件 prop 时,传递给 useReducer 钩子(Hook)的 Reducer 函数会针对一次调度调用执行多次