How to use throttle or debounce with React Hook?

后端 未结 17 989
甜味超标
甜味超标 2020-11-30 04:27

I\'m trying to use the throttle method from lodash in a functional component, e.g.:

const App = () => {
  const [value, setValue         


        
相关标签:
17条回答
  • 2020-11-30 04:56

    After some time passed I'm sure it's much easier to handle things by your own with setTimeout/clearTimeout(and moving that into separate custom hook) than working with functional helpers. Handling later one creates additional challenges right after we apply that to useCallback that can be recreated because of dependency change but we don't want to reset delay running.

    original answer below

    you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

    Something like that

    const App = () => {
      const [value, setValue] = useState(0)
      const throttled = useRef(throttle((newValue) => console.log(newValue), 1000))
    
      useEffect(() => throttled.current(value), [value])
    
      return (
        <button onClick={() => setValue(value + 1)}>{value}</button>
      )
    }
    

    As for useCallback:

    It may work too as

    const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []);
    

    But if we try to recreate callback once value is changed:

    const throttled = useCallback(throttle(() => console.log(value), 1000), [value]);
    

    we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

    So I see useCallback in case of delayed run does not provide significant advantage. It's up to you.

    [UPD] initially it was

      const throttled = useRef(throttle(() => console.log(value), 1000))
    
      useEffect(throttled.current, [value])
    

    but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

    So be careful while pushing functions into useRef because of closure feature.

    0 讨论(0)
  • 2020-11-30 04:58

    Here's a simple hook to debounce your calls.

    To use the below code, all you have to do is declare it as so

    const { debounceRequest } = useDebounce(someFn);

    And, then call it as so

    debounceRequest(); 
    

    Implementation is shown below

    import React from "react";
    
    const useDebounce = (callbackFn: () => any, timeout: number = 500) => {
    const [sends, setSends] = React.useState(0);
    
    const stabilizedCallbackFn = React.useCallback(callbackFn, [callbackFn]);
    
    const debounceRequest = () => {
      setSends(sends + 1);
    };
    
    // 1st send, 2nd send, 3rd send, 4th send ...
    // when the 2nd send comes, then 1st set timeout is cancelled via clearInterval
    // when the 3rd send comes, then 2nd set timeout is cancelled via clearInterval
    // process continues till timeout has passed, then stabilizedCallbackFn gets called
    // return () => clearInterval(id) is critical operation since _this_ is what cancels 
    //  the previous send.
    // *                                                                    
    0 讨论(0)
  • 2020-11-30 05:02

    I wrote two simple hooks (use-throttled-effect and use-debounced-effect) for this use case maybe it wil be useful for someone else looking for a simple solution.

    import React, { useState } from 'react';
    import useThrottledEffect  from 'use-throttled-effect';
    
    export default function Input() {
      const [count, setCount] = useState(0);
    
      useEffect(()=>{
        const interval = setInterval(() => setCount(count=>count+1) ,100);
        return ()=>clearInterval(interval);
      },[])
    
      useThrottledEffect(()=>{
        console.log(count);     
      }, 1000 ,[count]);
    
      return (
        {count}
      );
    }
    
    0 讨论(0)
  • 2020-11-30 05:03

    useThrottle , useDebounce

    How to use both

    const App = () => {
      const [value, setValue] = useState(0);
      // called at most once per second (same API with useDebounce)
      const throttledCb = useThrottle(() => console.log(value), 1000);
      // usage with useEffect: invoke throttledCb on value change
      useEffect(throttledCb, [value]);
      // usage as event handler
      <button onClick={throttledCb}>log value</button>
      // ... other render code
    };
    

    useThrottle (Lodash)

    import _ from "lodash"
    
    function useThrottle(cb, delay) {
      const options = { leading: true, trailing: false }; // add custom lodash options
      const cbRef = useRef(cb);
      // use mutable ref to make useCallback/throttle not depend on `cb` dep
      useEffect(() => { cbRef.current = cb; });
      return useCallback(
        _.throttle((...args) => cbRef.current(...args), delay, options),
        [delay]
      );
    }
    

    const App = () => {
      const [value, setValue] = useState(0);
      const invokeDebounced = useThrottle(
        () => console.log("changed throttled value:", value),
        1000
      );
      useEffect(invokeDebounced, [value]);
      return (
        <div>
          <button onClick={() => setValue(value + 1)}>{value}</button>
          <p>value will be logged at most once per second.</p>
        </div>
      );
    };
    
    function useThrottle(cb, delay) {
      const options = { leading: true, trailing: false }; // pass custom lodash options
      const cbRef = useRef(cb);
      useEffect(() => {
        cbRef.current = cb;
      });
      return useCallback(
        _.throttle((...args) => cbRef.current(...args), delay, options),
        [delay]
      );
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity="sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ=" crossorigin="anonymous"></script>
    <script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
    <div id="root"></div>

    useDebounce (Lodash)

    import _ from "lodash"
    
    function useDebounce(cb, delay) {
      // ...
      const inputsRef = useRef(cb); // mutable ref like with useThrottle
      useEffect(() => { inputsRef.current = { cb, delay }; }); //also track cur. delay
      return useCallback(
        _.debounce((...args) => {
            // Debounce is an async callback. Cancel it, if in the meanwhile
            // (1) component has been unmounted (see isMounted in snippet)
            // (2) delay has changed
            if (inputsRef.current.delay === delay && isMounted())
              inputsRef.current.cb(...args);
          }, delay, options
        ),
        [delay, _.debounce]
      );
    }
    

    const App = () => {
      const [value, setValue] = useState(0);
      const invokeDebounced = useDebounce(
        () => console.log("debounced", value),
        1000
      );
      useEffect(invokeDebounced, [value]);
      return (
        <div>
          <button onClick={() => setValue(value + 1)}>{value}</button>
          <p> Logging is delayed until after 1 sec. has elapsed since the last invocation.</p>
        </div>
      );
    };
    
    function useDebounce(cb, delay) {
      const options = {
        leading: false,
        trailing: true
      };
      const inputsRef = useRef(cb);
      const isMounted = useIsMounted();
      useEffect(() => {
        inputsRef.current = { cb, delay };
      });
    
      return useCallback(
        _.debounce(
          (...args) => {
            // Don't execute callback, if (1) component in the meanwhile 
            // has been unmounted or (2) delay has changed
            if (inputsRef.current.delay === delay && isMounted())
              inputsRef.current.cb(...args);
          },
          delay,
          options
        ),
        [delay, _.debounce]
      );
    }
    
    function useIsMounted() {
      const isMountedRef = useRef(true);
      useEffect(() => {
        return () => {
          isMountedRef.current = false;
        };
      }, []);
      return () => isMountedRef.current;
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity="sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ=" crossorigin="anonymous"></script>
    <script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
    <div id="root"></div>


    Customizations

    1. You might replace Lodash with your own throttle or debounce code, like:

    const debounceImpl = (cb, delay) => {
      let isDebounced = null;
      return (...args) => {
        clearTimeout(isDebounced);
        isDebounced = setTimeout(() => cb(...args), delay);
      };
    };
    
    const throttleImpl = (cb, delay) => {
      let isThrottled = false;
      return (...args) => {
        if (isThrottled) return;
        isThrottled = true;
        cb(...args);
        setTimeout(() => {
          isThrottled = false;
        }, delay);
      };
    };
    
    const App = () => {
      const [value, setValue] = useState(0);
      const invokeThrottled = useThrottle(
        () => console.log("throttled", value),
        1000
      );
      const invokeDebounced = useDebounce(
        () => console.log("debounced", value),
        1000
      );
      useEffect(invokeThrottled, [value]);
      useEffect(invokeDebounced, [value]);
      return <button onClick={() => setValue(value + 1)}>{value}</button>;
    };
    
    function useThrottle(cb, delay) {
      const cbRef = useRef(cb);
      useEffect(() => {
        cbRef.current = cb;
      });
      return useCallback(
        throttleImpl((...args) => cbRef.current(...args), delay),
        [delay]
      );
    }
    
    function useDebounce(cb, delay) {
      const cbRef = useRef(cb);
      useEffect(() => {
        cbRef.current = cb;
      });
      return useCallback(
        debounceImpl((...args) => cbRef.current(...args), delay),
        [delay]
      );
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
    <script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
    <div id="root"></div>

    2. useThrottle can be shortened up, if always used with useEffect (same for useDebounce):

    const App = () => {
      // useEffect now is contained inside useThrottle
      useThrottle(() => console.log(value), 1000, [value]);
      // ...
    };
    

    const App = () => {
      const [value, setValue] = useState(0);
      useThrottle(() => console.log(value), 1000, [value]);
      return (
        <div>
          <button onClick={() => setValue(value + 1)}>{value}</button>
          <p>value will be logged at most once per second.</p>
        </div>
      );
    };
    
    function useThrottle(cb, delay, additionalDeps) {
      const options = { leading: true, trailing: false }; // pass custom lodash options
      const cbRef = useRef(cb);
      const throttledCb = useCallback(
        _.throttle((...args) => cbRef.current(...args), delay, options),
        [delay]
      );
      useEffect(() => {
        cbRef.current = cb;
      });
      // set additionalDeps to execute effect, when other values change (not only on delay change)
      useEffect(throttledCb, [throttledCb, ...additionalDeps]);
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity="sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ=" crossorigin="anonymous"></script>
    <script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
    <div id="root"></div>

    0 讨论(0)
  • 2020-11-30 05:05

    It could be a tiny custom hook, like this:

    useDebounce.js

    import React, { useState, useEffect } from 'react';
    
    export default (value, timeout) => {
        const [state, setState] = useState(value);
    
        useEffect(() => {
            const handler = setTimeout(() => setState(value), timeout);
    
            return () => clearTimeout(handler);
        }, [value, timeout]);
    
        return state;
    }
    

    Usage example:

    import React, { useEffect } from 'react';
    
    import useDebounce from '/path/to/useDebounce';
    
    const App = (props) => {
        const [state, setState] = useState({title: ''});    
        const debouncedTitle = useDebounce(state.title, 1000);
    
        useEffect(() => {
            // do whatever you want with state.title/debouncedTitle
        }, [debouncedTitle]);        
    
        return (
            // ...
        );
    }
    // ...
    

    Note: As you probably know, useEffect always run on initial render, and because of that if you use my answer, you will probably see your component's render runs twice, don't worry, you just need to writing another custom hook. check out my other answer for more info.

    0 讨论(0)
  • 2020-11-30 05:05

    I'd like to join the party with my input debounce using useState:

    // import { React, useState } from 'react' // nomral import
    const { useState } = React // inline import
    
    const App = () => {
      const [t, setT] = useState();
      
      const handleChange = ({ target: { value } }) => {
        clearTimeout(t)
        setT(setTimeout(() => console.log(value), 700))
      }
      
      return (
        <input onChange={handleChange} />
      )
    }
    
    ReactDOM.render(<App />, document.getElementById('root'))
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
    <div id="root"></div>

    0 讨论(0)
提交回复
热议问题