116 votes

Comment utiliser l'accélérateur ou le ralentisseur avec React Hook ?

J'essaie d'utiliser le throttle méthode de lodash dans un composant fonctionnel, par exemple :

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

Puisque la méthode dans useEffect est redéclarée à chaque rendu, l'effet d'étranglement ne fonctionne pas.

Quelqu'un a-t-il une solution simple ?

1 votes

Est-ce une option pour vous de définir la fonction étranglée en dehors de la App et l'appeler simplement dans le composant useEffect fonction ?

0 votes

Oui, j'ai essayé et cela fonctionne, mais dans mon cas, ce n'est pas très élégant, car j'utilise des variables de composants dans la méthode d'étranglement.

114voto

skyboyer Points 36

Après un certain temps, je suis sûr que c'est plus facile de gérer les choses par soi-même avec setTimeout/clearTimeout (et le déplacer dans un hook personnalisé séparé) que de travailler avec des aides fonctionnelles. La gestion de cette dernière crée des défis supplémentaires juste après que nous l'ayons appliquée à l'aide fonctionnelle. useCallback qui peut être recréé en raison d'un changement de dépendance, mais nous ne voulons pas réinitialiser le délai d'exécution.

réponse originale ci-dessous

vous pouvez (et avez probablement besoin) useRef pour stocker la valeur entre les rendus. Tout comme l'est suggéré pour les chronométreurs

Quelque chose comme ça

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>
  )
}

Quant à useCallback :

Cela peut aussi fonctionner comme

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

Mais si nous essayons de recréer le callback une fois value est modifié :

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

nous pourrons constater que cela ne retarde pas l'exécution : une fois que value est modifié, la callback est immédiatement recréée et exécutée.

Donc je vois useCallback en cas d'exécution différée n'apporte pas d'avantage significatif. C'est vous qui décidez.

[UPD] initialement c'était

  const throttled = useRef(throttle(() => console.log(value), 1000))

  useEffect(throttled.current, [value])

mais de cette façon throttled.current est liée à l'initiale value (de 0) par fermeture. Il n'a donc jamais été modifié, même sur les rendus suivants.

Soyez donc prudent en poussant les fonctions dans useRef en raison de la fonction de fermeture.

58voto

Todd Skelton Points 991

J'ai créé mon propre crochet personnalisé appelé useDebouncedEffect qui attendra pour effectuer un useEffect jusqu'à ce que l'état n'ait pas été mis à jour pendant la durée du délai.

Dans cet exemple, votre effet sera enregistré dans la console après que vous ayez cessé de cliquer sur le bouton pendant 1 seconde.

Exemple de bac à sable https://codesandbox.io/s/react-use-debounced-effect-6jppw

App.jsx

import { useState } from "react";
import { useDebouncedEffect } from "./useDebouncedEffect";

const App = () => {
  const [value, setValue] = useState(0)

  useDebouncedEffect(() => console.log(value), [value], 1000);

  return (
    <button onClick={() => setValue(value + 1)}>{value}</button>
  )
}

export default App;

useDebouncedEffect.js

import { useEffect } from "react";

export const useDebouncedEffect = (effect, deps, delay) => {
    useEffect(() => {
        const handler = setTimeout(() => effect(), delay);

        return () => clearTimeout(handler);
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [...deps || [], delay]);
}

Le commentaire pour désactiver exhaustive-deps est nécessaire à moins que vous ne vouliez voir un avertissement parce que lint se plaindra toujours de ne pas avoir effect comme dépendance. Ajouter effect comme dépendance déclenchera useEffect à chaque rendu. Au lieu de cela, vous pouvez ajouter la vérification à useDebouncedEffect pour s'assurer que toutes les dépendances lui sont transmises. (voir ci-dessous)

Ajout d'une vérification exhaustive des dépendances à useDebouncedEffect

Si vous voulez que eslint vérifie useDebouncedEffect pour des dépendances exhaustives, vous pouvez l'ajouter à la configuration d'eslint en package.json

  "eslintConfig": {
    "extends": [
      "react-app"
    ],
    "rules": {
      "react-hooks/exhaustive-deps": ["warn", {
        "additionalHooks": "useDebouncedEffect"
      }]
    }
  },

https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks#advanced-configuration

36voto

ford04 Points 112

useThrottle , useDebounce

Comment utiliser les deux

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, delay}); // 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>

Personnalisations

1. Vous pouvez remplacer Lodash par votre propre throttle o debounce code, comme :

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 peut être raccourci, s'il est toujours utilisé avec useEffect (idem pour 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>

18voto

Mehdi Dehghani Points 3665

Ça pourrait être un petit crochet personnalisé, comme celui-ci :

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;
}

Exemple d'utilisation :

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 (
        // ...
    );
}
// ...

Nota: Comme vous le savez probablement, useEffect s'exécute toujours lors du rendu initial, et à cause de cela, si vous utilisez ma réponse, vous verrez probablement que le rendu de votre composant s'exécute deux fois, ne vous inquiétez pas, vous devez juste écrire un autre hook personnalisé. consultez mon autre réponse pour plus d'informations.

13voto

Jaskaran Singh Points 1117

Debounce avec l'aide du hook useCallback.

import React, { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';

function App() {
    const [value, setValue] = useState('');
    const [dbValue, saveToDb] = useState(''); // would be an API call normally

    // highlight-starts
    const debouncedSave = useCallback(
        debounce(nextValue => saveToDb(nextValue), 1000),
        [], // will be created only once initially
    );
    // highlight-ends

    const handleChange = event => {
        const { value: nextValue } = event.target;
        setValue(nextValue);
        // Even though handleChange is created on each render and executed
        // it references the same debouncedSave that was created initially
        debouncedSave(nextValue);
    };

    return <div></div>;
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X