React入门(四)-- 管理组件状态

admin 2023-05-13 08:00:25 1117

状态管理是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 0
Message 0
Message 0

但要是在 Message.tsx里的渲染代码中添加修改代码"count++",如下:

let count = 0;
const Message = () => {
  count++;
  return <div>Message {count}</div>;
  ...

输出的各个Message就不一样了,这样组件就不纯了。

# 访问:http://localhost:5173
Message 2
Message 4
Message 6

所以在组件内,不要修改已存在的对象,这样才能保持组件是纯的。另外注意,如果创建和更新对象均放在组件内部实现,这是不影响的。比如我们将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

可爱猫?Telegram电报群 https://t.me/ikeaimao

社区声明 1、本站提供的一切软件、教程和内容信息仅限用于学习和研究目的
2、本站资源为用户分享,如有侵权请邮件与我们联系处理敬请谅解!
3、本站信息来自网络,版权争议与本站无关。您必须在下载后的24小时之内,从您的电脑或手机中彻底删除上述内容
最新回复 (0)

您可以在 登录 or 注册 后,对此帖发表评论!

返回