React useCallback с debounce работает со старым значением, как получить фактическое значение состояния?
Не могу выполнить все условия:
- Мне нужна какая-то функция внутри
useCallback
, потому что я установил его как свойства для дочернего компонента (для предотвращения повторного рендеринга) - Мне нужно использовать
debounce
, потому что моя функция является "конечной точкой" и может вызываться ~100 раз / сек. - Мне нужно получить текущие (фактические значения) после дебона.
У меня проблема с последней точкой, мои значения после debounce (1000ms) устарели.
Как получить текущие значения с помощью useCallback
+ debounce
? (значения в предупреждении должны быть такими же, как на странице)
//ES6 const, let
//ES6 Destructuring
const { Component, useCallback, useState, useEffect } = React;
const SUBChildComponent = (props) => (<button onClick={props.getVal}>GetValue with debounce</button>);
const ChildComponent = () => {
// some unstable states
const [someVal1, setSomeVal1] = useState(0);
const [someVal2, setSomeVal2] = useState(0);
const [someVal3, setSomeVal3] = useState(0);
// some callback witch works with states AND called from subClild components
const getVal = useCallback(_.debounce(() => {
alert(`${someVal1}\n${someVal2}\n${someVal3}`);
}, 1000), [someVal1, someVal2, someVal3]);
// some synthetic changes
useEffect(() => {
const id = setInterval(() => setSomeVal1(someVal1 + 1), 50);
return () => clearInterval(id);
}, [someVal1]);
// some synthetic changes
useEffect(() => {
const id = setInterval(() => setSomeVal2(someVal2 + 1), 100);
return () => clearInterval(id);
}, [someVal2]);
// some synthetic changes
useEffect(() => {
const id = setInterval(() => setSomeVal3(someVal3 + 1), 250);
return () => clearInterval(id);
}, [someVal3]);
return <React.Fragment><SUBChildComponent getVal={getVal}/><br/>{someVal1}<br/>{someVal2}<br/>{someVal3}
</React.Fragment>;
};
class App extends Component {
render() {
return (<div><ChildComponent/></div>);
}
}
ReactDOM.render(<App/>, document.querySelector(".container"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
<div class="container"></div>
1 ответ
Прежде всего, вы должны отметить, что функция debounce устанавливает состояния с момента своего закрытия при создании. Теперь функция выполняется через несколько секунд, и к тому времени состояния уже изменились. Также новый экземпляр debounce будет создаваться каждый раз при обновлении состояний, поэтому, если вы вообще используете функцию debounce onClick, она не будет работать правильно, поскольку разные вызовы будут вызывать разные экземпляры функции debounce, а не один и тот же
Решение в таких случаях состоит в том, чтобы передать значения состояния в качестве аргумента функции debounce вместо того, чтобы позволить ей полагаться на закрытие. Однако он по-прежнему использует значение, с которым был вызван debounce, как вы можете видеть во фрагменте ниже.
//ES6 const, let
//ES6 Destructuring
const { Component, useCallback, useState, useEffect } = React;
const SUBChildComponent = ({getVal, someVal1,someVal2,someVal3}) => (<button onClick={() => getVal(someVal1,someVal2,someVal3)}>GetValue with debounce</button>);
const ChildComponent = () => {
// some unstable states
const [someVal1, setSomeVal1] = useState(0);
const [someVal2, setSomeVal2] = useState(0);
const [someVal3, setSomeVal3] = useState(0);
// some callback witch works with states AND called from subClild components
const getVal = useCallback(_.debounce((val1, val2, val3) => {
alert(`${val1}\n${val2}\n${val3}`);
}, 1000), []); // create debounce function only once
// some synthetic changes
useEffect(() => {
const id = setInterval(() => setSomeVal1(someVal1 + 1), 50);
return () => clearInterval(id);
}, [someVal1]);
// some synthetic changes
useEffect(() => {
const id = setInterval(() => setSomeVal2(someVal2 + 1), 100);
return () => clearInterval(id);
}, [someVal2]);
// some synthetic changes
useEffect(() => {
const id = setInterval(() => setSomeVal3(someVal3 + 1), 250);
return () => clearInterval(id);
}, [someVal3]);
return <React.Fragment><SUBChildComponent someVal1={someVal1} someVal2={someVal2} someVal3={someVal3} getVal={getVal}/><br/>{someVal1}<br/>{someVal2}<br/>{someVal3}
</React.Fragment>;
};
class App extends Component {
render() {
return (<div><ChildComponent/></div>);
}
}
ReactDOM.render(<App/>, document.querySelector(".container"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
<div class="container"></div>
Теперь другое решение - сохранить ссылки состояния и использовать их в функции debounce, что вы хотите в вашем случае.
//ES6 const, let
//ES6 Destructuring
const { Component, useCallback, useState, useEffect, useRef } = React;
const SUBChildComponent = React.memo(({getVal}) => {
console.log('child render');
return <button onClick={() => getVal()}>GetValue with debounce</button>;
});
const ChildComponent = () => {
// some unstable states
const [someVal1, setSomeVal1] = useState(0);
const [someVal2, setSomeVal2] = useState(0);
const [someVal3, setSomeVal3] = useState(0);
const someVal1Ref = useRef(someVal1);
const someVal2Ref = useRef(someVal2);
const someVal3Ref = useRef(someVal3);
useEffect(() => {
someVal1Ref.current = someVal1;
someVal2Ref.current = someVal2;
someVal3Ref.current = someVal3;
}, [someVal1, someVal2, someVal3])
// some callback witch works with states AND called from subClild components
const getVal = useCallback(_.debounce(() => {
alert(`${someVal1Ref.current}\n${someVal2Ref.current}\n${someVal3Ref.current}`);
}, 1000), []); // create debounce function only once
// some synthetic changes
useEffect(() => {
const id = setInterval(() => setSomeVal1(someVal1 + 1), 50);
return () => clearInterval(id);
}, [someVal1]);
// some synthetic changes
useEffect(() => {
const id = setInterval(() => setSomeVal2(someVal2 + 1), 100);
return () => clearInterval(id);
}, [someVal2]);
// some synthetic changes
useEffect(() => {
const id = setInterval(() => setSomeVal3(someVal3 + 1), 250);
return () => clearInterval(id);
}, [someVal3]);
return <React.Fragment><SUBChildComponent getVal={getVal}/><br/>{someVal1}<br/>{someVal2}<br/>{someVal3}
</React.Fragment>;
};
class App extends Component {
render() {
return (<div><ChildComponent/></div>);
}
}
ReactDOM.render(<App/>, document.querySelector(".container"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
<div class="container"></div>
PS. Такая реализация намного проще в компонентах класса и не требует какой-либо работы, поскольку вы не зависите от замыканий.