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'));
这是因为 Babel(或 TypeScript)的标准 JSX 转换器使用命名约定来决定是否将字符串或标识符传递给 React。
因此,转换后的代码如下所示:
const myComponent = () => React.createElement("div", null, "Hello World!");
ReactDOM.render(React.createElement("myComponent", null), document.querySelector('#app'));
这不是我们想要的。相反,我们可以使用 PascalCase 命名法。在这种情况下,JSX 转换器将检测自定义组件的使用情况以及所需的引用。
const MyComponent = () => <div>Hello World!</div>;
ReactDOM.render(<MyComponent />, document.querySelector('#app'));
在这种情况下,一切都很好:
ReactDOM.render(React.createElement(MyComponent, null), document.querySelector('#app'));
虽然其他约定不那么严格,但仍应遵循。例如,使用带引号的字符串属性代替 JSX 表达式是有意义的:
// avoid
<input type={'text'} />
// better
<input type="text" />
同样,保持属性引用样式的一致性也是有意义的。大多数指南会在 JS 表达式中使用单引号字符串,并在 React props 中使用双引号字符串。最终,只要代码库中的用法一致,这都没关系。
说到约定和道具,这些也应该遵循使用驼峰式命名的标准 JS 命名约定。
// avoid
const MyComponent = ({ is_valid, Value }) => {
// ...
return null;
};
// better
const MyComponent = ({ isValid, value }) => {
// ...
return null;
};
此外,请确保不要误用内置 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>;
};
这使得道具的意图更加清晰,并建立了对于有效使用更大的组件集合至关重要的一致性级别。
组件分离
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>
);
};
当然,无组件的操作在这里更合适。但关键在于,编写的组件必须能够收集数据并显示数据。
更清晰的模型意味着分离可能如下所示:
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} />
);
};
为了进一步改进它,最理想的分离是提取到自定义钩子中:
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>
);
};
钩子
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>)}
</>
);
};
考虑到该数组中可能有很多项目,并且 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>)}
</>
);
};
useMemo 的美妙之处在于它几乎是隐形的。正如你所见,我们需要做的就是将计算包装在一个函数中。就是这样,无需其他任何更改。
一个更微妙的问题是缺少 useCallback。让我们看一些非常通用的代码:
const MyComponent = () => {
const save = () => {
// some computation
};
return <OtherComponent onSave={save} />;
};
现在,我们对 OtherComponent 一无所知,但这里可能发生某些变化,例如:
- 它是一个纯组件,只要所有道具保持不变,就会防止重新渲染。
- 它在某些记忆或效果钩子上使用回调。
- 它将回调传递给使用其中一个属性的某个组件。
无论如何,将本质上未发生变化的值作为 props 传递也应该会导致值未发生变化。在渲染函数内部声明一个函数这一事实会带来问题。
一个简单的方法是使用 useCallback 编写相同的内容:
const MyComponent = () => {
const save = React.useCallback(() => {
// some computation
}, []);
return <OtherComponent onSave={save} />;
};
现在,只有当数组中指定的依赖项之一发生变化时,才会执行重新计算的回调。否则,将返回先前的回调(例如,一个稳定的引用)。
与之前一样,此优化几乎不需要任何代码修改。因此,你应该始终使用 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>
));
React 文档对memo 做了非常详细的说明。它写道:“如果你的组件在相同的 props 下渲染了相同的结果,你可以将其包装在对 React.memo 的调用中,以便在某些情况下通过 memo 来提升性能。这意味着 React 将跳过渲染组件,并重用上次渲染的结果。”
请记住,就像 React 进行的其他比较一样,props 只是进行浅比较。因此,只有当我们谨慎传递参数时,这种优化才会生效。例如,如果我们使用 useMemo 和其他技术来处理复杂的 props,例如数组、对象和函数。
你可能注意到了,我们只使用了函数式组件。事实上,自从引入 hooks 之后,你几乎可以不用类组件了。
仍然使用类组件的可能原因只有两个:
- 您希望访问更复杂的生命周期事件。例如,shouldComponentUpdate。
- 您想引入错误边界。
然而,即使在这些情况下,你可能只需要编写一个 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;
}
}
该组件不仅会捕获其子组件中可能出现的任何错误,而且还会显示作为 ShowError 传入的后备组件,该组件接收单个 prop:错误。
运算符
一些运算符可用于简化 React 中的树构造。例如,三元运算符允许我们编写如下代码:
<div>
{currentUser ? <strong>{currentUser}</strong> : <span>Not logged in</span>}
</div>
布尔运算符(例如 && 和 ||)也可能有用,但需要注意一些陷阱。例如,请看以下代码片段:
<div>
{numUsers && <i>There are {numUsers} users logged in.</i>}
</div>
假设 numUsers 始终是 0 到用户总数之间的数字,如果 numUsers 为正数,我们就会得到预期的输出。
<div>
<i>There are 5 users logged in.</i>
</div>
然而,对于零用户的特殊情况,我们会得到这样的结果:
<div>
0
</div>
这可能不是我们想要的,所以布尔转换或更明确的比较可能会有所帮助。通常,下面的代码更易读:
<div>
{numUsers > 0 && <i>There are {numUsers} users logged in.</i>}
</div>
现在,在零用户边缘情况中我们得到:
<div>
</div>
使用三元运算符作为独占布尔运算符可以完全避免这个问题。但是如果我们不想渲染任何东西怎么办?我们可以使用 false 或空片段:
<div>
{numUsers ? <i>There are {numUsers} users logged in.</i> : <></>}
</div>
空的 fragment 的优点在于,它允许我们稍后添加内容。然而,对于不太熟悉 React 的用户来说,它看起来可能有点奇怪。
结论
在本文中,我们介绍了一些让你的 React 代码库更易于使用的最佳实践。通过从类组件切换到函数组件,你可以更深入地了解 Hooks。这将自动实现良好的关注点分离,其中行为方面全部在函数中完成,渲染则在组件中定义。
通过遵循一组有用的约定,并结合一些技术(例如使用正确的运算符、钩子和关注点分离),您最终应该得到一个可以轻松维护和扩展的干净代码库。
文章来源:https://dev.to/mescius/best-practices-for-react-developers-in-2021-42a