Entprellen mit React Hooks: Ein Funktions-Hook

Hallo! Mein Name ist Igor Shamaev, ich bin der Chefentwickler im SmartData-Team. Ich beschäftige mich mit der Full-Stack-Entwicklung eines internen analytischen BI-Systems. In unserem Unternehmen wird React als Hauptstandard für die Erstellung von Benutzeroberflächen akzeptiert. Wie die meisten Mitglieder der React-Community verwenden wir in unserer täglichen Arbeit häufig Haken.



Kontinuierliches Lernen ist ein wesentlicher Bestandteil der Arbeit eines guten Entwicklers. Deshalb möchte ich heute meinen bescheidenen Beitrag zu diesem Prozess leisten und einen kleinen Leitfaden für diejenigen präsentieren, die anfangen, aktiv zu lernen, zu reagieren und mit Haken zu arbeiten. Auf dem Weg dorthin erhalten Sie ein kleines und nützliches Werkzeug für die Arbeit mit dem neuen React-Standard.



In der Übersetzung des Artikels Debouncing with React Hooks haben wir gelernt, wie Sie ohne Bibliotheken von Drittanbietern, die nur die Funktionen von React verwenden, einen Hook in mehreren Codezeilen erstellen können, um mit verzögerten Änderungen von Variablenwerten zu arbeiten. Jetzt schlage ich vor, einen weiteren nützlichen Hook in Betracht zu ziehen, der uns hilft, einen Funktionsaufruf zu verschieben. Wenn die Funktion mehrmals hintereinander aufgerufen wird, erfolgt der eigentliche Aufruf erst nach dem von uns festgelegten Verzögerungsintervall. Das heißt, nur für den letzten Anruf in der Serie. Die Lösung ist auch sehr kompakt und einfach in React zu implementieren. Wenn Sie interessiert sind, bitte unter Katze.





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