React 开发者关于 State 的必知必会 📣

前言

状态(State)是 React 中最为重要的概念之一,可以说是 React 的核心 🏆。React 之所以被称为 React,是因为"React reacts to state changes by re-rendering the UI"。在 React 中,所有的事情都围绕着状态展开,因此每个 React 开发者都必须熟练掌握状态的概念和使用。本文通过回答 5 个问题来详细介绍关于状态的内容,以对 React 中的状态有更深入的理解:

  • What is State?
  • What type of state we need?
  • When to use state?
  • Where to place state?
  • How to manage state?

1、What is State?

当一个组件需要保存自己的数据,并且希望在整个生命周期中一直保留这些数据时,React 提供了一种解决方案,那就是使用状态(State)。状态是组件中用于存储和管理数据的机制。它可以包含简单的内容,例如输入框的文本,也可以是复杂的数据结构,比如一个完整的表单。React 是一种声明式的编程模型,我们不直接操作 DOM,而是通过操作状态来更新视图,当我们更新组件的状态时,React 会重新渲染该组件,从而触发视图的更新。状态在 React 中起到了保持用户界面与数据同步的重要作用,用户界面的变化和发展正是由状态随着时间的推移而变化得到的。总结而言,状态让我们能够完成两件重要的事情:

  1. 状态允许我们在组件中保存和管理数据,使组件在不同的时间点保持一致的数据状态;
  2. 通过更新状态,我们可以触发组件的重新渲染,从而实现视图的更新;

2、What type of state we need?

👉 根据状态可访问性可以分为两种,1️⃣ 局部状态 Local State 和 2️⃣ 全局状态 Global State:

  • 局部状态,由某个组件创建且只有该组件可以访问,但是可以通过 props 向子组件传递;
  • 全局状态,整个应用程序中所有组件都能访问,会被各种处于不同层级的组件所使用;

不要过早考虑全局状态,始终从本地状态开始,只有当真正需要时才使用它!

👉 从状态域来说状态可以分为两种类型,1️⃣ UI 状态 UI State 和 2️⃣ 远程状态 Remote State:

  • 远程状态,它是存在于服务器上的状态,我们从远程服务器中请求得到,这是异步的,数据也可能经常需要重新获取和更新;
  • UI 状态,除远程状态之外的都是 UI 状态,用于展现视图的内容,其通常是同步的并且存储在应用中,不与服务器进行交互;

3、When to use state?

在组件中,并不是每个数据都需要作为状态。事实上,我们应该谨慎判断何时使用状态,以避免不必要的重新渲染和潜在的性能问题:

  1. 如果数据在某个时刻不会发生改变,那么我们可以将其作为"常规变量"存储,例如使用 const 来声明数据;
  2. 如果我们的数据可以通过已有的状态或属性进行计算或加工,那么我们可以将其视为"派生状态",避免重复存储相似的数据;
  3. 如果我们的数据发生改变时,并不需要触发组件的重新渲染来更新视图,那么我们可以使用"Ref(useRef)";
  4. 当上述条件均不满足时,我们就需要使用状态了 🏆

派生状态

JSX 复制代码
const [cart, setCart] = useState([
  { name: 'bag', price: 10 },
  { name: 'shirt', price: 20 },
]);

// 派生状态
const numItems = cart.length;
const totalPrice = cart.reduce((acc, cur) => acc + cur.price, 0);

所谓的派生状态(Derive State)指的是通过对已有的状态(State)或属性(Props)进行计算而得到的值。当一些值可以直接从状态或属性中计算而来时,就没有必要创建额外状态,只需使用常规变量来存储即可。派生状态和原始状态会保持同步,当状态发生改变时,组件会重新渲染,并重新调用相关函数,派生状态将会重新计算得到。通过这种方式,可以减少状态的数量,从而减少了一些不必要的重新渲染。这种方式能够使代码更加简洁、可读性更强,并提高了应用程序的可维护性。

useRef

JSX 复制代码
const myRef = useRef(0);

// 设置 ref
myRef.current = 1;
// 读取 ref
console.log(my.current);

在 React 中,我们可以使用 useRef 钩子来创建一个称为 Ref 的对象。那么,什么是 Ref 呢?Ref 提供了一个名为 .current 的可变属性,用于存储和读取数据。从本质上讲,它像是一个盒子,我们可以将任何需要在渲染期间保留的数据放入其中。与普通变量不同,Ref 在渲染时不会被重置,而是保留其之前的值。然而,需要注意的是,当 Ref 的值发生变化时,并不会导致组件重新渲染。因此,Ref 通常只在事件处理程序或副作用(effect)中使用。如果需要写在 JSX 中作为视图所对应的数据,那你应该使用状态,而不是 Ref(当然,除非你需要获得 DOM 元素)。以下列举两个 Ref 非常常见的使用场景:

  1. 创建渲染之间保持不变的变量,例如 previous state、setTimeout id 等;
  2. 选择和存储 DOM 元素(最常使用!)
JSX 复制代码
// Ref 选择和存储 DOM 元素示例
function RefDemo() {
  const inputEl = useRef<null | HTMLInputElement>(null);
  return <input ref={inputRef} />;
}

4、Where to place state?

当确定需要使用状态来存储数据时,我们需要思考下一个问题:"状态应该存储在哪里?":

  1. 当状态数据仅在当前组件内使用时,可以将状态存储在当前组件中,这样的状态仅在当前组件的生命周期内可见和可用;
  2. 当状态数据不仅在当前组件使用,还需要传递给子组件时,可以通过 Props 传递给子组件,这样子组件可以访问和使用父组件的状态数据;
  3. 当组件及其少数几个相邻组件需要共享状态数据时,可以将状态提升至它们的最近共同父组件中,并通过 Props 向下传递给子组件;
  4. 当上述条件不适用时(例如,数据需要被位于不同层级的组件使用),我们可以考虑使用全局状态或浏览器存储。

具体来讲,我们主要有 6 种选择来存储状态:

  1. Local state
  2. Lifting up state
  3. Global state (preferably UI state)
  4. Global state (remote or UI state)
  5. Global state (passing between pages)
  6. Storing data in user's browser

状态提升

假设组件 A 包含了组件 B 和组件 C 两个子组件,并且存在一项状态数据,这个状态数据需要被组件 B 和组件 C 同时使用。在 React 中,数据流是单向的,只能从父组件向子组件进行传递,而不能在同级组件之间进行传递。这时候,我们需要使用"状态提升🚀"的技术,将这个状态放置在离组件 B 和组件 C 最近的共同父组件上,并通过 Props 向下传递,直至需要使用这个状态的组件。通常情况下,我们不建议在状态提升的过程中超过 3 个层级。

此外,我们知道 Props 是只读的,子组件无法直接修改父组件传递过来的 Props。那么,如果子组件需要更改这个 Props 应该怎么处理呢?解决方法是将更新状态的方法同步传递给子组件,子组件可以通过调用这个方法来请求更新状态。这种数据流方式也被称为逆向数据流(Child-to-parent communication / inverse data flow)。

5、How to manage state?

在"2、What type of state we need?"中我们介绍了状态可以分为局部/全局状态和远程/UI状态,根据状态分类,如上图所示,不同的状态有着不同的状态管理方案,以下简单介绍最常使用的几种类型,第三方库内容不再展开,查看官方文档即可。

useState

JSX 复制代码
import { useState } from 'react';

// Creating state
const [state, setState] = useState(0);
const [state, setState] = useState(() => {
  return localStorage.getItem('state');
});

// Updating state
setState(1);
setState((c) => c + 1);

使用 State 最常用也是最简单的方法是通过 useState hook,其主要包含两部分内容:

  1. 创建 State:如上示例,我们可以直接设置值创建 State,也可以基于回调函数创建 State,回调函数必须是纯函数且不允许有入参,这个回调函数只会在初始化渲染的时候被调用,后续的重新渲染不会被调用,当初始值依赖于某些计算时我们就可以使用回调函数的方法;
  2. 更新 State:如上示例,我们可以直接在 useState 返回的 setter 函数中传递一个值作为下一个 State 值,而当我们需要根据当前 State 值来更新State 值时,我们需要给 setter 函数传递一个回调函数,回调函数的第一个入参就是当前 State 值,返回值为下一个 State 值;

注意,当 State 为数组或对象时,不要直接更新这个数组或对象,而是创建一个新的数组或对象来代替当前值,不然不会发生变更!!!

JSX 复制代码
function Demo() {
  const [state, setState] = useState([0]);
  const [state2, setState2] = useState({ a: 1 });
  
  const handleClick = () => {
    // 以下都是无效变更!
    setState((c) => {
      c.push(2);
      return c;
    });
    setState2((c) => {
      c.a = 2;
      return c;
    });
  };
  
  return (
    <div>
      <div> {JSON.stringify(state)}-{JSON.stringify(state2)} </div>
      <button onClick={handleClick}>click</button>
    </div>
  );
}

useReducer

一般情况下使用 useState 足够管理状态,但随着组件和状态更新变得越来越复杂,使用 useState 来管理所有状态是不够的,这个时候使用 useReducer 不失为一种选择,例如以下场景:

  1. 当组件具有大量的状态变量和状态更新,并且这些状态变量和状态更新分散在组件的许多事件处理程序中时;
  2. 当需要同时进行多个状态更新,且是作为对同一事件的反应时;
  3. 当更新一个状态依赖于一个或多个其他状态时;
JSX 复制代码
// 最重要!!!reducer 函数包含了更新状态的所有逻辑,且不含任何副作用。它实现了状态逻辑与组件的解耦。reducer 函数的参数 action 描述了如何更新状态,通常包含一个动作类型和一个有效载荷(一般为输入数据)
function reducer(state, action) {
  switch(action.type) {
    case 'dec':
      return state - 1;
    case 'inc':
      return state + 1;
    case 'updateDay':
      return action.payload;
    default: 
      throw new Error('Unknown');
  }
}
const initalState = 0;
// state 存储了一组相关联的状态,initState 是初始值
const [state, dispatch] = useReducer(reducer, initalState);

// dispatch 用于触发状态更新,将一个操作从事件处理程序分派给 reducer 来更新状态。
dispatch({ type: 'dec' });
dispatch({ type: 'inc' });
dispatch({ type: 'updateDay', payload: 10 });

当需要更新组件的状态时,我们可以通过调用从 useReducer 钩子函数中获取的 dispatch 函数来实现。dispatch 函数会发送一个 action 给 reducer,这个 action 包含了指示 reducer 如何更新状态的信息。在 reducer 函数中,它会将当前状态和接收到的 action 结合起来,生成一个全新的状态。当状态更新时,组件实例会触发重新渲染,从而更新视图。整个过程可以类比于数组的 reduce 方法,采用了一种累积的思想。在每次调用 reducer 函数时,它会根据当前的状态和 action 来生成新的状态,而不是直接修改原有的状态。通过使用 useReducer,我们可以更好地管理和更新组件的状态。它提供了一种可预测和一致的状态更新方式,将状态逻辑与组件的其余部分解耦,使代码更易于维护和理解。

useReducer 简单来说是一组 useState,那么应该如何选择使用 useState 还是 useReducer 呢?

  1. 如果我们只需要一个状态,那毋庸置疑只需要使用 useState;
  2. 如果有一组状态经常需要一起更新,那可以考虑使用 useReducer 一起管理这一组状态;
  3. 如果超过三四个状态是相关联的,并且存在 Objects,也可以考虑使用 useReducer;
  4. 如果我们觉得太多事件处理程序使得组件大而复杂,可以考虑使用 useReducer;
  5. 否则,我们就使用 useState 即可,注意,useState 应该始终作为管理状态的首先,不要过早考虑 useReducer!

Context API

useReducer 用于处理一组状态更新的场景,而 Context API 则用于解决状态的另一个问题,"PROP DRILLING"。当我们需要将状态传递到嵌套较深的子组件中时,通过 Props 会一层一层往下传递,同时可能中间组件根本不需要使用这个状态,但为了传递状态依旧需要加入这个 Props,这个对状态管理来说无疑增加了非常大的成本。

JSX 复制代码
/**
 * 创建 Context
 */
import { createContext, useState, useContext } from "react";

const MyContext = createContext(null);

// Provider 是特殊的响应组件,允许所有子组件来访问 context 中的值
export function ContextProvider({ children }: { children: React.ReactNode }) {
  const [name, setName] = useState("sherwin");
  // value 是我们需要提供进行广播的状态数据
  return <MyContext.Provider value={{ name, setName }}>{children}</MyContext.Provider>;
}
export function useMyContext() {
  const context = useContext(MyContext);
  if (context === undefined) throw new Error("Context was used outside the Provider");
  return context;
}
JSX 复制代码
/**
 * 使用 Context
 */
import { ContextProvider, useMyContext } from 'XXX';

function ChildrenComponent() {
  // 通过 useContext 来消费所提供的上下文值
  const { name } = useMyContext();
  return <div>{name}</div>;
}

export default function ExampleComponent() {
  return (
    <ContextProvider>
      <ChildrenComponent></ChildrenComponent>
    </ContextProvider>
  );
}

Context API 是一种可以在整个应用程序中传递数据的解决方案,它允许组件树中的各个组件读取状态,而无需逐层传递数据。实质上,Context API 将全局状态广播到使用上下文的所有组件。需要注意的是,这个全局状态只对使用同一个上下文的组件可见,对于上下文外层的组件来说是无法获取的。使用 Context API,我们可以避免通过 Props 层层传递数据的繁琐过程。相反,我们可以将需要共享的数据放入上下文中,然后在需要访问该数据的组件中直接读取。这样可以简化组件之间的通信,提高代码的可读性和可维护性。另外,当上下文的值发生改变时,所有订阅该上下文的组件都会触发重新渲染。这意味着当共享的上下文值发生变化时,相关的组件会相应地更新其视图以反映最新的数据。

URL

URL 是一个极好的存储状态(尤其是 UI 状态)的位置,比如表格筛选信息、展示模块信息等内容。在开发中,结合 React-Router 库,URL 成为了一个不可忽视的状态管理方案。通过将状态放置在 URL 上,我们可以将其变为全局状态,使应用中的所有组件都能轻松访问到。同时,这也是一种将页面数据传递到另一个页面的方法。此外,当我们将页面添加为书签或分享时,可以保留当时的状态,以便打开页面时恢复到相应的状态。通过将状态编码到 URL 中,我们可以实现可持久化的状态管理。用户可以通过直接修改 URL 或通过页面导航来改变应用的状态。这种方式使得用户可以轻松地分享特定状态的页面,并且在打开链接时能够保留原始的状态。使用 URL 作为状态的存储位置有助于简化组件间的通信和数据传递,同时提供了一种可靠的方法来保存和恢复应用的状态。React-Router 库提供了强大的路由功能,可以帮助我们管理和解析 URL 中的状态信息,并将其传递给相应的组件。

总结

状态对于 React 的重要性不言而喻,本文从什么是状态、状态的类型、什么时候以及什么地方去使用状态、如何使用和管理状态等各方面详细介绍了状态相关的内容,构建 React 应用主要就是围绕状态来进行思考,总而言之,React 开发者的视角不再是基于传统的 DOM 元素,而是思考状态随着时间的转换而发生的变化:

  • Step1: 将所需构建的内容分解成组件并形成组件树
  • Step2: 构建一个不包含状态的静态版本页面/组件
  • Step3: 开始思考状态 👉 什么时候去使用状态?使用什么类型的 State?State 应该放置在哪里?用什么方式管理状态?......
  • Step4: 建立状态数据流 👉 One-way data flow、Child-to-parent communication 或 Accessing global state 等。

参考资料:the Ultimate React Course

相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef4 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6415 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云5 小时前
npm淘宝镜像
前端·npm·node.js