使用 useCallback 反应去抖动问题
React debouncing problem with useCallback
在我的 React 组件中,我有这些:
const [vendor,setVendor] = useState("");
const [taggedWith,setTaggedWith] = useState("");
function updateQuery () {
const filters = [];
if(vendor) {
filters.push({
label: `Vendor: ${vendor}`
})
}
if(taggedWith) {
filters.push({
label: `Tagged with: ${taggedWith}`
})
}
props.onUpdate(filters);
}
function debounce (func,delay){
let timer;
return function () {
clearTimeout(timer);
timer = setTimeout(()=>{
func();
},delay);
};
};
const updateQueryWithDebounce = useCallback(debounce(updateQuery,300),[]);
useEffect(()=>{
updateQueryWithDebounce();
},[taggedWith,vendor]);
去抖动有效,但问题是,由于 useCallback,updateQuery 函数内的状态变量保持不变。如果我将这些状态传递给 useCallback 的依赖数组,去抖动函数会在每次渲染时重新声明,因此,会创建带有闭包的新函数,这会导致去抖动不起作用。我该如何解决?
您可以使用一个 ref 来存储计时器句柄,这样您就可以在任一状态变量发生变化时取消之前的更新:
const [vendor,setVendor] = useState("");
const [taggedWith,setTaggedWith] = useState("");
const updateRef = useRef(0);
useEffect(() => {
updateRef.current = setTimeout(() => {
updateRef.current = 0;
const filters = [];
if (vendor) {
filters.push({
label: `Vendor: ${vendor}`
});
}
if (taggedWith) {
filters.push({
label: `Tagged with: ${taggedWith}`
});
}
props.onUpdate(filters);
}, 300);
return () => { // Cleanup callback
clearTmeout(updateRef.current);
updateRef.current = 0;
};
}, [taggedWith, vendor]);
部分代码可以完全从组件中分离出来:
const buildFilters = (taggedWith, vendor) => {
const filters = [];
if (vendor) {
filters.push({
label: `Vendor: ${vendor}`
});
}
if (taggedWith) {
filters.push({
label: `Tagged with: ${taggedWith}`
});
}
return filters;
};
然后组件的主体变成:
const [vendor,setVendor] = useState("");
const [taggedWith,setTaggedWith] = useState("");
const updateRef = useRef(0);
useEffect(() => {
updateRef.current = setTimeout(() => {
updateRef.current = 0;
props.onUpdate(buildFilters(taggedWith, vendor));
}, 300);
return () => { // Cleanup callback
clearTmeout(updateRef.current);
updateRef.current = 0;
};
}, [taggedWith, vendor]);
注意: 以上所有假设 props.onUpdate
保证是一个稳定的函数(就像 useState
中的设置器一样)。如果不是,事情就更复杂了,因为您必须将它添加到依赖项列表,然后处理 onUpdate
已更改但 taggedWith
和 vendor
没有更改的可能性。
您甚至可以将去抖动逻辑包装在钩子中(并让钩子处理不稳定的回调)。这是一个相当 off-the-cuff 的例子:
const useDebounce = (fn, ms, deps) => {
const ref = useRef(null);
if (!ref.current) {
// One-time init
ref.current = {
timer: 0,
};
}
// Always remember the most recent `fn` on our ref object
ref.current.fn = fn;
useEffect(() => {
ref.current.timer = setTimeout(() => {
ref.current.timer = 0;
// Always use the most recent `fn`, not necessarily
// the one we had when scheduling the timer
ref.current.fn.call(); // `call` so we don't pass our ref obj as `this`
}, ms);
return () => {
clearTimeout(ref.current.timer);
ref.current.timer = 0;
};
}, deps);
};
然后组件代码(使用buildFilters
)如下所示:
const [vendor, setVendor] = useState("");
const [taggedWith, setTaggedWith] = useState("");
useDebounce(
() => {
props.onUpdate(buildFilters(taggedWith, vendor));
},
300,
[taggedWith, vendor]
);
实例:
const { useState, useRef, useEffect } = React;
const useDebounce = (fn, ms, deps) => {
const ref = useRef(null);
if (!ref.current) {
// One-time init
ref.current = {
timer: 0,
};
}
// Always remember the most recent `fn` on our ref object
ref.current.fn = fn;
useEffect(() => {
ref.current.timer = setTimeout(() => {
ref.current.timer = 0;
// Always use the most recent `fn`, not necessarily
// the one we had when scheduling the timer
ref.current.fn.call(); // `call` so we don't pass our ref obj as `this`
}, ms);
return () => {
clearTimeout(ref.current.timer);
ref.current.timer = 0;
};
}, deps);
};
const buildFilters = (taggedWith, vendor) => {
const filters = [];
if (vendor) {
filters.push({
label: `Vendor: ${vendor}`
});
}
if (taggedWith) {
filters.push({
label: `Tagged with: ${taggedWith}`
});
}
return filters;
};
const Example = (props) => {
const [vendor, setVendor] = useState("");
const [taggedWith, setTaggedWith] = useState("");
useDebounce(
() => {
props.onUpdate(buildFilters(taggedWith, vendor));
},
300,
[taggedWith, vendor]
);
return <div>
<label>
<input type="text" value={vendor} onChange={
({target: {value}}) => { setVendor(value); }
} />
</label>
<label>
<input type="text" value={taggedWith} onChange={
({target: {value}}) => { setTaggedWith(value); }
} />
</label>
</div>;
};
const App = () => {
const [filters, setFilters] = useState([]);
return <div>
<Example onUpdate={setFilters} />
<div>
Filters ({filters.length}):
{!!filters.length &&
<ul>
{filters.map(filter => <li key={filter.label}>{filter.label}</li>)}
</ul>
}
</div>
</div>;
};
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
我不能说 hook 已经过彻底测试,但即使回调不稳定,它似乎也能正常工作:
const { useState, useRef, useEffect } = React;
const useDebounce = (fn, ms, deps) => {
const ref = useRef(null);
if (!ref.current) {
// One-time init
ref.current = {
timer: 0,
};
}
// Always remember the most recent `fn` on our ref object
ref.current.fn = fn;
useEffect(() => {
ref.current.timer = setTimeout(() => {
ref.current.timer = 0;
// Always use the most recent `fn`, not necessarily
// the one we had when scheduling the timer
ref.current.fn.call(); // `call` so we don't pass our ref obj as `this`
}, ms);
return () => {
clearTimeout(ref.current.timer);
ref.current.timer = 0;
};
}, deps);
};
const buildFilters = (taggedWith, vendor) => {
const filters = [];
if (vendor) {
filters.push({
label: `Vendor: ${vendor}`
});
}
if (taggedWith) {
filters.push({
label: `Tagged with: ${taggedWith}`
});
}
return filters;
};
const Example = (props) => {
const [vendor, setVendor] = useState("");
const [taggedWith, setTaggedWith] = useState("");
useDebounce(
() => {
console.log(`filter update ${vendor} ${taggedWith}`);
props.onUpdate(buildFilters(taggedWith, vendor));
},
300,
[taggedWith, vendor]
);
return <div>
<label>
<input type="text" value={vendor} onChange={
({target: {value}}) => { setVendor(value); }
} />
</label>
<label>
<input type="text" value={taggedWith} onChange={
({target: {value}}) => { setTaggedWith(value); }
} />
</label>
</div>;
};
const App = () => {
const [counter, setCounter] = useState(0);
const [filters, setFilters] = useState([]);
const ref = useRef(null);
if (!ref.current) {
ref.current = {};
}
useEffect(() => {
const timer = setInterval(() => setCounter(c => c + 1), 100);
return () => {
clearInterval(timer);
};
}, []);
// An update callback that we intentionally recreate every render
// to check that useDebounce handles using the **latest** function
const onUpdate = ref.current.onUpdate = function onUpdate(filters) {
if (onUpdate !== ref.current.onUpdate) {
console.log("STALE FUNCTION CALLED");
}
setFilters(filters);
};
return <div>
Counter: {counter}
<Example onUpdate={onUpdate} />
<div>
Filters ({filters.length}):
{!!filters.length &&
<ul>
{filters.map(filter => <li key={filter.label}>{filter.label}</li>)}
</ul>
}
</div>
</div>;
};
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
在我的 React 组件中,我有这些:
const [vendor,setVendor] = useState("");
const [taggedWith,setTaggedWith] = useState("");
function updateQuery () {
const filters = [];
if(vendor) {
filters.push({
label: `Vendor: ${vendor}`
})
}
if(taggedWith) {
filters.push({
label: `Tagged with: ${taggedWith}`
})
}
props.onUpdate(filters);
}
function debounce (func,delay){
let timer;
return function () {
clearTimeout(timer);
timer = setTimeout(()=>{
func();
},delay);
};
};
const updateQueryWithDebounce = useCallback(debounce(updateQuery,300),[]);
useEffect(()=>{
updateQueryWithDebounce();
},[taggedWith,vendor]);
去抖动有效,但问题是,由于 useCallback,updateQuery 函数内的状态变量保持不变。如果我将这些状态传递给 useCallback 的依赖数组,去抖动函数会在每次渲染时重新声明,因此,会创建带有闭包的新函数,这会导致去抖动不起作用。我该如何解决?
您可以使用一个 ref 来存储计时器句柄,这样您就可以在任一状态变量发生变化时取消之前的更新:
const [vendor,setVendor] = useState("");
const [taggedWith,setTaggedWith] = useState("");
const updateRef = useRef(0);
useEffect(() => {
updateRef.current = setTimeout(() => {
updateRef.current = 0;
const filters = [];
if (vendor) {
filters.push({
label: `Vendor: ${vendor}`
});
}
if (taggedWith) {
filters.push({
label: `Tagged with: ${taggedWith}`
});
}
props.onUpdate(filters);
}, 300);
return () => { // Cleanup callback
clearTmeout(updateRef.current);
updateRef.current = 0;
};
}, [taggedWith, vendor]);
部分代码可以完全从组件中分离出来:
const buildFilters = (taggedWith, vendor) => {
const filters = [];
if (vendor) {
filters.push({
label: `Vendor: ${vendor}`
});
}
if (taggedWith) {
filters.push({
label: `Tagged with: ${taggedWith}`
});
}
return filters;
};
然后组件的主体变成:
const [vendor,setVendor] = useState("");
const [taggedWith,setTaggedWith] = useState("");
const updateRef = useRef(0);
useEffect(() => {
updateRef.current = setTimeout(() => {
updateRef.current = 0;
props.onUpdate(buildFilters(taggedWith, vendor));
}, 300);
return () => { // Cleanup callback
clearTmeout(updateRef.current);
updateRef.current = 0;
};
}, [taggedWith, vendor]);
注意: 以上所有假设 props.onUpdate
保证是一个稳定的函数(就像 useState
中的设置器一样)。如果不是,事情就更复杂了,因为您必须将它添加到依赖项列表,然后处理 onUpdate
已更改但 taggedWith
和 vendor
没有更改的可能性。
您甚至可以将去抖动逻辑包装在钩子中(并让钩子处理不稳定的回调)。这是一个相当 off-the-cuff 的例子:
const useDebounce = (fn, ms, deps) => {
const ref = useRef(null);
if (!ref.current) {
// One-time init
ref.current = {
timer: 0,
};
}
// Always remember the most recent `fn` on our ref object
ref.current.fn = fn;
useEffect(() => {
ref.current.timer = setTimeout(() => {
ref.current.timer = 0;
// Always use the most recent `fn`, not necessarily
// the one we had when scheduling the timer
ref.current.fn.call(); // `call` so we don't pass our ref obj as `this`
}, ms);
return () => {
clearTimeout(ref.current.timer);
ref.current.timer = 0;
};
}, deps);
};
然后组件代码(使用buildFilters
)如下所示:
const [vendor, setVendor] = useState("");
const [taggedWith, setTaggedWith] = useState("");
useDebounce(
() => {
props.onUpdate(buildFilters(taggedWith, vendor));
},
300,
[taggedWith, vendor]
);
实例:
const { useState, useRef, useEffect } = React;
const useDebounce = (fn, ms, deps) => {
const ref = useRef(null);
if (!ref.current) {
// One-time init
ref.current = {
timer: 0,
};
}
// Always remember the most recent `fn` on our ref object
ref.current.fn = fn;
useEffect(() => {
ref.current.timer = setTimeout(() => {
ref.current.timer = 0;
// Always use the most recent `fn`, not necessarily
// the one we had when scheduling the timer
ref.current.fn.call(); // `call` so we don't pass our ref obj as `this`
}, ms);
return () => {
clearTimeout(ref.current.timer);
ref.current.timer = 0;
};
}, deps);
};
const buildFilters = (taggedWith, vendor) => {
const filters = [];
if (vendor) {
filters.push({
label: `Vendor: ${vendor}`
});
}
if (taggedWith) {
filters.push({
label: `Tagged with: ${taggedWith}`
});
}
return filters;
};
const Example = (props) => {
const [vendor, setVendor] = useState("");
const [taggedWith, setTaggedWith] = useState("");
useDebounce(
() => {
props.onUpdate(buildFilters(taggedWith, vendor));
},
300,
[taggedWith, vendor]
);
return <div>
<label>
<input type="text" value={vendor} onChange={
({target: {value}}) => { setVendor(value); }
} />
</label>
<label>
<input type="text" value={taggedWith} onChange={
({target: {value}}) => { setTaggedWith(value); }
} />
</label>
</div>;
};
const App = () => {
const [filters, setFilters] = useState([]);
return <div>
<Example onUpdate={setFilters} />
<div>
Filters ({filters.length}):
{!!filters.length &&
<ul>
{filters.map(filter => <li key={filter.label}>{filter.label}</li>)}
</ul>
}
</div>
</div>;
};
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
我不能说 hook 已经过彻底测试,但即使回调不稳定,它似乎也能正常工作:
const { useState, useRef, useEffect } = React;
const useDebounce = (fn, ms, deps) => {
const ref = useRef(null);
if (!ref.current) {
// One-time init
ref.current = {
timer: 0,
};
}
// Always remember the most recent `fn` on our ref object
ref.current.fn = fn;
useEffect(() => {
ref.current.timer = setTimeout(() => {
ref.current.timer = 0;
// Always use the most recent `fn`, not necessarily
// the one we had when scheduling the timer
ref.current.fn.call(); // `call` so we don't pass our ref obj as `this`
}, ms);
return () => {
clearTimeout(ref.current.timer);
ref.current.timer = 0;
};
}, deps);
};
const buildFilters = (taggedWith, vendor) => {
const filters = [];
if (vendor) {
filters.push({
label: `Vendor: ${vendor}`
});
}
if (taggedWith) {
filters.push({
label: `Tagged with: ${taggedWith}`
});
}
return filters;
};
const Example = (props) => {
const [vendor, setVendor] = useState("");
const [taggedWith, setTaggedWith] = useState("");
useDebounce(
() => {
console.log(`filter update ${vendor} ${taggedWith}`);
props.onUpdate(buildFilters(taggedWith, vendor));
},
300,
[taggedWith, vendor]
);
return <div>
<label>
<input type="text" value={vendor} onChange={
({target: {value}}) => { setVendor(value); }
} />
</label>
<label>
<input type="text" value={taggedWith} onChange={
({target: {value}}) => { setTaggedWith(value); }
} />
</label>
</div>;
};
const App = () => {
const [counter, setCounter] = useState(0);
const [filters, setFilters] = useState([]);
const ref = useRef(null);
if (!ref.current) {
ref.current = {};
}
useEffect(() => {
const timer = setInterval(() => setCounter(c => c + 1), 100);
return () => {
clearInterval(timer);
};
}, []);
// An update callback that we intentionally recreate every render
// to check that useDebounce handles using the **latest** function
const onUpdate = ref.current.onUpdate = function onUpdate(filters) {
if (onUpdate !== ref.current.onUpdate) {
console.log("STALE FUNCTION CALLED");
}
setFilters(filters);
};
return <div>
Counter: {counter}
<Example onUpdate={onUpdate} />
<div>
Filters ({filters.length}):
{!!filters.length &&
<ul>
{filters.map(filter => <li key={filter.label}>{filter.label}</li>)}
</ul>
}
</div>
</div>;
};
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>