Rebote con React Hooks: un gancho de función

¡Hola! Mi nombre es Igor Shamaev, soy el ingeniero jefe de desarrollo del equipo de SmartData. Estoy involucrado en el desarrollo completo de un sistema de BI analítico interno. En nuestra empresa, React se acepta como el principal estándar para crear interfaces de usuario. Como la mayoría de la comunidad de React, usamos los ganchos de manera extensa en nuestro trabajo diario.



El aprendizaje continuo es una parte integral del trabajo de cualquier buen desarrollador. Por eso hoy me gustaría hacer mi humilde contribución a este proceso y presentar una pequeña guía para aquellos que están comenzando a aprender activamente a React y a trabajar con hooks. Y en el camino, le proporcionaremos una pequeña y útil herramienta para trabajar con el nuevo estándar React.



En la traducción del artículo Debouncing with React Hooks, aprendimos cómo, sin bibliotecas de terceros, usando solo las capacidades de React, puede crear un gancho en varias líneas de código para trabajar con cambios perezosos en los valores de las variables. Ahora propongo considerar otro gancho útil que nos ayudará a diferir una llamada de función. Si la función se llamará muchas veces seguidas, entonces la llamada real solo ocurrirá después del intervalo de retraso que establecimos. Es decir, solo para la última convocatoria de la serie. La solución también es muy compacta y fácil de implementar en React. Si está interesado, por favor, debajo del gato.





React . . , React , .



...
import { useRef, useEffect } from "react";

export default function useDebouncedFunction(func, delay, cleanUp = false) {
  const timeoutRef = useRef();

  function clearTimer() {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = undefined;
    }
  }

  useEffect(() => (cleanUp ? clearTimer : undefined), [cleanUp]);

  return (...args) => {
    clearTimer();
    timeoutRef.current = setTimeout(() => func(...args), delay);
  };
}


, - , . , . Material-UI.



import React from "react";
import { makeStyles, Typography, Slider } from "@material-ui/core";
import useDebouncedFunction from "./useDebouncedFunction";
import apiRequest from "./apiRequest";

const useStyles = makeStyles({
  root: {
    width: 300
  }
});

function valuetext(value) {
  return `${value}°C`;
}

export default function RangeSlider() {
  const classes = useStyles();
  const [value, setValue] = React.useState([20, 37]);

  const handleChange = (event, newValue) => {
    setValue(newValue);
  };

  return (
    <div className={classes.root}>
      <Typography id="range-slider" gutterBottom>
        Temperature range
      </Typography>
      <Slider
        value={value}
        onChange={handleChange}
        valueLabelDisplay="auto"
        aria-labelledby="range-slider"
        getAriaValueText={valuetext}
      />
    </div>
  );
}




, - , . , . , - , . , , console.log():



export default function valueLogging(value) {
  console.log(`Request processed. Value: ${value}`);
}


handleChange() valueLogging() , :



  const handleChange = (event, newValue) => {
    setValue(newValue);
    valueLogging(newValue);
  };




… , . valueLogging() . . . ?



1. useDebounce value .



useDebounce debouncedValue, . useEffect valueLogging(). - :



export default function RangeSlider() {
  const classes = useStyles();
  const [value, setValue] = React.useState([20, 37]);
  const [changedByUser, setChangedByUser] = React.useState(false);

  const debouncedValue = useDebounce(value, 300);

  useEffect(() => {
    if (changedByUser) {
      valueLogging(debouncedValue);
    }
  }, [debouncedValue]);

  const handleChange = (event, newValue) => {
    setValue(newValue);
    if (!changedByUser) {
        setChangedByUser(true);
    }
  };

  return (
    <div className={classes.root}>
      <Typography id="range-slider" gutterBottom>
        Temperature range
      </Typography>
      <Slider
        value={value}
        onChange={handleChange}
        valueLabelDisplay="auto"
        aria-labelledby="range-slider"
        getAriaValueText={valuetext}
      />
    </div>
  );
}


, , ? , useEffect, , valueLogging() . , . , useEffect , valueLogging(). , React useEffect changedByUser. , .



?



2. valueLogging() handleChange().



, :



export default function RangeSlider() {
  const classes = useStyles();
  const [value, setValue] = React.useState([20, 37]);

  const debouncedValueLogging = useDebouncedFunction(valueLogging, 300);

  const handleChange = (event, newValue) => {
    setValue(newValue);
    debouncedValueLogging(newValue);
  };

  return (
    <div className={classes.root}>
      <Typography id="range-slider" gutterBottom>
        Temperature range
      </Typography>
      <Slider
        value={value}
        onChange={handleChange}
        valueLabelDisplay="auto"
        aria-labelledby="range-slider"
        getAriaValueText={valuetext}
      />
    </div>
  );
}


useDebouncedFunction, .



React:



import { useRef } from "react";

export default function useDebouncedFunction(func, delay) {
  const ref = useRef(null);

  return (...args) => {
    clearTimeout(ref.current);
    ref.current = setTimeout(() => func(...args), delay);
  };
}


, . , . -, useRef ( useRef). : , React. -, . current .



, useRef, , DOM-. , . .

setTimeout() . timeoutId, setTimeout(), ref.current. , useDebouncedFunction. timeoutId clearTimeout(). , . , valueLogging() 300 . .



, ... useRef? ?

let timeoutId; :



export default function useDebouncedFunction(func, delay) {
  let timeoutId;

  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func(...args), delay);
  };
}


React. React . , , :





- , debouncedValueLogging() timeoutId. .



, :





! . , .



, . , - . . , , ?



, - . , , .



.



-, , value . , , RangeSlider .



import React, { useState } from "react";
import { ThemeProvider, createMuiTheme, Typography } from "@material-ui/core";
import RangeSlider from "./RangeSlider";

const theme = createMuiTheme({});

export default function App() {
  const [sliderShown, setSliderShown] = useState(true);

  //      
  function handleValueChange(value) {
    if (value[1] === 100) {
      setSliderShown(false);
    }
  }

  return (
    <ThemeProvider theme={theme}>
      {sliderShown ? (
        <RangeSlider onValueChange={handleValueChange} />
      ) : (
        <Typography variant="h2">Too hot!</Typography>
      )}
    </ThemeProvider>
  );
}


-, RangeSlider , , , . , - , , , . .



import React from "react";
import { makeStyles, Typography, Slider } from "@material-ui/core";
import useDebouncedFunction from "./useDebouncedFunction";
import valueLogging from "./valueLogging";
import checkIfOptimal from "./checkIfOptimal";

const useStyles = makeStyles({
  root: {
    width: 300
  }
});

function valuetext(value) {
  return `${value}°C`;
}

export default function RangeSlider(props) {
  const classes = useStyles();
  const [value, setValue] = React.useState([20, 37]);
  const [isOptimal, setIsOptimal] = React.useState(true);

  //  
  const debouncedValueLogging = useDebouncedFunction(
    newValue => valueLogging(newValue),
    300
  );

  //   
  const debouncedValueCheck = useDebouncedFunction(
    newValue => checkIfOptimal(newValue, setIsOptimal),
    300
  );

  const handleChange = async (event, newValue) => {
    setValue(newValue);
    debouncedValueLogging(newValue);
    debouncedValueCheck(newValue);
    if (props.onValueChange) {
      props.onValueChange(newValue);
    }
  };

  return (
    <div className={classes.root}>
      <Typography id="range-slider" gutterBottom>
        Temperature range
      </Typography>
      <Slider
        value={value}
        onChange={handleChange}
        valueLabelDisplay="auto"
        aria-labelledby="range-slider"
        getAriaValueText={valuetext}
        style={{ color: isOptimal ? "#4caf50" : "#f44336" }}
      />
    </div>
  );
}


-, checkIfOptimal():



//   
export default function checkIfOptimal(newValue, setIsOptimal) {
  return setIsOptimal(10 < newValue[0] && newValue[1] < 80);
}


, :





, , checkIfOptimal().





, React :



Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

in RangeSlider (at App.js:20)

? , true/false . setIsOptimal(). , 300 . . React. . ?



useDebouncedFunction: cleanUp. true, .



import { useRef, useEffect } from "react";

export default function useDebouncedFunction(func, delay, cleanUp = false) {
  const timeoutRef = useRef();

  //  
  function clearTimer() {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = undefined;
    }
  }

  //     ,  cleanUp   true
  //       
  useEffect(() => (cleanUp ? clearTimer : undefined), [cleanUp]);

  return (...args) => {
    clearTimer();
    timeoutRef.current = setTimeout(() => func(...args), delay);
  };
}


useEffect , . clearTimer() .

.



  //   
  const debouncedValueCheck = useDebouncedFunction(
    newValue => checkIfOptimal(newValue, setIsOptimal),
    300,
    true
  );


.





, . , .



?



, . checkIfOptimal() — , . checkIfOptimal() , , , .



? useDebouncedFunction , , , .



, .



. , .



, , codesandbox. :



useDebouncedFunction codesandbox




All Articles