useRef "桥梁"
用传统的套路来写一个todo-list,把数据放在公共的父级组件里面,只要父级的data发生改变,所有的子组件都会被执行(在不使用React.memo之类的手段进行优化时);
但是通过分析你可以发下, 数据变化的集中区域只在List组件中, 而Control中添加增加Task的功能只需要传递一个工作内容而已, 那么我们可不可以把这个回调事件保存在父级, 通过父级传给List?
// App.jsx
function App() {
const ref = useRef({})
return (
<div className="App">
<Header />
<List
handler={ref}
/>
<Control
handler={ref}
/>
</div>
);
}
// List.jsx
useLayoutEffect(() => {
handler.current.onInsert = (val) => {
console.log("insert");
if (val) {
setData((r) => r.concat(createItemData(val)));
}
return val;
};
}, []);
// Control.jsx
<button
className="button"
onClick={() => {
setToggle(true);
if (handler.current.onInsert(value)) {
setToggle(false);
setValue("");
}
}}
>
{toggle ? "确定" : "增加"}
</button>
对比
每次操作会更新所有组件 branch-main
每次操作只更新相关的组件 branch-todo-list-ref
貌似contextAPI也不做到这点 playground
useRef 和 useMemo
共同点
- 都可以用来缓存数据
- 都不会引发页面的更新
不同点
-
改变value的条件不同, useRef可以随时改变 而 useMemo 必须要通过deps改变而且deps必须是响应式数据(eg: useState数据)
也就是说useMemo只能React来调用 -
由于useMemo 是由于
state改变而引起的, 容易造成错觉, 以为useMemo改变了, 页面就会更新 -
useRef 除了能够保存数据之外, 还能拿到dom元素
// let count = 0
let [count, setCount] = useState(0)
const data = useMemo(() => {
console.log(count)
return `c==>${count}`
}, [count])
useEffect(() => {
let timer = setInterval(() => {
setCount(c => c + 1)
// count++
}, 1000)
return () => {
clearInterval(timer)
}
}, [])
关于re-render
此时也不会引起子组件的render, 因为Header是App的children
// App.jsx
<div className="App">
<Test>
<Header />
{/* <List
handler={ref}
/>
<Control
handler={ref}
/> */}
</Test>
</div>
// Test.jsx
function Test({ children }) {
const [c, setC] = useState(0)
useEffect(() => {
setInterval(() => {
setC(x => x + 1)
}, 1000)
}, [])
useEffect(() => {
console.log('update c', c, Test.name)
}, [c])
return children
}
小结:
- 闭包的本质就是局部数据共享
- 组件和闭包的本质一致
- 当组件之间需要共享数据时, 就应该在他们共有的父级组件中寻找突破口
- useRef可以充当这个桥梁, useMemo 适合"计算属性"