I’m using React 16.8.6
with Hooks and Formik 1.5.7
to build a form with a preview of the content that will be generated with that data later on. The form runs fine on its own, but as long as I render the preview as well, everything becomes a bit slow and sluggish.
I’ve fixed that debouncing the onChange
of the form using setTimeout
, but I’d like it to be called periodically even if the user keeps typing:
const Preview = ({ values: { name = '', message = '' } }) => (<div className="preview"> <p><strong>{ name }</strong></p> { message.split(/r?nr?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>
Advertisement
Answer
You could define a custom withThrottledCallback
hook that takes care of that and replaces/combines these lines:
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); }, []);
Into something like this:
const throttledHandleFormChange = useThrottledCallback((values) => { setFormValues(values); }, 500, []);
And that fires regularly every 500 ms even if the user keeps typing.
This way, rather than using setTimeout
directly, you would be able to reuse this functionality declaratively, just like Dan Abramov suggests for setInterval
in Making setInterval Declarative with React Hooks.
It will look something like this:
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?nr?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>
You can also find declarative version of setTimeout
and setInterval
, useTimeout
and useInterval
, plus a custom useThrottledCallback
hook written in TypeScript in https://www.npmjs.com/package/@swyg/corre.