React Hook State

2024/08/07

useState

在函数组件中存储内部 state

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

参数:

返回值:

基础用法

这个例子用来显示一个计数器。当你点击按钮,计数器的值就会增加

function Counter(props) {
  const [count, setCount] = useState(props.initialCount);

  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      count is:
      {' '}
      {count}
    </button>
  );
}
Counter.defaultProps = {
  initialCount: 10,
};

值得注意的是,类似class 组件中的setState,在我们执行setCount时count的值不是立即更新的,而是在下一个重渲染时才会更新,后调用的 setCount() 将覆盖同一周期内先调用 setCount 的值。

// 所以如在同一周期内多次执行setCount
setCount(count + 1);
setCount(count + 2);
setCount(count + 1);
// 这相当于setCount(count + 1);

因此count数仅增加一,解决办法可以参考函数式更新

更新状态中的对象和数组

当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份)

const [position, setPosition] = useState({
  x: 0,
  y: 0
});

setPosition({
  x: e.clientX,
  y: e.clientY
});

setPosition({
  ...position,
  y: e.clientY
});

使用 Immer 编写简洁的更新逻辑

由 Immer 提供的 draft 是一种特殊类型的对象,被称为 Proxy,它会记录你用它所进行的操作。这就是你能够随心所欲地直接修改对象的原因所在!从原理上说,Immer 会弄清楚 draft 对象的哪些部分被改变了,并会依照你的修改创建出一个全新的对象。

import { useImmer } from 'use-immer';

const [person, updatePerson] = useImmer({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',
  }
});

updatePerson((draft) => {
  draft.name = e.target.value;
});

函数式更新

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给State Hook 的更新函数,该回调函数将接收先前的 state,并返回一个更新后的值。

setCount(count => count + 1);
setCount(count => count + 1);
setCount(count => count + 2);
setCount(count => count + 1);
// 这相当于setCount(count + 4);

与 class 组件中的 setState 方法不同,setState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。

const [people, setPeople] = useState({
  age: 18,
  name: '小红',
});

setPeople({
  age: people.age + 1,
});

// 不会自动合并更新对象
// people {age: 18}

setPeople(prevState => {
  ..prevState,
  age: prevState.age + 1,
});

// people {age: 19, name: '小红'}

惰性初始 state

initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用(在严格模式中,React 将两次调用初始化函数)。

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

跳过 state 更新

如果您将 State Hook 更新为与当前状态相同的值,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 来比较 state)

如下面由于Object.is对比前后两次的state值未改变(引用内存中的同一个对象),所以未触发组件的更新。

function CountButton() {
  const countObj = {
    value: 18
  };
  const [count, setCount] = useState(countObj);
  return (
    <div>
      <button
        type="button"
        onClick={() => {
          countObj.value++;
          console.log(countObj);
          setCount(countObj);
          // setCount({...countObj}); 如果想要触发更新可以这样做
        }}
      >
        count is:
        {' '}
        {count.value}
      </button>
    </div>
  );
}

使用 key 重置 state 状态

当在相同的位置渲染相同的组件时,React 会保留状态。通过 key 传递给 组件,每当 key 变化时,React 将重新创建 DOM,并 重置 组件和它的所有子组件的 state

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // ✅ 当 key 变化时,该组件内的 comment 或其他 state 会自动被重置
  const [comment, setComment] = useState('');
  // ...
}

存储前一次渲染的信息

当你在渲染期间调用 set 函数时,React 将在你的组件使用 return 语句退出后立即重新渲染该组件,并在渲染子组件前进行。这样,子组件就不需要进行两次渲染。你的组件函数的其余部分仍会执行(然后结果将被丢弃)。如果你的条件判断在所有 Hook 调用的下方,可以提前添加一个 return; 以便更早地重新开始渲染。

export default function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);

  // 在渲染时调用 set 函数时,它必须位于条件语句中,例如 prevCount !== count,并且必须在该条件语句中调用 setPrevCount(count)。否则,你的组件将在循环中重新渲染
  if (prevCount !== count) {
    setPrevCount(count);
    setTrend(count > prevCount ? 'increasing' : 'decreasing');
  }
  return (
    <>
      <h1>{count}</h1>
      {trend && (
        <p>
          The count is
          {trend}
        </p>
      )}
    </>
  );
}

存储一个函数

你不能像这样把函数放入状态

const [fn, setFn] = useState(someFunction);

function handleClick() {
  setFn(someOtherFunction);
}

React 认为 someFunction 是一个 初始化函数,而 someOtherFunction 是一个 更新函数,于是它尝试调用它们并存储结果。要实际 存储 一个函数,你必须在两种情况下在它们之前加上 () =>。然后 React 将存储你传递的函数。

const [fn, setFn] = useState(() => someFunction);

function handleClick() {
  setFn(() => someOtherFunction);
}

注意事项

useReducer

对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中, 这个函数叫作 reducer。

useReducer(reducer, initialArg, init?)

参数

返回值

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

dispatch 不会在重新渲染之间变化,所以可以安全地从 useEffectuseCallback 的依赖列表中省略 dispatch

基础用法

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count:
      {' '}
      {state.count}
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  );
}

惰性初始化

你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

const [state, dispatch] = useReducer(reducer, initialArg, init);
// state = init(initialArg);

跳过 dispatch

useState类似,如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。

const initialState = { count: 1 };
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      // ❌ Object.is 判断返回state相同,所以将跳过子组件的渲染及副作用的执行
      state.count++;
      return state;
    case 'decrement':
      state.count--;
      return state;
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count:
      {' '}
      {state.count}
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  );
}

避免重新创建初始值

useState类似

// 每一次渲染的时候都会被调用
function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, createInitialState(username));
}

你可以通过给 useReducer 的第三个参数传入 初始化函数 来解决这个问题:

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, username, createInitialState);
}