一些 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)
    • 陷阱: 不要使用 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, 是否会造成循环更新(还是数据流问题)

  • useEffectshadow 比较

    在写 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, useXXHookuseMemo 只是做状态桥接的, 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 严格模式

    奇奇怪怪