Reactフックでデバウンス:関数フック

こんにちは!私の名前はIgorShamaevです。SmartDataチームのチーフ開発エンジニアです。私は内部分析BIシステムのフルスタック開発に従事しています。当社では、ユーザーインターフェースを構築するための主要な標準としてReactが認められています。ほとんどのReactコミュニティと同様に、私たちは日常業務でフックを幅広く使用しています。



継続的な学習は、優れた開発者の仕事の不可欠な部分です。それで、今日、私はこのプロセスに謙虚に貢献し、Reactを積極的に学び始めてフックを使って作業し始めている人々のために小さなガイドを提示したいと思います。そしてその過程で、新しいReact標準を操作するための小さくて便利なツールを提供します。



記事Debouncingwith React Hooksの翻訳では、サードパーティのライブラリを使用せずに、Reactの機能のみを使用して、コードの数行にフックを作成し、変数値の遅延変更を処理する方法を学びました。ここで、関数呼び出しを延期するのに役立つ別の便利なフックを検討することを提案します。関数が連続して何度も呼び出される場合、実際の呼び出しは、設定した遅延間隔の後にのみ発生します。つまり、シリーズの最後の呼び出しのみです。このソリューションは非常にコンパクトで、Reactに簡単に実装できます。興味のある方は猫の下でお願いします。





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