读完React新文档后的8条收获

7,790 阅读8分钟

不久前,React官网进行了重构。新官网将hook作为介绍的重头戏,重新通读了一遍react文档,有了一些自己的感悟。在此分享出来与各位共勉。

1. 换个角度认识Props与State

PropsState是React中两个略有相似的概念。在一个React组件中,它们作为数据来源可能同时存在,但它们之间有着巨大不同:

  1. Props更像函数中的参数。它主要用于组件之间的信息传递,它可以是任意类型的数据:数字、文本、函数、甚至于组件等等。
  2. State更像组件中的内存。它可以保存组件内部的状态,组件通过跟踪这些状态,从而渲染更新。
import React, { useState } from 'react';

// 父组件
const ParentComponent = () => {
  const [count, setCount] = useState(0); // 使用state来追踪count的值

  return (
    <div>
      <ChildComponent age={25} />
      <p>Count: {count}</p>
    </div>
  );
};

// 子组件
const ChildComponent = (props) => {
  const { age } = props; // 使用props来获取父组件传递的数据

  return (
    <div>
      <p>Age: {age}</p>
    </div>
  );
};

2. 不要嵌套定义组件

在一个组件中直接定义其他组件,可以省去很多传递Props的工夫,看上去很好。但我们不应该嵌套定义组件,原因在于**嵌套定义组件会导致渲染速度变慢,也更容易出现BUG**。 我们在嵌套定义组件的情况下,当父组件更新时,内部嵌套的子组件也会重新生成并渲染,子组件内部的state也会丢失。针对这种情况,我们可以通过两种方式来解决:

  1. 为子组件包上useMemo,避免不必要的更新;
  2. 不再嵌套定义,将子组件提至父组件外,通过Props传参。

这里更推荐第二种方法,因为这样代码更少,结构也更清晰,组件迁移维护都会更方便一些。

//🔴 Bad Case
export default function Gallery() {
  function Profile() {
    // ...
  }
  // ...
}
//✅  Good Case
function Profile() {
  // ...
}

export default function Gallery() {
  // ...
}

3. 尽量不要使用匿名函数组件

因为类似export default () => {}的匿名函数组件书写虽然方便,但会让debug变得更困难。 如下是两种不同类型组件出错时的控制台的表现:

  1. 具名组件出错时的提示,可直接的指出错的函数组件名称: image.png

  2. 匿名函数出错时的提示,无法直观确定页面内哪个组件出了错: image.png

4. 使用逻辑运算符&&编写JSX时,左侧最好不要是数字

运算符&&在JSX中的表现与JS略有不同:

  1. 在JS中,只有在&&左侧的值0nullundefinedfalse''等假值时,才会返回右侧值;
  2. 在JSX中,React对于falsenullundefined并不会做渲染处理,所以进行&&运算后,如果左侧值为假被返回后,会出现与直觉不同的渲染结果:可能会碰到页面上莫名奇妙出现了一个0的问题。为了避免出现这种情况,可以在书写JSX时,为左侧的值加上!!来进行强制类型转换。
const flag = 0
//🔴 Bad Case
{
    flag && <div>123</div>
}
//✅  Good Case 1
{
    !!flag && <div>123</div>
}
//✅  Good Case 2
{
    flag > 0 && <div>123</div>
}

关于JSX对各种常见假值的渲染,这里进行了总结:

  1. nullundefinedfalse:这些值在JSX中会被视为空,不会渲染任何内容。它们会被忽略,并且不会生成对应的DOM元素。
  2. 0NaN:这些值会在页面上渲染为字符串"0"、"NaN"。
  3. ''[]{}:空字符串会被渲染为什么都没有,不会显示任何内容。

注:这里感谢@小明家的bin的评论提醒,他的见解对我起到了很大的启发作用。

5.全写的 Fragment标签上可以添加属性key

在map循环添加组件时,需要为组件添加key来优化性能。如果待循环的组件有多个时,可以在外层包裹<Fragment>...</Fragment>标签,在其上添加key添加在<Fragment>...</Fragment>来避免创建额外的组件。

const list = [1,2,3]
//🔴 Bad Case
//不能添加key
{
    list.map(v=><> <div>1-1</div>  <div>1-2</div> </>)
}
//🔴 Bad Case
//创建了额外的div节点
{
    list.map(v=><div key={v}> <div>1-1</div>  <div>1-2</div> <div/>)
}
//✅  Good Case
{
    list.map(v=><Fragment key={v}> <div>1-1</div>  <div>1-2</div> </Fragment>)
}

注意简写的Fragment标签<>...</>上不支持添加key

6. 可以使用updater function,来在下一次渲染之前多次更新同一个state

React中处理状态更新时采用了批处理机制,在下一次渲染之前多次更新同一个state,可能不会达到我们想要的效果。如下是一个简单例子:

// 按照直觉一次点击后button中的文字应展示为3,但实际是1
function Demo(){
    const [a,setA] = useState(0)
    
    function handler(){
        setA(a + 1);
        setA(a + 1);
        setA(a + 1);
    }
    
    return <button onclick={handler}>{a}</button>
}

在某些极端场景下,我们可能希望state的值能够及时发生变化,这时便可以采用setA(n => n + 1)(这种形式被称为updater function)来进行更新:

// 一次点击后a的值会被更新为3
function Demo(){
    const [a,setA] = useState(0)
    
    function handler(){
        setA(n => n + 1);
        setA(n => n + 1);
        setA(n => n + 1);
    }
    
    return <button onclick={handler}>{a}</button>
}

7. 管理状态的一些原则

更加结构化的状态有助于提高我们组件的健壮性,以下是一些管理组件内状态时可以参考的原则:

  1. 精简相关状态:如果在更新某个状态时总是需要同步更新其他状态变量,可以考虑将它们合并为一个状态变量。
  2. 避免矛盾状态:避免出现两个或多个状态的值互相矛盾的情况。
  3. 避免冗余状态:一个状态可以由其余状态计算而来时,其不应单独作为一个状态来进行管理。
  4. 避免状态重复:当相同的数据在多个状态变量之间或嵌套对象中重复时,很难使它们保持同步。尽可能减少重复。
  5. 避免深度嵌套状态:嵌套层级过深的状态更新时相对麻烦许多,尽量保持状态扁平化。

8. 使用useSyncExternalStore订阅外部状态

useSyncExternalStore是一个React18中新提供的一个Hook,可以用于订阅外部状态。 它的使用方式如下:

import { useSyncExternalStore } from 'react';

function MyComponent() {
  const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
  // ...
}

useSyncExternalStore接受三个参数:

  • subscribe:一个订阅函数,用于订阅存储,并返回一个取消订阅的函数。
  • getSnapshot:一个从存储中获取数据快照的函数。在存储未发生变化时,重复调用getSnapshot应该返回相同的值。当存储发生变化且返回值不同时,React会重新渲染组件。
  • getServerSnapshot(可选):一个在服务器端渲染时获取存储初始快照的函数。它仅在服务器端渲染和在客户端进行服务器呈现内容的hydration过程中使用。如果省略该参数,在服务器端渲染组件时会抛出错误。

举一个具体的例子,我们可能需要获取浏览器的联网状态,并期望在浏览器网络状态发生变化时做一些处理。使用useSyncExternalStore可以很大程度上简化我们的代码,同时使逻辑更加清晰:

//🔴 Bad Case
function useOnlineStatus() {
  // Not ideal: Manual store subscription in an Effect
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);
    return () => {
      window.removeEventListener('online', updateState);
      window.removeEventListener('offline', updateState);
    };
  }, []);
  return isOnline;
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}
function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

// ✅ GoodCase
function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe, // React won't resubscribe for as long as you pass the same function
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

结语

文章的最后,再来一次无废话总结:

  1. 更清晰地认识了Props与State之间的区别。Props更像是函数的参数,用于组件之间的信息传递;而State更像是组件内部的内存,用于保存组件的状态并进行渲染更新。
  2. 不推荐在一个组件内部嵌套定义其他组件,因为这样会导致渲染速度变慢并容易产生BUG。推荐将子组件提到父组件外部并通过Props传递数据。
  3. 尽量避免使用匿名函数组件,因为在出错时会增加调试的难度。具名组件的出错提示更加直观和准确。
  4. 在使用逻辑运算符&&编写JSX时,左侧最好不要是数字。在JSX中,0会被当作有效的值,而不是假值,为了避免出现问题,可以在左侧的值加上!!进行强制类型转换。
  5. 当在使用全写的Fragment标签时,可以给Fragment标签添加属性key,以优化性能和避免创建额外的组件。
  6. 使用updater function的方式进行状态更新,可以确保在下一次渲染之前多次更新同一个state。这样可以避免批处理机制带来的问题。
  7. 在管理组件内状态时,可以遵循一些原则,如精简相关状态、避免矛盾状态、避免冗余状态、避免状态重复等,以提高组件的健壮性和可维护性。
  8. 使用useSyncExternalStore可以订阅外部状态,它是React 18中新增的Hook。通过订阅函数、获取数据快照的函数以及获取服务器初始快照的函数,我们可以简化订阅外部状态的代码逻辑。

通过对React文档的深入学习和实践,我对React的理解更加深入了解,希望这些收获也能对大家在学习和使用React时有所帮助。