React useState使用完整教程(译)

useState是一个Hook函数,让你在函数组件中拥有state变量。它接收一个初始化的state,返回是一个数组,数组里有两个元素,第一个元素是当前状态值和另一个更新该值的方法。
本教程主要是针对于React中的useState做一个详细的描述,它等同于函数组件中的this.state/this.setState,我们将会围绕下面的问题逐一解析:

  • React中的类组件和函数组件
  • React.useState hook做了什么
  • 在React中声明状态
  • React Hooks: Update State
  • 在useState hook中使用对象作为状态变量
  • React Hooks中如何更新嵌套对象状态
  • 多个状态变量还是一个状态对象
  • 使用useState的规则
  • useReducer Hook的使用

如果您是刚开始学习使用useState,请查看官方文档或者该视频教程

React中的类组件和函数组件

React有两种类型的组件:类组件和函数组件。
类组件是继承React.Component的ES6类,它有自己的状态和生命周期函数:

class Message extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      message: ""
    };
  }

  componentDidMount() {
    /*...*/
  }

  render() {
    return <div>{this.state.message}</div>
  }
}

函数组件是一个函数,它能接收任何组件的属性作为参数,并且可以返回有效的JSX。

function Message(props) {
  return <div>{props.message} </div>
}
//或者使用箭头函数
const Message = (props) => <div>{props.message}</div>

正如所看到的,函数组件没有任何状态和生命周期方法。不过,在React16.8,我们可以使用Hooks。
React Hooks是方法,它可以给函数组件添加状态变量,并且可以模拟类组件的生命周期方法。他们倾向以use作为Hook名的开始。

React.useState hook做了什么

正如之前了解的,useState可以给函数组件添加状态,函数组件中的useState可以生成一系列与其组件相关联的状态。

类组件中的状态总是一个对象,不过Hooks中的状态可以是任意类型。每个state可以有单一的值,也可以是一个对象、数组、布尔值或者能想到的任意类型。

So,你会什么时候用useStateHook呢?它对组件自身的状态很有用,然而大项目可能会需要另外的状态管理方案。

React声明状态

useStateReact的命名输出出,因此你可以这么写:

  React.useState

或者可以直接这么写:

import React, { useState } from "react";

然而不像在类组件里声明状态对象那样,useState允许声明多个状态变量:

import React from "react";

class Message extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      message: "",
      list: "",
    }
  }
  /*...*/
}

useStateHook一次只能声明一个状态变量,不过这个状态变量可以是任意类型的:

import React, { useState } from "react";

const Message = () => {
  const messageState = useState("");
  const listState = useState([]);
}

useState接收状态的初始值作为一个参数。
正如之前例子展示的,可以直接给函数传递,也可以使用函数来延迟初始化该变量(当初始化状态基于一次昂贵的计算,这种方式是很有用的):

const Message = () => {
  const messageState = useState(() => expensiveComputation());
  /*...*/
}

初始化的值仅仅会在第一次渲染时被赋值(如果他是一个函数,也是会在初次渲染时执行)。
在后续的更新中(由于组件本身的状态更改或者是说父组件导致的变化),useStateHook参数(初始值)将会被忽略,当前的值将会被使用。
理解它是非常重要的,举个例子,如果你想更新基于组件接收的新属性的状态:

const Message = (props) => {
  const messageState = useState(props.message);
}

只单独使用useState不会工作的,因为它的参数仅仅在第一次生效,并不是每次属性更改时生效(可以结合useEffect使用,具体查看该回答
不过,useState不是像之前所说的仅仅返回一个变量。
它返回的是一个数组,第一个元素是状态变量,第二个元素是更新该变量值的方法。

const Message= () => {
  const messageState = useState("");
  const message = messageState[0]; // 是一个空字符串
  const setMessage = messageState[1]; // 是一个方法
}

一般我们会选择数组解构的方式来简化上述代码:

const Message = () => {
  const [message, setMessage] = useState("");
}

在函数组件中可以像其他变量一样使用状态变量:

const Message = () => {
  const [message, setMessage] = useState("");
  return <p>{message}</p>;
}

但是为什么useState会返回一个数组呢?
因为与对象相比、数组是非常灵活且容易使用。
如果这个方法返回的是一个包括一系列属性集的对象,那么就不能很容易自定义变量名,比如:

// 没有使用对象解构
const messageState = useState( '' );
const message = messageState.state;
const setMessage = messageState;

//使用对象解构
const { state: message, setState: setMessage } = useState( '' );
const { state: list, setState: setList } = useState( [] );

React Hooks: 更新状态

useState返回的第二个元素是一个方法,它用新值来更新状态变量。
举个🌰,使用输入框在每次改变时更新状态变量示例

const Message = () => {
  const [message, setMessage] = useState( '' );

  return (
    <div>
      <input
         type="text"
         value={message}
         placeholder="Enter a message"
         onChange={e => setMessage(e.target.value)}
       />
      <p>
        <strong>{message}</strong>
      </p>
    </div>
  );
};

然而,这个更新函数不会立即更新值。相反它会排队等待更新操作。在重新渲染组件后,useState的参数将被忽略,这个更新方法将会返回最新的值。
如果你需要用之前的值来更新状态,你一定得传递一个接收之前值的方法来返回新值示例

const Message = () => {
  const [message, setMessage] = useState("");

  return (
    <div>
      <input
        type="text"
        value={message}
        placeholder="Enter a message"
        onChange={(e) => {
          const val = e.target.value;
          setMessage((prev) => prev + val);
        }}
      />
      <p>
        <strong>{message}</strong>
      </p>
    </div>
  );
};

useState hook中使用对象作为状态变量

当使用对象时,需要记住的是:

  1. 不可变的重要性
  2. useState返回的更新方法不是像类组件中的setState合并对象

关于第一点,如果你是用相同的值作为当前值来更新state(React使用的Object.is来做比较),React不会触发更新的。

当使用对象时,很容易出现下面的错误示例,输入框不能输入文本:

const MessageOne = () => {
  const [messageObj, setMessageObj] = useState({ message: "" });

  return (
    <div>
      <input
        type="text"
        value={messageObj.message}
        placeholder="Enter a message"
        onChange={(e) => {
          messageObj.message = e.target.value;
          setMessageObj(messageObj);
        }}
      />
      <p>
        <strong>{messageObj.message}</strong>
      </p>
    </div>
  );
};

上面的例子没有创建一个新对象,而是改变已经存在的状态对象。对于React来说,它们是同一个对象。为了正常运行,我们创建一个新对象示例

onChange={(e) => {
  const newMessageObj = { message: e.target.value };
  setMessageObj(newMessageObj);
}}

这个让我们看到了你需要记住的第二件事情。
当你创建一个状态变量时,类组件中的this.setState 自动合并更新对象,而函数组件中useState的更新方法则是直接替换对象 。
继续上面的例子,如果我们给message对象添加一个id属性,将会发生什么呢示例

const MessageThree = () => {
  const [messageObj, setMessageObj] = useState({ message: "", id: 1 });

  return (
    <div>
      <input
        type="text"
        value={messageObj.message}
        placeholder="Enter a message"
        onChange={(e) => {
          const newMessageObj = { message: e.target.value };
          setMessageObj(newMessageObj);
        }}
      />
      <p>
        <strong>
          {messageObj.id}: {messageObj.message}
        </strong>
      </p>
    </div>
  );
};

当只更新message属性时,React将替换原先的状态值{ message: '', id: 1 },当触发onChange属性时,状态值将仅仅包含message属性:

{message: "····"} // id属性丢失了

当然,通过替换的对象和扩展运算之前的对象结合作为参数也可以在函数组件中复制setState()的行为示例

onChange={(e) => {
  const val = e.target.value;
  setMessageObj((prevState) => {
    return { ...prevState, message: val };
  });
}}

...prevState会得到对象所有的属性,message: val会重新赋值给message属性。

当然使用Object.assign也会得到相同的结果(需要创建新对象)示例

//使用Object.assign
setMessageObj((prevState) => {
  return Object.assign({}, prevState, { message: val });
});          

不过扩展运算可以简化这个操作,而且也可以应用到数组上。
一般来讲,当在数组上使用时,扩展运算移除了括号,你可以用旧数组中的值创建另一个数组:

[
  ...['a', 'b', 'c'],
  'd'
]
// Is equivalent to
[ 'a', 'b', 'c','d']

再来个🌰,如何用数组来使用useState示例
需要注意的是,处理多维数组时需要谨慎的使用扩展运算,因为可能最终的结果不是你所期待的。
所以这个时候,我们就需要考虑使用对象作为状态。

React Hooks中如何更新嵌套对象状态

JS中,多维数组是数组里嵌套数组:

[
  ['value1','value2'],
  ['value3','value4']
]

你可以在用它们来把你所有的状态变量集中在一个地方,然而,为了这个目的,最好使用内嵌对象:

{
  'row1' : {
    'key1' : 'value1',
    'key2' : 'value2'
  },
  'row2' : {
    'key3' : 'value3',
    'key4' : 'value4'
  }
}

当使用内嵌对象和多维数组时,有一个问题是Object.assign和扩展运算是创建了一个浅拷贝并非是深拷贝。

当拷贝数组时,扩展运算仅仅做了一层的拷贝,因此,对于多维数组来说,使用它是不合适的,就像下面例子中所示(使用Object.assign()和扩展元算结果都是true):

let a = [[1], [2], [3]];
let b = [...a];
b.shift().shift(); // 1
//此时数组a的结果是[[], [2], [3]]

StackOverflow关于上面的例子提供了一个比较好的解释,不过目前重要的是,当使用内嵌对象时,我们不能用扩展运算来更新状态对象。

再举个🌰

const [messageObj, setMessageObj] = useState({
  author: "",
  message: {
    id: 1,
    text: "",
  }
});

来,先看看更新text字段的错误方式示例

//错误, 文本更改后,messageObj的值是{author: "", message: {id: 1, text: ""}, text: "*"}
setMessageObj((prevState) => ({
  ...prevState,
  text: val,
}));

//错误,文本更改后messageObj值是{id: "*", text: "*"}
setMessageObj((prevState) => ({
  ...prevState.message,
  text: val
}));

//错误,文本更改后,messageObj的值是{author: "", message: {text: "*"}},缺少id属性
setMessageObj((prevState) => ({
  ...prevState,
  message: {
    text: val,
  }
}));

为了能正确的更新text属性,我们需要拷贝一个原始对象,这个新对象包括整个原始对象的所有属性:

setMessageObj((prevState) => ({
  ...prevState,  //赋值第一层的key值
   message: {  //创建包含更新key值的对象
    ...prevState.message, //复制包含key值的对象
    text: val,  //给需要更新的字段重新赋值
  }
}));

以同样的方式,我们也可以更新author字段:

setMessageObj((prevState) => ({
  author: "Joe",
  message: { ...prevState.message },
}));

如果message对象变化了,则用以下的方式:

setMessageObj((prevState) => ({
  author: "Joe",
  message: { 
    ...prevState.message, 
    text: val, 
  },
}));

声明多个状态变量还是一个状态对象

当你的应用中使用多个字段或值作为状态变量时,你可以选择组织多个变量:

const [id, setId] = useState(-1);
const [message, setMessage] = useState('');
const [author, setAuthor] = useState('');

或者使用一个对象状态变量:

const [messageObj, setMessage] = useState({ 
  id: 1, 
  message: '', 
  author: '' 
});

不过,需要谨慎的使用复杂数据结构(内嵌对象)的状态对象,考虑下这个🌰 :

const [messageObj, setMessage] = useState({
  input: {
    author: {
      id: -1,
      author: {
        fName:'',
        lName: ''
      }
    },
    message: {
      id: -1,
      text: '',
      date: new Date(),
    }
  }
});

如果你需要更新嵌套在对象深处的指定字段时,你必须复制所有其他对象和包含该指定字段的的对象的键值对一起复制:

setMessage(prevState => ({
  input: {
    ...prevState.input,
    message: {
      ...prevState.input.message, 
      text: '***',
    }
  }
}));

在某些情况下,拷贝深度内嵌对象是比较昂贵的,因为React可能会依赖那些没有改变过的字段值重新渲染你应用的部分内容。
对于这个原因,首先要做的是尝试扁平化你的对象。需要关注的是,React官方推荐根据哪些值倾向于一起变化,将状态分割成多个状态变量
如果这个不可能的话,推荐使用第三方库来帮助你使用不可变对象,例如immutable.js或者 immer

useState使用规则

useState和所有的Hooks一样,都遵循同样的规则:

  • 在顶层调用Hooks
  • 在React函数中调用Hooks

第二个规则很容易理解,不要在类组件中使用useState

或者在常规JS方法中(不能在一个方法组件中被调用的):


如果项目中有ESLint的话, 则可以看到对应错误提示;如果没有,则可以从该文档中看到出现的错误提示

第一个规则指的是:即使在函数组件内部,不能在循环、条件或者内嵌方法中调用useState,因为React依赖于useState函数被调用的顺序来获取特定变量的正确值。
在这方面,常见的错误是,在if语句使用useState(它们不是每次都被执行的):

if (condition) { // 有时它会执行,导致useState的调用顺序发生变化
  const [message, setMessage] = useState( '' );
  setMessage( aMessage );  
}
const [list, setList] = useState( [] );
setList( [1, 2, 3] );

函数组件中会多次调用useState或者其他的Hooks。每个Hook是存储在链表中的,而且有一个变量来追踪当前执行的Hook。
useState被执行时,当前Hook的状态被读取(第一次渲染期间是被初始化),之后,变量被改变为指向下一个Hook。
这也就是为什么总是始终保持Hook相同的调用顺序是很重要的。否则的话,状态值就会属于另一个状态变量。

总体来说,这儿有一个例子来一步步说明它是如何工作的:

  1. React初始化Hook链表,并且用一个变量追踪当前Hook
  2. React首次调用你的组件
  3. React发现了useState的调用,创建了一个新的Hook对象(带有初始状态),将当前Hook变量指向该Hook对象,将该对象添加到Hooks链表中,然后返回一个带有初始值和更新状态方法的数组
  4. React发现另一个useState的调用,重复上面的步骤,存储新的hook对象,改变当前Hook变量。
  5. 组件状态发生变化
  6. React给要处理的队列发送新的状态更新操作(执行useState返回的方法)
  7. React决定组件是否需要重新渲染
  8. React重置当前Hook变量,并且调用组件
  9. React发现了一个useState的调用,但此时,在Hooks链表第一位置已经有一个Hook,它仅仅改变了当前Hook变量,返回一个带状态值和更新状态方法的数组
  10. React发现另一个useState的调用,因为第二个位置有一个Hook了,它再次仅仅改变了当前Hook变量,返回一个带状态值和更新状态方法的数组
    这是一个简单的useState工作流程,具体可以看ReactFiberHooks源码

useReducerHook的使用

对于很多高级使用情况,我们可以使用useReducerHook来代替useState。当处理复杂的状态逻辑时,它是很有用的。比如包含多个子值或者状态依赖之前的值。
Look,看下如何使用useReducerHook,示例:

const [state, dispatch] = useReducer(reducer, initialArgument, init);
//useReducer返回一个带有当前状态值和dispatch方法的数组。如果你使用过Redux,这个就得心应手了。

useState,你调用更新状态的方法,然而useReducer中,你调用dispatch方法,然后传递给它一个action,eg: 至少是带有一个type属性的对象。

dispatch({type:"increase"})

一般来讲,一个action对象也会有其他的属性,eg: {action: "increase", payload: "10"}
然而传递一个action对象并不是绝对的,具体参考Redux

总结

uesState是一个Hook函数,它让你可以在组件中拥有状态变量,你可以给这个方法传递初始值,并且返回一个当前状态值的变量(不一定是初始值)和另一个更新该值的方法。

这儿有一些需要记住的关键的点:

  • 更新方法不会立即更新值
  • 如果你需要用到之前的值更新状态,你必须将之前的值传递给该方法,则它会返回一个更新后的值,eg: setMessage(previousVal => previousVal + currentVal)
  • 如果你使用同样的值作为当前状态,则React不会触发重新渲染。(React是用Object.is来做比较的)
  • useState不像类组件中的this.setState合并对象,它是直接替换对象。
  • useState和所有的hooks遵循相同的规则,特别是,注意这些函数的调用顺序。(可以借助 ESLint plugin,它会帮助我们强制实施这些规则)

相关文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容