精读《React — 5 Things That Might Surprise You》

1. 使用之前的状态设置状态是不可预测的

状态管理是 React 的基础,虽然useState可能是最常见的钩子,但可能对其实际行为有些不了解。让我们来看看以下组件:

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [counter, setCounter] = useState(0);
  return (
    <div className="App">
      <h1>Counter: {counter}</h1>
      <button
        onClick={() => {
          setCounter(counter + 1);
          setCounter(counter + 1);
        }}
      >
        +
      </button>
    </div>
  );
}

在用户单击按钮后,您希望计数器状态的值是多少?A. 2 B. 1 ✔️

点击demo

原因是在我们的状态更新期间,我们使用了之前的状态值:setCounter(count + 1)。本质上,setState函数被包装在功能组件闭包中,因此它提供了在该闭包中捕获的值。这意味着当它最终被执行时(setState函数是异步的),它可能持有一个不再相关的状态值。最重要的是,setState 的连续执行可能会导致 React 的调度算法使用相同的事件处理程序处理多个非常快速的状态更新。在异步函数中设置状态时也可能出现同样的问题:

onClick={() => { 
   setTimout(() => { setCounter(counter + 1); ), 1000); 
}};

但是,不用担心,React 实际上为这个问题提供了一个简单的解决方案——“functional updates”。setCounter((prevCounter) => prevCounter + 1);

注意:「每当你的状态更新依赖于之前的状态时,请务必使用functional updates!」

// incorrect
const update = useCallback(() => {
   setCounter(counter + 1);
}, [counter]);

// correct ✔️
const update = useCallback(() => {
   setCounter(prevCounter => prevCounter + 1);
}, []);

2.可以使用useRef来存储静态变量

我们习惯于使用 React 中的 ref 机制作为访问元素的 DOM 节点的手段,无论是因为我们需要它来计算其大小、设置焦点状态,或者基本上做任何 React 自然不能做的事情。但是 refs 也可以用于不同的目的——我们可以使用类组件非常容易·实现这一点,但我们不能使用函数式组件——保留一个不会在每次渲染时重新创建的静态变量

点击demo

在函数式组件中我们可以使用ref存储静态变量

3. React 可以强制重新挂载一个组件

写入DOM的成本非常高。这就是为什么我们通常不想重新mount 组件,除非绝对必要。但是有时我们必须,出于各种原因。那么在那种情况下,我们如何告诉 react 卸载并立即重新mount 组件?用一个简单的技巧——为我们的组件提供一个key,并改变它的值。

key prop 是一个特殊的 React 属性

著名的 React 警告

image

key是帮助 React 跟踪元素的东西,即使我们已经改变了它在组件结构中的位置或重新渲染了父级(否则每次渲染都会导致整个组件数组被重新安装,这是不好的性能)。

使用这种机制,我们可以欺骗 React 认为一个组件与其之前的自己不同,并导致它重新挂载。

点击demo

import React, { useEffect, useState, useCallback } from "react";
import "./styles.css";

export default function App() {
  const [key, setKey] = useState(1);
  const [console, setConsole] = useState([]);

  const onLifecycleChange = useCallback((message) => {
    setConsole((prevConsole) => [message, ...prevConsole]);
  }, []);
  return (
    <div className="App">
      <button
        onClick={() => {
          setKey((oldKey) => oldKey + 1);
        }}
      >
        Remount
      </button>
      <ChildComp key={key} onLifecycleChange={onLifecycleChange} />

      <div className="console">
        {console.map((text, i) => (
          <div key={i}>{text}</div>
        ))}
      </div>
    </div>
  );
}

const ChildComp = React.memo(({ onLifecycleChange }) => {
  useEffect(() => {
    onLifecycleChange("mounting ChildComp");
    return () => {
      onLifecycleChange("ummounting ChildComp");
    };
  }, [onLifecycleChange]);

  return <div style={{ marginTop: 10 }}>Child Comp</div>;
});

4.Context不像你期望的那样工作

Context用来解决 “prop drilling” 问题,但是它会带来性能问题,(context value如果是对象)其中一个属性状态发生变化,会导致其它订阅Context的组件都发生更新,所以context一般用于不频繁更新的场景比如(locale和theme)

  • use-context-selector可以解决context带来的性能问题

  • 频繁更新状态(状态共享)的,推荐使用Redux等状态管理工具

import React, { useState, useContext } from "react";
import "./styles.css";

const SomeContext = React.createContext({});

export default function App() {
  const [contextValue, setContextValue] = useState({ name: "John", age: 55 });

  const onChangeAge = (e) => {
    const age = e.target.value;
    setContextValue((prevContextValue) => ({ ...prevContextValue, age }));
  };

  const onChangeName = (e) => {
    const name = e.target.value;
    setContextValue((prevContextValue) => ({ ...prevContextValue, name }));
  };

  return (
    <div className="App">
      <SomeContext.Provider value={contextValue}>
        <Wrapper />
        <input value={contextValue.age} onChange={onChangeAge} />
        <input value={contextValue.name} onChange={onChangeName} />
      </SomeContext.Provider>
    </div>
  );
}

const Wrapper = () => {
  return (
    <div>
      <Name />
      <Age />
    </div>
  );
};

const Name = () => {
  const { name } = useContext(SomeContext);
  console.log("name rendered");
  return <h1>Name: {name}</h1>;
};

const Age = () => {
  const { age } = useContext(SomeContext);
  console.log("age rendered");
  return <h1>Age: {age}</h1>;
};

点击demo

5. React 有一个完整的 API 来处理 children 属性

React为Children属性提供了一系列API

React.Children.toArray(children)
// If you want to use map/forEach:
React.Children.map(children, fn)
React.Children.forEach(children, fn)
React.Children.count(children)

如果你需要在您的组件中强制执行单个子项(我最近注意到 formik 这样做),你可以简单地在您的组件中包含以下行,React 将为你运行检查和错误处理:

React.Children.only(children)
import React from "react";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <Wrapper>
        <h2 style={{ color: "red", margin: 0 }}>Red</h2>
        <h2 style={{ color: "blue" }}>Blue</h2>
        <h2 style={{ color: "green" }}>Green</h2>
      </Wrapper>
      <Wrapper>hello</Wrapper>
    </div>
  );
}

const Wrapper = ({ children }) => {
  const childrenArray = React.Children.toArray(children);
  console.log(childrenArray);
  return (
    <div style={{ border: "1px solid", padding: 20, margin: 5 }}>
      <div>{children}</div>
      <div>Number of children: {React.Children.count(children)}</div>
      <div>
        children type: <strong>{typeof children}</strong>
      </div>
    </div>
  );
};

点击demo

参考文献

  • https://medium.com/geekculture/react-5-things-that-might-surprise-you-ddefd9fbac0f

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值