Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

简单易懂的 React useState() Hook 指南(长文建议收藏) #147

Open
husky-dot opened this issue Nov 18, 2019 · 0 comments
Open

Comments

@husky-dot
Copy link
Owner

作者:Dmitri Pavlutin

译者:前端小智

来源:dmitripavlutin.com


阿里云最近在做活动,低至2折,真心觉得很划算了,可以点击本条内容或者链接进行参与
https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=pxuujn3r

腾讯云最近在做活动,百款云产品低至 1 折,可以点击本条内容或者链接进行参与


状态是隐藏在组件中的信息,组件可以在父组件不知道的情况下修改其状态。我更偏爱函数组件,因为它们足够简单,要使函数组件具有状态管理,可以useState() Hook。

本文会逐步讲解如何使用useState() Hook。此外,还会介绍一些常见useState() 坑。

1.使用 useState() 进行状态管理

无状态的函数组件没有状态,如下所示(部分代码):

import React from 'react';

function Bulbs() {
  return <div className="bulb-off" />;
}

可以找 codesandbox 尝试一下。

运行效果:

这时,要如何添加一个按钮来打开/关闭灯泡呢? 为此,咱们需要具有状态的函数组件,也就是有状态函数组件。

useState()是实现灯泡开关状态的 Hoook,将状态添加到函数组件需要4个步骤:启用状态、初始化、读取和更新。

1.1 启用状态

要将<Bulbs> 转换为有状态组件,需要告诉 React:从'react'包中导入useState钩子,然后在组件函数的顶部调用useState()

大致如下所示:

import React, { useState } from 'react';

function Bulbs() {
  ... = useState(...);
  return <div className="bulb-off" />;
}

Bulbs函数的第一行调用useState()(暂时不要考Hook的参数和返回值)。 重要的是,在组件内部调用 Hook 会使该函数成为有状态的函数组件。

启用状态后,下一步是初始化它。

1.2初始化状态

始时,灯泡关闭,对应到状态应使用false初始化 Hook:

import React, { useState } from 'react';

function Bulbs() {
  ... = useState(false);
  return <div className="bulb-off" />;
}

useState(false)false初始化状态。

启用和初始化状态之后,如何读取它?来看看useState(false)返回什么。

1.3 读取状态

当 hook useState(initialState)被调用时,它返回一个数组,该数组的第一项是状态值

const stateArray = useState(false);
stateArray[0]; // => 状态值

咱们读取组件的状态

function Bulbs() {
  const stateArray = useState(false);
  return <div className={stateArray[0] ? 'bulb-on' : 'bulb-off'} />;
}

<Bulbs>组件状态初始化为false,可以打开 codesandbox 看看效果。

useState(false)返回一个数组,第一项包含状态值,该值当前为false(因为状态已用false初始化)。

咱们可以使用数组解构来将状态值提取到变量on上:

import React, { useState } from 'react';

function Bulbs() {
  const [on] = useState(false);
  return <div className={on ? 'bulb-on' : 'bulb-off'} />;
}

on状态变量保存状态值。

状态已经启用并初始化,现在可以读取它了。但是如何更新呢?再来看看useState(initialState)返回什么。

####1.4 更新状态

用值更新状态

咱们已经知道,useState(initialState)返回一个数组,其中第一项是状态值,第二项是一个更新状态的函数。

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

// 将状态更改为 'newState' 并触发重新渲染
setState(newState);

// 重新渲染`state`后的值为`newState`

要更新组件的状态,请使用新状态调用更新器函数setState(newState)。组件重新渲染后,状态接收新值newState

当点击开灯按钮时将灯泡开关状态更新为true,点击关灯时更新为 false

import React, { useState } from 'react';

function Bulbs() {
  const [on, setOn] = useState(false);

  const lightOn = () => setOn(true);
  const lightOff = () => setOn(false);

  return (
    <>
      <div className={on ? 'bulb-on' : 'bulb-off'} />
      <button onClick={lightOn}>开灯</button>
      <button onClick={lightOff}>关灯</button>
    </>
  );
}

打开 codesandbox 自行尝试一下。

单击开灯按钮时,lightOn()函数将on更新为true: setOn(true)。单击关灯时也会发生相同的情况,只是状态更新为false

状态一旦改变,React 就会重新渲染组件,on变量获取新的状态值。

状态更新作为对提供一些新信息的事件的响应。这些事件包括按钮单击、HTTP 请求完成等,确保在事件回调或其他 Hook 回调中调用状态更新函数。

使用回调更新状态

当使用前一个状态计算新状态时,可以使用回调更新该状态:

const [state, setState] = useState(initialState);
...
setState(prevState => nextState);

...

下面是一些事例:

// Toggle a boolean
const [toggled, setToggled] = useState(false);
setToggled(toggled => !toggled);

// Increase a counter
const [count, setCount] = useState(0);
setCount(count => count + 1);

// Add an item to array
const [items, setItems] = useState([]);
setItems(items => [...items, 'New Item']);

接着,通过这种方式重新实现上面电灯的示例:

import React, { useState } from 'react';

function Bulbs() {
  const [on, setOn] = useState(false);

  const lightSwitch = () => setOn(on => !on);

  return (
    <>
      <div className={on ? 'bulb-on' : 'bulb-off'} />
      <button onClick={lightSwitch}>开灯/关灯</button>
    </>
  );
}

打开 codesandbox 自行尝试一下。

setOn(on => !on)使用函数更新状态。

1.5 小结一波

  • 调用useState() Hook 来启用函数组件中的状态。

  • useState(initialValue)的第一个参数initialValue是状态的初始值。

  • [state, setState] = useState(initialValue)返回一个包含2个元素的数组:状态值和状态更新函数。

  • 使用新值调用状态更新器函数setState(newState)更新状态。或者,可以使用一个回调setState(prev => next)来调用状态更新器,该回调将返回基于先前状态的新状态。

  • 调用状态更新器后,React 确保重新渲染组件,以使新状态变为当前状态。

2. 多种状态

通过多次调用useState(),一个函数组件可以拥有多个状态。

function MyComponent() {
  const [state1, setState1] = useState(initial1);
  const [state2, setState2] = useState(initial2);
  const [state3, setState3] = useState(initial3);
  // ...
}

需要注意的,要确保对useState()的多次调用在渲染之间始终保持相同的顺序(后面会讲)。

我们添加一个按钮添加灯泡,并添加一个新状态来保存灯泡数量,单击该按钮时,将添加一个新灯泡。

新的状态count 包含灯泡的数量,初始值为1

import React, { useState } from 'react';

function Bulbs() {
  const [on, setOn] = useState(false);
  const [count, setCount] = useState(1);

  const lightSwitch = () => setOn(on => !on);
  const addBulbs = () => setCount(count => count + 1);

  const bulb = <div className={on ? 'bulb-on' : 'bulb-off'} />;
  const bulbs = Array(count).fill(bulb);

  return (
    <>
      <div className="bulbs">{bulbs}</div>
      <button onClick={lightSwitch}>开/关</button>
      <button onClick={addBulbs}>添加灯泡</button>
    </>
  );
}

打开演示,然后单击添加灯泡按钮:灯泡数量增加,单击开/关按钮可打开/关闭灯泡。

  • [on, setOn] = useState(false) 管理开/关状态
  • [count, setCount] = useState(1)管理灯泡数量。

多个状态可以在一个组件中正确工作。

3.状态的延迟初始化

每当 React 重新渲染组件时,都会执行useState(initialState)。 如果初始状态是原始值(数字,布尔值等),则不会有性能问题。

当初始状态需要昂贵的性能方面的操作时,可以通过为useState(computeInitialState)提供一个函数来使用状态的延迟初始化,如下所示:

function MyComponent({ bigJsonData }) {
  const [value, setValue] = useState(function getInitialState() {
    const object = JSON.parse(bigJsonData); // expensive operation
    return object.initialValue;
  });

  // ...
}

getInitialState()仅在初始渲染时执行一次,以获得初始状态。在以后的组件渲染中,不会再调用getInitialState(),从而跳过昂贵的操作。

4. useState() 中的坑

现在咱们基本已经初步掌握了如何使用useState(),尽管如此,咱们必须注意在使用useState()时可能遇到的常见问题。

4.1 在哪里调用 useState()

在使用useState() Hook 时,必须遵循 Hook 的规则

  1. 仅顶层调用 Hook :不能在循环,条件,嵌套函数等中调用useState()。在多个useState()调用中,渲染之间的调用顺序必须相同。

  2. 仅从React 函数调用 Hook:必须仅在函数组件或自定义钩子内部调用useState()

来看看useState()的正确用法和错误用法的例子。

有效调用useState()

useState()在函数组件的顶层被正确调用

function Bulbs() {
  // Good
  const [on, setOn] = useState(false);
  // ...
}

以相同的顺序正确地调用多个useState()调用:

function Bulbs() {
  // Good
  const [on, setOn] = useState(false);
  const [count, setCount] = useState(1);
  // ...

useState()在自定义钩子的顶层被正确调用

function toggleHook(initial) {
  // Good
  const [on, setOn] = useState(initial);
  return [on, () => setOn(!on)];
}

function Bulbs() {
  const [on, toggle] = toggleHook(false);
  // ...
}

useState() 的无效调用

在条件中调用useState()是不正确的:

function Switch({ isSwitchEnabled }) {
  if (isSwitchEnabled) {
    // Bad
    const [on, setOn] = useState(false);
  }
  // ...
}

在嵌套函数中调用useState()也是不对的

function Switch() {
  let on = false;
  let setOn = () => {};

  function enableSwitch() {
    // Bad
    [on, setOn] = useState(false);
  }

  return (
    <button onClick={enableSwitch}>
      Enable light switch state
    </button>
  );
}

4.2 过时状态

闭包是一个从外部作用域捕获变量的函数。

闭包(例如事件处理程序,回调)可能会从函数组件作用域中捕获状态变量。 由于状态变量在渲染之间变化,因此闭包应捕获具有最新状态值的变量。否则,如果闭包捕获了过时的状态值,则可能会遇到过时的状态问题。

来看看一个过时的状态是如何表现出来的。组件<DelayedCount>延迟3秒计数按钮点击的次数。

function DelayedCount() {
  const [count, setCount] = useState(0);

  const handleClickAsync = () => {
    setTimeout(function delay() {
      setCount(count + 1);
    }, 3000);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
    </div>
  );
}

打开演示,快速多次点击按钮。count 变量不能正确记录实际点击次数,有些点击被吃掉。

delay() 是一个过时的闭包,它从初始渲染(使用0初始化时)中捕获了过时的count变量。

为了解决这个问题,使用函数方法来更新count状态:

function DelayedCount() {
  const [count, setCount] = useState(0);

  const handleClickAsync = () => {
    setTimeout(function delay() {
      setCount(count => count + 1);
    }, 3000);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
    </div>
  );
}

现在etCount(count => count + 1)delay()中正确更新计数状态。React 确保将最新状态值作为参数提供给更新状态函数,过时闭包的问题解决了。

打开演示,快速单击按钮。 延迟过去后,count 能正确表示点击次数。

4.3 复杂状态管理

useState()用于管理简单状态。对于复杂的状态管理,可以使用useReducer() hook。它为需要多个状态操作的状态提供了更好的支持。

假设需要编写一个最喜欢的电影列表。用户可以添加电影,也可以删除已有的电影,实现方式大致如下:

import React, { useState } from 'react';

function FavoriteMovies() {
  const [movies, setMovies] = useState([{ name: 'Heat' }]);

  const add = movie => setMovies([...movies, movie]);

  const remove = index => {
    setMovies([
      ...movies.slice(0, index),
      ...movies.slice(index + 1)
    ]);
  }

  return (
    // Use add(movie) and remove(index)...
  );
}

尝试演示:添加和删除自己喜欢的电影。

状态列表需要几个操作:添加和删除电影,状态管理细节使组件混乱。

更好的解决方案是将复杂的状态管理提取到reducer中:

import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, action.item];
    case 'remove':
      return [
        ...state.slice(0, action.index),
        ...state.slice(action.index + 1)
      ];
    default:
      throw new Error();
  }
}

function FavoriteMovies() {
  const [state, dispatch] = useReducer(reducer, [{ name: 'Heat' }]);

  return (
    // Use dispatch({ type: 'add', item: movie })
    // and dispatch({ type: 'remove', index })...
  );
}

reducer管理电影的状态,有两种操作类型:

  • "add"将新电影插入列表

  • "remove"从列表中按索引删除电影

尝试演示并注意组件功能没有改变。但是这个版本的<FavoriteMovies>更容易理解,因为状态管理已经被提取到reducer中。

还有一个好处:可以将reducer 提取到一个单独的模块中,并在其他组件中重用它。另外,即使没有组件,也可以对reducer 进行单元测试。

这就是关注点分离的威力:组件渲染UI并响应事件,而reducer 执行状态操作。

4.4 状态 vs 引用

考虑这样一个场景:咱们想要计算组件渲染的次数。

一种简单的实现方法是初始化countRender状态,并在每次渲染时更新它(使用useEffect() hook)

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

function CountMyRenders() {
  const [countRender, setCountRender] = useState(0);
  
  useEffect(function afterRender() {
    setCountRender(countRender => countRender + 1);
  });

  return (
    <div>I've rendered {countRender} times</div>
  );
}

useEffect()在每次渲染后调用afterRender()回调。但是一旦countRender状态更新,组件就会重新渲染。这将触发另一个状态更新和另一个重新渲染,依此类推。

可变引用useRef()保存可变数据,这些数据在更改时不会触发重新渲染,使用可变的引用改造一下<CountMyRenders>

import React, { useRef, useEffect } from 'react';

function CountMyRenders() {
  const countRenderRef = useRef(1);
  
  useEffect(function afterRender() {
    countRenderRef.current++;
  });

  return (
    <div>I've rendered {countRenderRef.current} times</div>
  );
}

打开演示并单击几次按钮来触发重新渲染。

每次渲染组件时,countRenderRef可变引用的值都会使countRenderRef.current ++递增。 重要的是,更改不会触发组件重新渲染。

5. 总结

要使函数组件有状态,请在组件的函数体中调用useState()

useState(initialState)的第一个参数是初始状态。返回的数组有两项:当前状态和状态更新函数。

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

使用 setState(newState)来更新状态值。 另外,如果需要根据先前的状态更新状态,可以使用回调函数setState(prevState => newState)

在单个组件中可以有多个状态:调用多次useState()

当初始状态开销很大时,延迟初始化很方便。使用计算初始状态的回调调用useState(computeInitialState),并且此回调仅在初始渲染时执行一次。

必须确保使用useState()遵循 Hook 规则。

当闭包捕获过时的状态变量时,就会出现过时状态的问题。可以通过使用一个回调来更新状态来解决这个问题,这个回调会根据先前的状态来计算新的状态。

最后,您将使用useState()来管理一个简单的状态。为了处理更复杂的状态,一个更好的的选择是使用useReducer() hook。


原文:https://dmitripavlutin.com/react-usestate-hook-guide/

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug


交流(欢迎加入群,群工作日都会发红包,互动讨论技术)

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。

clipboard.png

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant