状态管理是React中的一个基本概念,这一篇文章我们就来了解下组件状态是如何存储和更新的,这些知识对于创建复杂应用极为重要,我们需要透彻理解。 
 
 
理解状态钩子(Understanding the State Hook) 
我们已学习了State的用法,这里我们需要了解三点:
 
一,React更新 State是异步的。
比如我们在设置State语句后立即查看State,会发现并没有马上更新的。如下面的代码,点击按钮在控制台输出"false",可见并没有立即更新为 true 。这是为了避免频繁重新渲染页面,在事件处理函数中,可能还有类似setName('kelemi')等其他语句,一般是等事件处理函数结束后,才一并重新渲染。
function App() {
  const  [isVisible, setVisible] = useState(false);
  const handleClick = () => {
    setVisible(true);
    console.log(isVisible);
  };
  return (
    <div>
      <button onClick={handleClick}>Show</button>
    </div>
  );
}
export default App;二,React的State实际上是存于组件外部的。
因为定义在组件里的变量只作用于该组件函数里,当重新渲染时,里面的变量将被重置,所以必须存在于外部,否则没法工作。当屏幕上长久不显示该组件时,存在于组件外部的state变量将被自动清除。
三,在组件的顶部使用State Hook。
如下,我们再定义了一个State Hook,内部命名为isApprove,而React实际是不管这个名字的,它记录的类似列表,它只知道有两个boolean值,分别为 false,true。在重新新渲染时,它根据顺序映射到组件内部的名称中,所以不能将State放在 if 语句、循环语句以及嵌套语句中,这样会破坏顺序。我们在使用中要将State定义在组件函数的开头处。
function App() {
  const  [isVisible, setVisible] = useState(false);
  const  [isApprove, setApprove] = useState(true);
  ...
}选择State结构 
首先要避免冗余,看下面的State Hook,我们定义了firstName和lastName,就没必要再定义一个名为fullName的State Hook,因为我们完全可以由firstName和lastName组成fullName。
function App() {
  const  [firstName, setFirstName] = useState("");
  const  [lastName, setLastName] = useState("");
  const fullName = firstName + " " + lastName;
  return <div>{fullName}</div>;
}其次,我们可以将相关联的State组合在一起,比如我们可以将firstName和lastName组合起来形成person。 
function App() {
  const  [person, setPerson] = useState({
    firstName: "",
    lastName: "",
  });
  const  [isLoading, setLoading] = useState(false);
  ...
}另外,我们不能让State Hook的结构层次太深,多层嵌套。比如下面这样就不好。尽量保持扁平结构。 
...
  const  [person,setPerson] = useState({
    firstName:'',
    lastName:'',
    contact:{
      address:{
        street:'',
      }
    }
  })
  ...小结下State结构的最佳实践如下。 
BEST PRACTICES
Avoid redundant state variables.
Group related variables inside an object.
Avoid deeply nested structures. 保持组件纯度 
什么是纯度?它是计算机科学的一个基本概念。纯的函数是指给它同样的输入,总是输出一样的结果。如果同样的输入在不同的时间点输出是不一样的,那该函数就是不纯的。
 
React围绕着这个概念进行,当提供的输入props是一样时,期望输出也是一样的,也就可以不用重新渲染。
那如何保持组件的纯度呢?
将更改放在渲染阶段之外! 
 
我们来看一下这是什么意思?
 
我们有Message组件,代码Message.tsx:
let count = 0;
const Message = () => {
  return <div>Message {count}</div>;
};
export default Message;然后在App.tsx中使用3个Message组件: 
import Message from "./Message";
function App() {
  return (
    <div>
      <Message />
      <Message />
      <Message />
    </div>
  );
}
export default App;输出的三个组件是一致的,说明组件是纯的,如下: 
# 访问:http://localhost:5173 但要是在 Message.tsx里的渲染代码中添加修改代码"count++",如下: 
let count = 0;
const Message = () => {
  count++;
  return <div>Message {count}</div>;
  ...
输出的各个Message就不一样了,这样组件就不纯了。
# 访问:http://localhost:5173 所以在组件内,不要修改已存在的对象,这样才能保持组件是纯的。另外注意,如果创建和更新对象均放在组件内部实现,这是不影响的。比如我们将count的初始化也放在Message组件里就可以。 
const Message = () => {
  let count = 0;
  count++;
  return <div>Message {count}</div>;
};理解Strict模式 
前面演示组件的示例中,你是否注意到不纯的显示是Message 2、Message 4 以及Message 6,而不是期望的1、2、3,你知道为什么吗?
 
这跟React的strict模式有关。main.tsx中,App组件包含在 React.StrictMode中,这是React中内置的一个组件,不显示具体内容,而用于发现一些潜在的问题,其中之一就是检查组件是否是纯的。
ReactDOM. createRoot ( document . getElementById("root") as HTMLElement) . render(
<React .StrictMode><!--注意看 StrictMode -->
<App />
«/React. StrictMode>
);在开发模式下,StrictMode执行2次,第1次用于检查,第2次用于实际渲染,所以我们看到的是2、4、6。 
为了更清楚看到这个过程,我们在APP组件只留一个Message组件,同时修改Message.tsx,通过在控制台打印出相关信息: 
let count = 0;
const Message = () => {
  console.log("Message called", count);
  count++;
  return <div>Message {count}</div>;
...查看控制台,看到执行了2次,第2行灰色的表示是strict模式输出。 
另外,需要说明的是,在React 18下,默认Strict模式是开启的,也就是执行二次,主要用于发现组件是否不纯等问题,比如我们希望是1,结果输出是2,我们就能知道该组件不纯。而且,只在开发环境中strict模式才生效,当我们部署在生产环境时,Strict是关闭的,也就是只执行一次。
 
更新对象 
前面我们说过,关联的State可以组成对象,我们在App组件添加名为drink的State。对于State,我们要把它看成是不变的或只读的,不要尝试改变它,这样是不会正确工作的。代码里,我们在击点处理事件里,修改了drink,但在页面上我们点击按钮时,不会有任何变化,说明这是不工作的。
function App() {
  const  [drink, setDrink] = useState({
    title: "Americano",
    price: 5,
  });
  const handleClick = () => {
    drink.price = 6;
    setDrink(drink);
  };
  return (
    <div>
      {drink.price}
      <button onClick={handleClick}>Click Me</button>
    </div>
  );
}我们在handleClick中需要定义一个新对象newDrink,然后再调用setDrink,如下: 
const handleClick = () => {
    const newDrink = {
      title: drink.title,
      price: 6,
    };
    setDrink(newDrink);
  };当State对象有很多属性时,我们可以使用对象复制(3个省略号)来处理。 
const handleClick = () => {
    setDrink({ ...drink, price: 6 });
  };更新嵌套对象 
我们看一个稍复杂的State Hook,customer有两个属性: name和address,其中address也是对象且有两属性city和zipCode。click事件的工作就是更改zipCode。我们的代码是先复制整个customer,然后需要再复制customer.address。如果不复制customer.address的话,新建的customer对象与原来的customer指向了同一个address,这样是不行的,setCustomer的新建customer不要与原有的customer有任何关联。
function App() {
  const  [customer, setCustomer] = useState({
    name: "kelemi",
    address: {
      city: "San Francisco",
      zipCode: 94111,
    },
  });
  const handleClick = () => {
    setCustomer({
      ...customer,
      address: { ...customer.address, zipCode: 94112 },
    });
  };
...我们看到,嵌套的StateHook更改会比较复杂。一般情况,尽量保持State Hook扁平。 
更新数组 
  
数组作为State Hook也类似,需要新的数组赋于。增删改如下。
function App() {
  const  [tags, setTags] = useState( ["happy", "cheerful"]);
  const handleClick = () => {
    //添加,不能在原来基础上使用tag.push(''),而是要先复制
    setTags( [...tags, "exciting"]);
    //删除
    setTags(tags.filter((tag) => tag !== "happy"));
    //更新
    setTags(tags.map((tag) => (tag === "happy" ? "happiness" : tag)));
  };
  ...更新对象数组 
  
上一节说过数组更新用map,对象数组也不例会。下面的代码有个bug列表,点击按钮修改id为1的bug的fixed为true.
function App() {
  const  [bugs, setBugs] = useState( [
    { id: 1, title: "Bug 1", fixed: false },
    { id: 2, title: "Bug 2", fixed: false },
  ]);
  const handleClick = () => {
    setBugs(bugs.map((bug) => (bug.id === 1 ? { ...bug, fixed: true } : bug)));
  };
  ...我们可视化上述的代码。B1和B2是原有数组元素,B1*是新建的,而B2则就是原有数组元素,我们不必全部新建所有数组元素,只需新建要更新的元素。 
B1  B2
B1* B2使用Immer简化更新逻辑 
我们看到,更新数组和对象有些麻烦,我们可以使用Immer简化它。首先安装Immer. 
npm i immer@9.0.19
再使用immer简化更新逻辑。首先导入produce,然后在setBugs中将produce函数作为参数,注意produce函数的参数 draft,它表示原来的State,在这里就相当于已存在的tags的副本,然后就可以像普通Javascript一样修改这个副本,Immer会自动设置好修改后的State. 
import produce from "immer";
function App() {
  ...
  const handleClick = () => {
    setBugs(
      produce((draft) => {
        const bug = draft.find((bug) => bug.id === 1);
        if (bug) bug.fixed = true;
      })
    );
  };
...我们来验证下是否生效。 
...
  return (
    <div>
      {bugs.map((bug) => (
        <p key={bug.id}>
          {bug.title} {bug.fixed ? "Fixed" : "New"}
        </p>
      ))}
      <button onClick={handleClick}>Click Me</button>
    </div>
  );
  ...当我们点击按钮时,Bug 1 就变成了 Fixed了,与我们期望的一致。
 
组件间共享State 
  
想象一个电子商务网站,导航栏组件显示购物车货物的数量,而购物车组件列出具体的购物清单,显示,这两个组件需要共享State。
如何共享State呢?
 
查看组件树,NavBar和Cart组件有共同的父组件App,我们可以将购物车清单这个State提升到App组件,App组件再通props传给2个子组件,这样就实现了State共享。
看下代码实现。先创建NavBar组件(快捷键rafce可以方便生成模板)。NavBar组件传递cartItemsCount 的 props,用于指示购物里清单数量。 
import React from "react";
interface Props {
  cartItemsCount: number;
}
const NavBar = ({ cartItemsCount }: Props) => {
  return <div>NavBar:{cartItemsCount}</div>;
};
export default NavBar;再创建Cart组件。定义props两个属性,一个是购物车清单cartItems,另一个是清除购物车操作,清除操作也要升至由父组App组件处理。 
import React from "react";
interface Props {
  cartItems: string [];
  onClear: () => void;
}
const Cart = ({ cartItems, onClear }: Props) => {
  return (
    <>
      <div>Cart</div>
      <ul>
        {cartItems.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
      <button onClick={onClear}>Clear</button>
    </>
  );
};
export default Cart;现来看App组件。很简单,一目了然。 
import { useState } from "react";
import NavBar from "./NavBar";
import Cart from "./Cart";
function App() {
  const  [cartItems, setCartItems] = useState( ["Product1", "Product2"]);
  return (
    <div>
      <NavBar cartItemsCount={cartItems.length} />
      <Cart cartItems={cartItems} onClear={() => setCartItems( [])} />
    </div>
  );
}
export default App;更新State- 练习1
我们有个State为game, 当我们点击按扭时,更改game的player的name.
function App() {
  const  [game, setGame] = useState({
    id: 1,
    player: {
      name: "John",
    },
  });
  ...答案: 
...
  const handleClick = () => {
    setGame({ ...game, player: { ...game.player, name: "Bob" } });
  };
  ...我们也可以使用immer来简化逻辑,这里就不详写了。
 
 
更新State- 练习2
  
我们有个State是pizza,当我们点击按钮时,添加配料Cheese.
function App() {
  const  [pizza, setPizza] = useState({
    name: "Spicy Pepperoni",
    toppings:  ["Mushroom"],
  });
  ...答案: 
...
  const handleClick = () => {
    setPizza({ ...pizza, toppings:  [...pizza.toppings, "Cheese"] });
  };
  ...更新State- 练习3
 
更新cart,当点击按钮时,将id为1的protuct的quantity增加1。
function App() {
  const  [cart, setCart] = useState({
    discount: 1,
    items:  [
      { id: 1, title: "Product 1", quantity: 1 },
      { id: 2, title: "Product 2", quantity: 1 },
    ],
  });答案: 
...
  const handleClick = () => {
    setCart({
      ...cart,
      items: cart.items.map((item) =>
        item.id === 1 ? { ...item, quantity: item.quantity + 1 } : item
      ),
    });
  };
  ...练习:创建可扩展文本组件 
 
我们创建一个可扩展文件组件,可以指定显示前几个字符,点'more'按钮可以查看全部,点'less'又可以恢复缩略显示。
 
代码:
 
新建的ExpandableText.tsx:
import React, { useState } from "react";
interface Props {
  children: string;
  maxChars?: number;
}
const ExpandableText = ({ children, maxChars = 100 }: Props) => {
  const  [isExpanded, setExpanded] = useState(false);
  if (children.length <= maxChars) return <p>{children}</p>;
  const text = isExpanded ? children : children.substring(0, maxChars);
  return (
    <p>
      {text}...
      <button onClick={() => setExpanded(!isExpanded)}>
        {isExpanded ? "Less" : "More"}
      </button>
    </p>
  );
};
export default ExpandableText;App.tsx: 
import ExpandableText from "./ExpandableText";
function App() {
  return (
    <div>
      <ExpandableText>lorem100</ExpandableText>
    </div>
  );
}
export default App;说明:
lorem会自动随机生成文件,100表示100个随机单词,键入时就会随机生成。maxChars是可选的,默认为100。
有人可能会好奇为什么在ExpandableText里没用将text作为StateHook,正如前面我们说到过的,fullName没有必要使用StateHook存储一样,它可能根据其他生成,text也是同样道理。存储在StateHook里的只是那些随时间会变化,而且变化会导致重新渲染的变量,在我们这个例子里,只有 isExpanded需要存在StateHook里。
 
效果如下:
小结 
 
状 态管理是React中的一个基本概念,本文我们了解了组件状态的存储和更新相关知识。下一篇介绍表单的创建。 
 
若后续有更多课程章节,请移步到这里看:mp.weixin.qq.com/s/29JIyr7n954oni165pPYfQ 
					
					
										
					
								
							
				
				
    社区声明 
1、本站提供的一切软件、教程和内容信息仅限用于学习和研究目的用户分享 ,如有侵权请邮件与我们联系处理