React-hook

Hook规则

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。

为什么要保证顺序?
因为React通过Hook调用的顺序来确定state和useState的对应关系

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}
// ------------
// 首次渲染
// ------------
useState('Mary')           // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm)     // 2. 添加 effect 以保存 form 操作
useState('Poppins')        // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle)     // 4. 添加 effect 以更新标题

// -------------
// 二次渲染
// -------------
useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm)     // 2. 替换保存 form 的 effect
useState('Poppins')        // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle)     // 4. 替换更新标题的 effect

// ...

只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。

  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

在第一次渲染中 name !== ‘’ 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:

useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm)  // 🔴 此 Hook 被忽略!
useState('Poppins')        // 🔴 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle)     // 🔴 3 (之前为 4)。替换更新标题的 effect 失败

React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。

只在 React 函数中调用 Hook

1.不要在普通的 JavaScript 函数中调用 Hook
2.在 React 的函数组件中调用 Hook
3.在自定义 Hook 中调用其他 Hook

一、useState

作用:返回一个 state,以及更新 state 的函数。setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。

使用:在渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。

const [state, setState] = useState(initialState);

setState(newState);

注意:
1.如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
2.initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。
3.如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state。

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

4.如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。


如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。
下面的计数器组件示例展示了 setState 的两种用法:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

useState 目前的一种实践,是将变量名打平,而非像 Class Component 一样写在一个 State 对象里:

class ClassComponent extends React.PureComponent {
  state = {
    left: 0,
    top: 0,
    width: 100,
    height: 100
  };
}
function FunctionComponent {
  const [left,setLeft] = useState(0)
  const [top,setTop] = useState(0)
  const [width,setWidth] = useState(100)
  const [height,setHeight] = useState(100)
}

实际上在 Function Component 中也可以聚合管理 State:

function FunctionComponent() {
  const [state, setState] = useState({
    left: 0,
    top: 0,
    width: 100,
    height: 100
  });
}

只是更新的时候,不再会自动 merge,而需要使用 …state 语法:

setState(state => ({ ...state, left: e.pageX, top: e.pageY }));

性能注意事项
useState 函数的参数虽然是初始值,但由于整个函数都是 Render,因此每次初始化都会被调用,如果初始值计算非常消耗时间,建议使用函数传入,这样只会执行一次:

function FunctionComponent(props) {
  const [rows, setRows] = useState(() => createRows(props.count));
}

二、useEffect

作用:该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

副作用:在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作。

执行时机:effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候才执行。


1.每次 Render的时候,都有自己的 Props 与 State

每次Render的时候,里面的变量都是全新的(所以用const定义state变量是无伤大雅的,因为根本就不需要在同一次render中改变state)。

其实不仅是对象,函数在每次渲染时也是独立的。这就是 Capture Value 特性,后面遇到这种情况就不会一一展开,只描述为 “此处拥有 Capture Value 特性”。

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
2.每次 Render 都有自己的事件处理

解释了为什么下面的代码会输出 5 而不是 3:

const App = () => {
  const [temp, setTemp] = React.useState(5);

  const log = () => {
    setTimeout(() => {
      console.log("3 秒前 temp = 5,现在 temp =", temp);
    }, 3000);
  };

  return (
    <div
      onClick={() => {
        log();
        setTemp(3);
        // 3 秒前 temp = 5,现在 temp = 5
      }}
    >
      xyz
    </div>
  );
};

在 log 函数执行的那个 Render 过程里,temp 的值可以看作常量 5,执行 setTemp(3) 时会交由一个全新的 Render 渲染,所以不会执行 log 函数。而 3 秒后执行的内容是由 temp 为 5 的那个 Render 发出的,所以结果自然为5
原因就是 temp、log 都拥有 Capture Value 特性。

3.如何绕过 Capture Value

利用 useRef 就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。

function Example() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  useEffect(() => {
    // Set the mutable latest value
    latestCount.current = count;
    setTimeout(() => {
      // Read the mutable latest value
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });
  // ...
}

也可以简洁的认为,ref 是 Mutable(可变的) 的,而 state 是 Immutable(不可变的) 的。

4.回收机制

在组件被销毁时,通过 useEffect 注册的监听需要被销毁,这一点可以通过 useEffect 的返回值做到:

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
  };
});
5.用同步取代 “生命周期”

Function Component 不存在生命周期,所以不要把 Class Component 的生命周期概念搬过来试图对号入座。Function Component 仅描述 UI 状态,React 会将其同步到 DOM,仅此而已。

既然是状态同步,那么每次渲染的状态都会固化下来,这包括 state props useEffect 以及写在 Function Component 中的所有函数。

然而舍弃了生命周期的同步会带来一些性能问题,所以我们需要告诉 React 如何比对 Effect。

6.告诉 React 如何对比 Effects

告诉 React 如何对比 Effects
虽然 React 在 DOM 渲染时会 diff 内容,只对改变部分进行修改,而不是整体替换,但却做不到对 Effect 的增量修改识别。因此需要开发者通过 useEffect 的第二个参数告诉 React 用到了哪些外部变量:

import React, { useState, useEffect } from 'react'

function Lianxi() {
    const [namea, setNamea] = useState(1);
    const [nameb, setNameb]=useState('lishuang')

    function ona(){
      setNamea(namea+1)
    }

    useEffect(() => {
      console.log('fsgh')
    }, [nameb]); // Wrong: name is missing in dep

    function onb(){
      setNameb(nameb+'1')
    }
    return (
        <div>
            <button onClick={ona}>{namea}</button>
            <button onClick={onb}>{nameb}</button>
        </div>
    )
}

export default Lianxi

直到 nameb 改变时的 Render,useEffect 才会再次执行,namea改变时的Render,useEffect不执行

7.不要对 Dependencies 撒谎
const [count, setCount] = useState(0);

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

<button onClick={()=>{setCount(count+1)}}>{count}</button>

这样每一次点击button,count都是从0开始的,因为每一次render的时候setInterval还没执行

setInterval 我们只想执行一次,所以我们自以为聪明的向 React 撒了谎,将依赖写成 []。

“组件初始化执行一次 setInterval,销毁时执行一次 clearInterval,这样的代码符合预期。” 你心里可能这么想。

但是你错了,由于 useEffect 符合 Capture Value 的特性,拿到的 count 值永远是初始化的 0。相当于 setInterval 永远在 count 为 0 的 Scope 中执行,你后续的 setCount 操作并不会产生任何作用。

8.诚实的代价

笔者稍稍修改了一下标题,因为诚实是要付出代价的:

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

你老实告诉 React “嘿,等 count 变化后再执行吧”,那么你会得到一个好消息和两个坏消息。

好消息是,代码可以正常运行了,拿到了最新的 count。

坏消息有:

计时器不准了,因为每次 count 变化时都会销毁并重新计时。
频繁 生成/销毁 定时器带来了一定性能负担。

9.怎么既诚实又高效呢?

上述例子使用了 count,然而这样的代码很别扭,因为你在一个只想执行一次的 Effect 里依赖了外部变量。

既然要诚实,那只好 想办法不依赖外部变量:

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

setCount 还有一种函数回调模式,你不需要关心当前值是什么,只要对 “旧的值” 进行修改即可。这样虽然代码永远运行在第一次 Render 中,但总是可以访问到最新的 state。

三、useCallback

返回一个缓存的函数

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

四、useMemo

返回一个缓存的变量
作用:有助于避免在每次渲染时都进行高开销的计算。(相当于vue的计算属性,把变量给缓存起来了)

执行时机:把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。如果传入一个空数组[]会只执行一次,如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

注意:传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

用 useMemo 做局部 PureRender

const Child = (props) => {
  useEffect(() => {
    props.fetchData()
  }, [props.fetchData])

  return useMemo(() => (
    // ...
  ), [props.fetchData])
}

可以看到,我们利用 useMemo 包裹渲染代码,这样即便函数 Child 因为 props 的变化重新执行了,只要渲染函数用到的 props.fetchData 没有变,就不会重新渲染。
这里发现了 useMemo 的第一个好处:更细粒度的优化渲染。所谓更细粒度的优化渲染,是指函数 Child 整体可能用到了 A、B 两个 props,而渲染仅用到了 B,使用 useMemo的话,A改变不会导致重新渲染。

五、useRef

作用:useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。(可以简单理解为:通过 useRef 创建的对象,其值只有一份,而且在所有 Rerender 之间共享。与useState的区别在于 useState创建的对象,在每个Rerender内都是独立的而ueseRef是共享的)

function Counter() {
  const count = useRef(0);

  const log = () => {
    count.current++;
    setTimeout(() => {
      console.log(count.current);
    }, 3000);
  };

  return (
    <div>
      <p>You clicked {count.current} times</p>
      <button onClick={log}>Click me</button>
    </div>
  );
}

六.useReducer

先介绍一下 useReducer 的用法:

const [state, dispatch] = useReducer(reducer, initialState);

useReducer 返回的结构与 useState 很像,只是数组第二项是 dispatch,而接收的参数也有两个,初始值放在第二位,第一位就是 reducer。

reducer 定义了如何对数据进行变换,比如一个简单的 reducer 如下:

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + 1
      };
    default:
      return state;
  }
}

这样就可以通过调用 dispatch({ type: ‘increment’ }) 的方式实现 count 自增了。

七.useImmer

作用:当数据嵌套的太深或者太复杂的时候可以用useImmer

const [state, setState] = useImmer({
    people: [
      {
        name: '马云',
        englishName: 'Jack Ma'
      },
      {
        name: '马化腾',
        englishName: 'Pony Ma'
      },
      {
        name: '李彦宏',
        englishName: 'Robin Li'
      }
    ]
})

setState(state => {state.people[2].name = 'Robin Lee'})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值