React开发者的JavaScript技巧
过去几年我一直在使用 React,所以很自然地,我对刚开始使用 React 时写的代码并不感到自豪,因为现在我知道我当时犯的错误,而我当时并没有意识到这些错误。
但快进到今天,我一路走来学到了很多东西,包括为开源项目做贡献、观看/阅读一些有趣的博客和会议演讲,以及观察其他人如何编写代码。
以下是一些JavaScript技巧,如果早点知道,或许对现在的我以及你都有帮助,可以编写出更高效、更易于维护的React代码——
1. 有效利用条件渲染
作为一名 React 开发人员,你一定遇到过这样的情况:你只想在 prop 或 state 中的某个条件得到满足时才显示组件,或者根据 state 的不同值渲染不同的组件。
例如,如果你有一个组件,你想在发出请求时显示加载指示器,并在请求成功后使用数据渲染该组件,我喜欢这样做——
const SomeComponent = ({ isLoading, data }) => {
if(isLoading) {
return <Loader/>
}
return (
<DataHandler>
.
.
</DataHandler>
);
}
但是,如果你想在满足特定条件时在 JSX 中渲染某些内容,那么你可以使用逻辑与运算符 ( &&) 来实现。
const Button = ({ showHomeIcon, children, onClick }) => (
<button type="button" onClick={onClick}>
{showHomeIcon && <HomeIcon />}
{children}
</button>
);
虽然更实用的场景是这样做,即提供一个名为 icon 的可选属性,该属性是一个字符串,包含可用于相应地渲染图标组件的图标名称 -
const Button = ({ icon, children, onClick }) => (
<button type="button" onClick={onClick}>
{/* Icon won't be rendered if the value of
icon prop is anything other than a string */}
{typeof icon === "string" && <Icon name={icon} />}
{children}
</button>
);
// Renders a button with a home icon
<Button icon="home" onClick={handleClick}>Home</Button>
// Renders a button without an icon
<Button onClick={handleClick}>About</Button>
这样可以解决只有一个组件时的问题,但是如果有两个或两个以上的组件,并且想要根据某些属性或状态变量来渲染它们,该怎么办呢?
对于两个分量,三元运算符是我的首选方法,因为它简单易用——
const App = props => {
const canViewWelcomeText = isUserAuthenticated(props);
return canViewWelcomeText ? (
<div>Hey, there! Welcome back. Its been a while.</div>
) : (
<div>You need to login to view this page</div>
);
};
如果你有很多组件需要根据条件进行渲染,那么 switch case 可能是最佳选择——
const getCurrentComponent = currentTab => {
switch (currentTab) {
case 'profile':
return <Profile />;
case 'settings':
return <Settings />;
default:
return <Home />;
}
};
const Dashboard = props => {
const [currentTab, setTab] = React.useState('profile');
return (
<div className="dashboard">
<PrimaryTab currentTab={currentTab} setTab={setTab} />
{getCurrentComponent(currentTab)}
</div>
);
};
2. 避免使用真值检验。
如果你熟悉 JavaScript,那么你可能了解真值和假值。真值测试其实就是在控制流语句中使用 JavaScript 的这种强制转换能力,例如:
// ❌ Avoid adding checks like these
// for non boolean variables
if (somVar) {
doSomething();
}
乍一看,如果你想避免类似这样的情况,这似乎不错,null因为它是一个假值,所以语句会按预期运行。但问题在于,这种方法很容易出现难以追踪的错误。这是因为上述语句不仅会阻塞非空值,还会阻塞所有我们想要避免的null假值。someVar
someVar = 0
someVar = ""
someVar = false
someVar = undefined
那么,进行这些检查的正确方法是什么?
正确的做法是尽可能简化这些检查,以避免引入任何错误。对于上述情况,正确的做法是:
// ✅ Explictly check for the conditions you want
if (someVar !== null) {
doSomething();
}
如果您确定传递的变量是布尔值,那么您可以使用前一种方法。
这同样适用于使用我们在上一个技巧中看到的逻辑与运算符进行条件渲染的情况。
如果第一个运算符为假,JavaScript 会返回该对象。因此,对于类似这样的表达式,` 0 && "javascript"is` 将返回 `false` 0,而对于类似这样的表达式false && "javascript",`is` 将返回 `false` false。如果你这样做,可能会遇到问题——
// ❌ This will end up rendering 0 as the text if
// the array is empty
{cats.length && <AllCats cats={cats} />}
// ✅ Use this instead because the result of the
// condition would be a boolean
{cats.length > 0 && <AllCats cats={cats} />}
3. 使用可选链式调用和空值合并
在我们的应用程序中处理数据时,我们经常需要处理调用 benull或undefined提供默认值的数据部分。
假设我们有一个 API,它以以下格式返回宠物的详细信息 -
// Endpoint - /api/pets/{id}
{
id: 42,
name: 'Ghost',
type: 'Mammal',
diet: 'Carnivore'
owner: {
first_name: 'Jon',
last_name: 'Snow',
family: {
name: 'Stark',
location: 'Winterfell'
}
}
}
所以,如果你想要宠物主人的名字,你可以这样做。
const ownerName = pet.owner.first_name;
但就像宇宙中所有事物都不可能完美一样,我们的 API 不能保证任何给定宠物的全部详细信息都可用,并且可能存在null问题undefined。
在这种情况下,上述代码行可能会导致以下错误“引用错误,无法读取属性first_name” null,如果所有者是,则会导致应用程序崩溃null。
这时可选链就派上用场了。可选链运算符 ( ?.) 允许你读取链中深处的属性,而无需验证链是否有效,并且它不会返回引用错误,而是返回相同的旧值undefined。
因此,我们可以轻松地检查所有者姓名,甚至是所有者的姓氏,而无需担心任何错误,就像这样——
const ownerName = pet?.owner?.first_name;
const ownerFamily = pet?.owner?.family?.name;
可选链式调用也可以用于对不确定是否存在的属性进行函数调用。例如,你可以对数组执行此操作
friends?.join(","),即使 friends 不是数组,也不会导致错误undefined。
这样可以避免错误,但你仍然不希望用户看到undefined不可用的选项。这就是空值合并的用武之地——
const ownerName = pet?.owner?.first_name ?? 'Unknown';
空合并运算符( )??当左侧为null或undefined时返回右侧操作数,否则返回左侧操作数。
你可能会认为逻辑或运算符(` ||)也能达到同样的效果。如果是这样,我希望你没有忘记我们刚才讲过的 JavaScript 真假值混乱的问题。因为对于所有假值,这个运算符都会返回右侧的操作数,正如上一节提到的,这可能会导致难以调试的错误。
由于这些方法是最近才随ECMAScript 2020 规范发布的,因此这些方法的浏览器兼容性仍处于初步阶段,目前只有现代浏览器支持它。
但别担心,我们大多数用于编译应用程序的配置都已经涵盖了这些需求。如果您使用的是 TypeScript 3.7 或更高版本,那么这些方法都是开箱即用的。
否则,如果您使用的是典型的 Webpack + babel 设置,则可以在 babel 配置中包含
@babel/plugin-proposal-optional-chainingand插件来支持这些方法。@babel/plugin-proposal-nullish-coalescing-operator
4.避免过早优化
在 React 中使用 memoize 时一定要非常小心,因为如果操作不当,可能会导致性能更差。
我经常看到人们过早地对所有遇到的事物进行优化,却不考虑这样做的代价。例如,useCallback在类似这样的情况下——
const MyForm = () => {
const [firstName, setFirstName] = React.useState('');
const handleSubmit = event => {
/**
* Ommitted for brevity
*/
};
// ❌ useCallback is unnecessary and can actually be worse for performance
const handleChange = React.useCallback(event => {
setFirstName(event.target.value);
}, []);
return (
<form onSubmit={handleSubmit}>
<input type="text" name="firstName" onChange={handleChange} />
<button type="submit" />
</form>
);
};
你可能听说过,useCallback通过缓存函数并仅在依赖项发生变化时更新它,可以提高性能。这没错,但你需要明白,任何优化都会带来一定的代价。
在上述例子中,你创建了一个useCallback本身就运行一些逻辑表达式检查的函数,这实际上做了更多的工作,因此你最好直接像这样定义内联函数——
const handleChange = event => {
setFirstName(event.target.value);
};
同样的情况也适用于组件React.memo。如果你有一个像这样的组件,它接受子组件的属性,那么如果子组件没有被记忆化,那么对组件进行记忆化基本上是没用的。
const UselessMemoizedHeader = React.memo(({ children }) => <div>{children}</div>);
const SomeComponent = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<UselessMemoizedHeader>
<span>Header</span>
</UselessMemoizedHeader>
Count: {count}
<button type="button" onClick={() => setCount(currentCount => currentCount + 1)}>
Increment count
</button>
</div>
);
};
在上述情况下,UselessMemoizedHeader即使您可能认为组件已进行记忆化,但每次增加计数时,组件都会重新渲染。
但为什么呢?因为 memo 只是对当前 props 和之前的 props 进行浅层比较,而且 children prop 不会具有引用相等性,UselessMemoizedHeader所以每次计数改变时,最终都会重新渲染组件。
由于每次渲染时都进行了不必要的 children 属性比较,你的代码最终会变得更糟。
那么,究竟什么时候需要记笔记呢?肯特·C·多兹在他的文章中详细探讨了以上所有问题,以及何时应该记笔记。我建议你读一读。
5. 注意依赖关系数组
与 memoization 相关的 React hooks(useCallback和useMemo)以及useEffecthook 接受第二个参数,该参数是一个数组,通常称为依赖数组。
useEffect仅当依赖数组的浅相等性检查与先前的值不相等时,才会重新运行该效果。
React.useEffect(() => {
/**
* Fetch data with new query
* and update the state
*/
}, [query]); // < The effect reruns only when the query changes
类似地,记忆化钩子也仅在其依赖数组中的值发生变化时才会重新计算。
const someValue = React.useMemo(() =>
computationallyExpensiveCalculation(count),
[count]); // < someValue is recomputed only when count changes
现在明白了。你能找出为什么每次 CatSearch 组件重新渲染时都会触发这个效果吗?即使查询、高度和颜色属性基本相同?
const CatSearch = ({ height, color, query, currentCat }) => {
const filters = {
height,
color,
};
React.useEffect(() => {
fetchCats(query, filters);
}, [query, filters]); // ❌ This effect will run on every render
return (
/**
* Ommited for brevity
*/
);
};
正如我们在上一节中讨论的那样,React 只是对依赖数组中的项目进行浅比较,由于过滤器对象在每次渲染时都会创建,因此它永远不可能与上一次渲染中的过滤器对象在引用上相等。
所以正确的做法是——
React.useEffect(() => {
fetchCats(query, { height, color });
}, [query, height, color]); // ✅ The effect will now run only when one of these props changes
同样的情况也适用于像这样传播依赖项——
React.useEffect(() => {
/**
* Ommited for brevity
*/
}, [...filterArray, query]); // ❌ This effect would also run on every render
如果你使用ESLint作为代码检查工具,那么你绝对应该安装eslint-plugin-react-hooks,它会对你发出警告,甚至会自动修复问题(如果启用了保存时自动修复),以解决人们在使用这些钩子时通常犯的大多数错误。
如果您对useEffectuseEffect的工作原理以及依赖数组如何影响效果更感兴趣,那么您绝对应该看看Dan Abramov的《useEffect 完全指南》。
如果你读到了这里,我希望你从这篇文章中学到了新知识。如果学到了,请在推特上分享。
文章来源:https://dev.to/psuranas/javascript-tips-for-react-developers-592b