2021 年 React 开发者的最佳实践

2025-05-28

2021 年 React 开发者的最佳实践

或许难以置信,React 今年已经八岁了。在技术领域,尤其是在客户端 Web 开发领域,这真是令人瞩目。一个简单的 UI 构建库,怎么会这么老,却依然如此重要?

原因在于,React 不仅彻底革新了 UI 的构建方式,还使构建 UI 的函数式范式流行起来。即便如此,React 并没有止步于此。他们在不破坏现有代码的情况下,继续推进创新理念。因此,React 比以往任何时候都更稳定、更精简、更快速。

然而,React 不断发展的特性也带来了一个缺点,那就是最佳实践会随着时间推移而改变。为了获得一些最新的性能优势,我们需要仔细研究新增的功能。而弄清楚这一点并不总是那么容易,有时甚至根本不简单。

在本文中,我们将介绍 2021 年适用于 React 的最佳实践。

公约

为了组织你的 React 工作,遵循一些约定是有意义的。有些约定甚至是工具顺利运行所必需的。例如,如果你使用驼峰命名法命名组件,那么以下命名将不起作用:

const myComponent = () => <div>Hello World!</div>;

ReactDOM.render(<myComponent />, document.querySelector('#app'));
Enter fullscreen mode Exit fullscreen mode

这是因为 Babel(或 TypeScript)的标准 JSX 转换器使用命名约定来决定是否将字符串或标识符传递给 React。

因此,转换后的代码如下所示:

const myComponent = () => React.createElement("div", null, "Hello World!");

ReactDOM.render(React.createElement("myComponent", null), document.querySelector('#app'));
Enter fullscreen mode Exit fullscreen mode

这不是我们想要的。相反,我们可以使用 PascalCase 命名法。在这种情况下,JSX 转换器将检测自定义组件的使用情况以及所需的引用。

const MyComponent = () => <div>Hello World!</div>;

ReactDOM.render(<MyComponent />, document.querySelector('#app'));
Enter fullscreen mode Exit fullscreen mode

在这种情况下,一切都很好:

ReactDOM.render(React.createElement(MyComponent, null), document.querySelector('#app'));
Enter fullscreen mode Exit fullscreen mode

虽然其他约定不那么严格,但仍应遵循。例如,使用带引号的字符串属性代替 JSX 表达式是有意义的:

// avoid
<input type={'text'} />

// better
<input type="text" />
Enter fullscreen mode Exit fullscreen mode

同样,保持属性引用样式的一致性也是有意义的。大多数指南会在 JS 表达式中使用单引号字符串,并在 React props 中使用双引号字符串。最终,只要代码库中的用法一致,这都没关系。

说到约定和道具,这些也应该遵循使用驼峰式命名的标准 JS 命名约定。

// avoid
const MyComponent = ({ is_valid, Value }) => {
  // ...
  return null;
};

// better
const MyComponent = ({ isValid, value }) => {
  // ...
  return null;
}; 
Enter fullscreen mode Exit fullscreen mode

此外,请确保不要误用内置 HTML 组件 props 的名称(例如 style 或 className)。如果使用这些 props,请将它们转发到相应的内置组件。并且,请保留它们的原始类型(例如,style 为 CSS 样式对象,className 为字符串)。

// avoid
const MyComponent = ({ style, cssStyle }) => {
  if (style === 'dark') {
    // ...
  }

  // ...
  return <div style={cssStyle}>...</div>;
};

// better
const MyComponent = ({ kind, style }) => {
  if (kind === 'dark') {
    // ...
  }

  // ...
  return <div style={style}>...</div>;
};
Enter fullscreen mode Exit fullscreen mode

这使得道具的意图更加清晰,并建立了对于有效使用更大的组件集合至关重要的一致性级别。

组件分离

React 最大的优势之一是它能够轻松地测试和推理组件。然而,这只有在组件足够小且足够专用的情况下才能实现。

当 React 刚刚流行起来时,他们引入了控制器和视图组件的概念,以便高效地构建大型组件。即使如今我们有了专门的状态容器和钩子,以某种方式对组件进行结构化和分类仍然是有意义的。

让我们考虑加载一些数据的简单示例:

const MyComponent = () => {
  const [data, setData] = React.useState();

  React.useEffect(() => {
    let active = true;

    fetch('...')
      .then(res => res.json())
      .then(data => active && setData(data))
      .catch(err => active && setData(err));


    return () => {
      active = false;
    };
  }, []);

  return (
    data === undefined ?
      <div>Loading ...</div> :
      data instanceof Error ?
        <div>Error!</div> :
        <div>Loaded! Do something with data...</div>
  );
};
Enter fullscreen mode Exit fullscreen mode

当然,无组件的操作在这里更合适。但关键在于,编写的组件必须能够收集数据并显示数据。

更清晰的模型意味着分离可能如下所示:

const MyComponent = ({ error, loading, data }) => {
  return (
    loading ?
      <div>Loading ...</div> :
      error ?
        <div>Error!</div> :
        <div>Loaded! Do something with data...</div>
  );
};

const MyLoader = () => {
  const [data, setData] = React.useState();

  React.useEffect(() => {
    let active = true;

    fetch('...')
      .then(res => res.json())
      .then(data => active && setData(data))
      .catch(err => active && setData(err));

    return () => {
      active = false;
    };
  }, []);

  const isError = data instanceof Error;

  return (
    <MyComponent
      error={isError ? data : undefined}
      loading={data === undefined}
      data={!isError ? data : undefined} />
  );
};
Enter fullscreen mode Exit fullscreen mode

为了进一步改进它,最理想的分离是提取到自定义钩子中:

function useRemoteData() {
  const [data, setData] = React.useState();

  React.useEffect(() => {
    let active = true;

    fetch('...')
      .then(res => res.json())
      .then(data => active && setData(data))
      .catch(err => active && setData(err));

    return () => {
      active = false;
    };
  }, []);

  const isError = data instanceof Error;

  return [data === undefined, !isError ? data : undefined, isError ? data : undefined];
}

const MyComponent = () => {
  const [loading, data, error] = useRemoteData();

  return (
    loading ?
      <div>Loading ...</div> :
      error ?
        <div>Error!</div> :
        <div>Loaded! Do something with data...</div>
  );
}; 
Enter fullscreen mode Exit fullscreen mode

钩子

React Hooks 是前端领域最受争议的技术特性之一。它刚推出时,被认为是优雅且创新的。但另一方面,多年来,批评的声音却与日俱增。

抛开优点和缺点,一般来说,根据场景使用钩子可能是最佳实践。

请记住,有一些钩子可以帮助您进行性能优化:

  • useMemo 有助于避免在每次重新渲染时进行昂贵的计算。
  • useCallback 产生稳定的处理程序,类似于 useMemo,但更方便地用于回调。

作为示例,我们来看看没有 useMemo 的以下代码:

const MyComponent = ({ items, region }) => {
  const taxedItems = items.map(item => ({
      ...item,
      tax: getTax(item, region),
  }));

  return (
      <>
        {taxedItems.map(item => <li key={item.id}>
          Tax: {item.tax}
        </li>)}
      </>
  );
};
Enter fullscreen mode Exit fullscreen mode

考虑到该数组中可能有很多项目,并且 getTax 操作非常昂贵(没有双关语的意思),假设项目和区域变化最少,那么您将有相当糟糕的重新渲染时间。

因此,使用 useMemo 会让代码受益匪浅:

const MyComponent = ({ items, region }) => {
  const taxedItems = React.useMemo(() => items.map(item => ({
      ...item,
      tax: getTax(item, region),
  })), [items, region]);

  return (
      <>
        {taxedItems.map(item => <li key={item.id}>
          Tax: {item.tax}
        </li>)}
      </>
  );
}; 
Enter fullscreen mode Exit fullscreen mode

useMemo 的美妙之处在于它几乎是隐形的。正如你所见,我们需要做的就是将计算包装在一个函数中。就是这样,无需其他任何更改。

一个更微妙的问题是缺少 useCallback。让我们看一些非常通用的代码:

const MyComponent = () => {
  const save = () => {
    // some computation
  };
  return <OtherComponent onSave={save} />;
}; 
Enter fullscreen mode Exit fullscreen mode

现在,我们对 OtherComponent 一无所知,但这里可能发生某些变化,例如:

  • 它是一个纯组件,只要所有道具保持不变,就会防止重新渲染。
  • 它在某些记忆或效果钩子上使用回调。
  • 它将回调传递给使用其中一个属性的某个组件。

无论如何,将本质上未发生变化的值作为 props 传递也应该会导致值未发生变化。在渲染函数内部声明一个函数这一事实会带来问题。

一个简单的方法是使用 useCallback 编写相同的内容:

const MyComponent = () => {
  const save = React.useCallback(() => {
    // some computation
  }, []);
  return <OtherComponent onSave={save} />;
};
Enter fullscreen mode Exit fullscreen mode

现在,只有当数组中指定的依赖项之一发生变化时,才会执行重新计算的回调。否则,将返回先前的回调(例如,一个稳定的引用)。

与之前一样,此优化几乎不需要任何代码修改。因此,你应该始终使用 useCallback 来包装回调。

成分

说到纯组件,虽然类组件具有 PureComponent 抽象,但可以使用 memo 明确地将功能性纯组件引入 React。

// no memoed component
const MyComponent = ({ isValid }) => (
  <div style=\{{ color: isValid ? 'green' : 'red' }}>
    status
  </div>
);

// memoed component
const MyComponent = React.memo(({ isValid }) => (
  <div style=\{{ color: isValid ? 'green' : 'red' }}>
    status
  </div>
));
Enter fullscreen mode Exit fullscreen mode

React 文档memo 做了非常详细的说明。它写道:“如果你的组件在相同的 props 下渲染了相同的结果,你可以将其包装在对 React.memo 的调用中,以便在某些情况下通过 memo 来提升性能。这意味着 React 将跳过渲染组件,并重用上次渲染的结果。”

请记住,就像 React 进行的其他比较一样,props 只是进行浅比较。因此,只有当我们谨慎传递参数时,这种优化才会生效。例如,如果我们使用 useMemo 和其他技术来处理复杂的 props,例如数组、对象和函数。

你可能注意到了,我们只使用了函数式组件。事实上,自从引入 hooks 之后,你几乎可以不用类组件了。

仍然使用类组件的可能原因只有两个:

  1. 您希望访问更复杂的生命周期事件。例如,shouldComponentUpdate。
  2. 您想引入错误边界。

然而,即使在这些情况下,你可能只需要编写一个 React 类组件就能满足你的需求。看看这个边界:

export class Boundary extends React.Component {
  state = {
    error: undefined,
  };

  componentDidCatch(error) {
    this.setState({
      error,
    });
  }

  render() {
    const { error } = this.state;
    const { children, ShowError } = this.props;

    if (error) {
      return <ShowError error={error} />;
    }

    return children;
  }
}
Enter fullscreen mode Exit fullscreen mode

该组件不仅会捕获其子组件中可能出现的任何错误,而且还会显示作为 ShowError 传入的后备组件,该组件接收单个 prop:错误。

运算符

一些运算符可用于简化 React 中的树构造。例如,三元运算符允许我们编写如下代码:

<div>
  {currentUser ? <strong>{currentUser}</strong> : <span>Not logged in</span>}
</div> 
Enter fullscreen mode Exit fullscreen mode

布尔运算符(例如 && 和 ||)也可能有用,但需要注意一些陷阱。例如,请看以下代码片段:

<div>
  {numUsers && <i>There are {numUsers} users logged in.</i>}
</div>
Enter fullscreen mode Exit fullscreen mode

假设 numUsers 始终是 0 到用户总数之间的数字,如果 numUsers 为正数,我们就会得到预期的输出。

<div>
  <i>There are 5 users logged in.</i>
</div>
Enter fullscreen mode Exit fullscreen mode

然而,对于零用户的特殊情况,我们会得到这样的结果:

<div>
  0
</div>
Enter fullscreen mode Exit fullscreen mode

这可能不是我们想要的,所以布尔转换或更明确的比较可能会有所帮助。通常,下面的代码更易读:

<div>
  {numUsers > 0 && <i>There are {numUsers} users logged in.</i>}
</div> 
Enter fullscreen mode Exit fullscreen mode

现在,在零用户边缘情况中我们得到:

<div>
</div>
Enter fullscreen mode Exit fullscreen mode

使用三元运算符作为独占布尔运算符可以完全避免这个问题。但是如果我们不想渲染任何东西怎么办?我们可以使用 false 或空片段:

<div>
  {numUsers ? <i>There are {numUsers} users logged in.</i> : <></>}
</div> 
Enter fullscreen mode Exit fullscreen mode

空的 fragment 的优点在于,它允许我们稍后添加内容。然而,对于不太熟悉 React 的用户来说,它看起来可能有点奇怪。

结论

在本文中,我们介绍了一些让你的 React 代码库更易于使用的最佳实践。通过从类组件切换到函数组件,你可以更深入地了解 Hooks。这将自动实现良好的关注点分离,其中行为方面全部在函数中完成,渲染则在组件中定义。

通过遵循一组有用的约定,并结合一些技术(例如使用正确的运算符、钩子和关注点分离),您最终应该得到一个可以轻松维护和扩展的干净代码库。

文章来源:https://dev.to/mescius/best-practices-for-react-developers-in-2021-42a
PREV
我的 React 面试题集锦(第一部分)+10 道 React 面试题,直接来自我的库
NEXT
扩展大型 Vue.js 应用程序的 3 个技巧