一些 React 的 Tips
基础
修改数组类型的 state
💩拷贝-修改-回填
function addItem(item){ const items = this.state.items.slice(); items.push(item); // push 会修改原数组所以必须复制 this.setState({ items }) }
👍️合并-回填
function addItem(item){ this.setState({ items: this.state.items.concat(item) }) // concat 并不会修改原数组 }
state
&props
异步的💩直接使用
state
&props
做更新this.setState({ counter: this.state.counter + this.props.increment, });
👍️使用函数更新
this.setState((state, props) => ({ counter: state.counter + props.increment }));
useState
初始值何时采用函数作为入参当初始值需要被计算时
const [count, setCount] = useState(props.reduce((pre, cur) => pre + cur)); // 💩 在每次渲染组件时, reduce 都会被计算一遍结果然后被丢弃 const [count, setCount] = useState(() => props.reduce((pre, cur) => pre + cur) ); // 👍️ 在每次渲染组件时, 都会声明一个匿名函数, 但是函数不会执行, 不会计算 reduce
初始值是很复杂对象时
const [count, setCount] = useState({k1, k2, k3, ..., k1000}); // 💩 在每次渲染组件时, 都要花大力气一个很大的对象, 然后丢弃 const [count, setCount] = useState(() => ({k1, k2, k3, ..., k1000})); // 👍️ 函数的声明耗时与函数内部的代码量不相关
useState
&useRef
区别:
useState
的值在每个render
中都是独立存在的, 而useRef.current
则更像是相对于render函数的一个全局变量, 每次他会保持render
的最新状态. (useRef
相当于创建了类组件的成员变量)场景
- 变量维护 UI 状态, 更新时需要刷新 UI:
useState
- 变量不维护 UI 状态, 更新时不需要刷新 UI:
useRef
- 变量不更新:
const [foo] = useState(initValue)
- 变量维护 UI 状态, 更新时需要刷新 UI:
陷阱: 不要使用
ref
非幂等(增量)的更新ref
. 在 StrictMode 的 Dev 环境下, 组件会被渲染两次, 以检查组件的幂等性, 并且不提供 Warning...ref1.current = ref1.current + 1 // 💩 会执行两次相当于 + 2 ref1.current = someState + 1 // 👍️ state 连续渲染两次值并不会变
不要吝啬
setState
与 Vue 不同, React 并不会在数据被 Set 后同步刷新数据, 而是会合并更新, 所以同步的多个
setState
不会造成性能问题不要在组件中过度使用
useEffect
将这层逻辑抽离成自己的
Hook
, 方便复用, 方便阅读. 否则光读useEffect
都读不完,useEffect
也可以让代码更加工整, 可读性也更高强调数据流向, 单一事实来源
- 尽量让响应式数据的数据来源变简单
- 没有了自动依赖手机, 要理清楚状态发生变化的原因, 数据变化会引发哪些状态变化
- 强调数据的提供与消费, 我向外暴露什么样的数据, 什么组件会消费我的数据
在
setState
的时候注意有没有 Effect 依赖于这个State
, 是否会造成循环更新(还是数据流问题)useEffect
的shadow
比较在写
useEffect(()=>{}, [])
时第二个参数是一个数组, 当数组内元素变化时会触发重新执行, 但是这里检测变化的方法是对数组中元素进行 shadow 比较, 并不是像 Vue 一样进行依赖追踪, 例如
每次渲染时传入的function Demo(vals = []){ useEffect(()=>{ console.log(vals) }, [vals]) }
vals
都是新vals
, react 在对比时就会发现vals
不同, 触发rerender
可以通过如下方法修复这个问题
useEffect(()=>{}, vals); // 每次对比的都是 vals 数组内部的值 useEffect(()=>{}, [...vals]);
千万注意 react 只是对数组进行 shadow 比较, 不是对我们传入的变量做依赖追踪
Hook 的闭包陷阱
只要是要在 Hook 中使用外部的东西就要注意陷阱, 例如
function Demo(){ function consoleStore(){ console.log(1) } useEffect(()=>{ consoleStore() }, []) }
跑起来没毛病, 但是如果后期将代码改成
function Demo(){ const [store, setStore] = useState(1) function consoleStore(){ console.log(store) } useEffect(()=>{ consoleStore() }, []) }
就完蛋了, 因为 useEffect 拿到的永远是最开始定义的
consoleStore
, 其中的store
也永远是第一次渲染的store
. 为了防止出这种问题, 一定要装这个包 eslint-plugin-react-hooks减小
useEffect
的体积俺理解 React 所谓的函数式并不是严格的函数式, 而是形式上的函数式. 如果我写一个组件, 里面没有
useEffect
, 只要事实来源不变, 整个 View 就不变, 事实来源变了, View 可以符合预期的变化. 当然, 不可能所有的组件都是纯函数的, 我们可以用一些 Hooks 在 Hooks 内部加入Effect
代码, 但是向外部表现为一个自动化的事实来源, 当状态发生变化时对 View 做一定程度的精修. 在外层组件看来, 代码并没有副作用, 外层组件也可以忘记 Hooks 内部存在副作用, 像着纯函数一样使用. 例如这样的函数式组件就看起来很舒服function Demo({display, disabled}){ // 事实来源是 props // working 是一个额外的属性, 但是他不是事实来源, 只是算出来的数据 const working = useMemo(()=>display && !disabled, [display, disabled]) // useLocalStorge 是一个 Hook, 内部通过 useEffect 获取数据, 同步修改 // 但是作为视图层, 我完全可以忽略这个问题, 从我的角度看这就是一个 Storgae 的事实来源, 从外部看这就是一个 state, 就是一个数据来源. 函数副作用这些脏活, 已经被 Hook 实现时候帮忙处理掉了 const [storage, setStorage] = useLocalStorage(['cookie', 'tocken']) return <> {...} </> }
所以, 俺觉得, 好的代码的事实来源一定是
props
,useXXHook
而useMemo
只是做状态桥接的,useState
只是帮忙存储数据的, 并不是发生变化的事实来源增加一个
useEffect
依赖的时候要想: 这个依赖会不会在每次 render 的时候变化造成循环依赖, 这个依赖变化的时候函数内部产生的内容应不应该变化useState
可以传引用类型如果
useState
传入一个引用类型, 在rerender
时候引用值并不会变, 例如下面的代码会一直输出false
import { useEffect, useState } from "react"; let demos = []; // 存储每次 rerender 时候的函数 export default function App() { const [demo, setDemo] = useState(() => () => 1); // 传入一个函数 const [foo, setFoo] = useState(1); // 传入一个常量用于触发更新 useEffect(() => { // 每秒钟 rerender 一下 setTimeout(() => setFoo((pre) => pre + 1), 1000); }, [foo]); demos.push(demo); // 将每次 rerender 的函数放在 demos 中 console.log(demos.some((d) => d !== demos.at(0))); // 检查函数引用是否发生变化 return ( <div className="App"> <h1>{demo()}</h1> <p>{foo}</p> </div> ); }
useMemo
的使用场景当成 Vue 的
computed
用, 能用useMemo
就不要用useState
, 减少可变数据, 减少数据源, 尽可能减少事实来源useCallback
的使用场景function Demo(){ const foo = ()=>{} const bar = useCallback(()=>{}, [deps]) }
对于
foo
, 每次 rerender 时候foo
的引用值都会发生变化.对于
bar
, 每次 rerender 事后, 只要deps
不发生变化,bar
的引用值不变.那么
useCallback
有什么用呢? 可以节约函数定义? 因为只要deps
不变, 返回的函数不变. 看起来我们没有定义新的函数, 但是useCallback
并不会节约函数定义的开销, 因为在调用useCallback
之前, 我们已经声明了这个函数为什么用
useCallback
而不是直接定义, 或者使用useState
useState
+useEffect(()=>{}, [deps])
当然可以, 但是useCallback
显然简洁直接定义并不会影响当前组件的调用, 但是如果该函数作为
props
传给了其他组件, 其他组件使用了memo()
, 只要props
不变组件就不 rerender, 但是你传给组件一个函数, 这父组件每次 rerender 就会导致函数引用变化, 就会导致子组件一直rerender
, 这时useCallback
就变得很有用, 所以给向外提供函数的时候尽量
useCallback
一下StrickMode
严格模式奇奇怪怪