面试官问你的React是不是健壮,你怎么回答?
为什么编写一个在不同环境和场景中都能良好运行的React库是一个挑战?
当我Review同事们的代码时,发觉创建一个健壮的React库是多么具有挑战性。就算是我们一般都会无脑拷贝的谷歌大神代码,有时也会引发问题,比如NextJS的hydration mismatching错误。参考这个、 这个、 和这个。
React库是否安全?这个看似简单的问题在React社区内却异常困难。
在本文中,我将分享一些编写安全的React库的技巧和最佳实践,这些库具有服务器端渲染(SSR)安全、并发渲染安全和最优依赖项。为了说明这些原则的实际应用,我们将review一个简单的useLocalStorage
钩子的实现。
免责声明:虽然我一般认同自己是一个Vue开发者,但我的日常工作涉及维护一个Next.js应用程序。如果本文中的任何信息已过时或不正确,请纠正我。
useLocalStorage简介
useLocalStorage
是一个自定义的React Hook,允许你在React组件中读取和写入localStorage。localStorage是浏览器的API,允许你在浏览器中存储键值对的数据。这个Hook将组件的状态与localStorage中存储的数据同步。需要注意的是,在本文中为了简洁起见,如果localStorage的更改由其他组件或浏览器标签触发,该Hook不会刷新视图。
useLocalStorage
Hook接受两个参数:key
和initialValue
。它返回一个包含两个值的数组:存储的值和一个设置器函数。你可以使用存储的值和设置器函数来读取和写入localStorage,就像使用useState
一样。
// 一个简单的自定义Hook,在React组件中使用localStorage
function useLocalStorage(key, initialValue) {
// 使用useState来在本地状态中存储值
const [storedValue, setStoredValue] = useState(() => {
// 从localStorage中获取值
const item = localStorage.getItem(key);
// 将值解析为JSON格式
return item ? JSON.parse(item) : initialValue;
});
// 使用useEffect在值更改时更新localStorage
useEffect(() => {
// 将值转换为JSON字符串
const valueToStore = JSON.stringify(storedValue);
// 将值存储到localStorage中
localStorage.setItem(key, valueToStore);
}, [key, storedValue]);
// 从Hook中返回值和设置器函数
return [storedValue, setStoredValue];
}
上述实现故意比较简单。接下来我们将审查它的局限性,并做出相应的改进。
什么是SSR安全?
SSR代表服务器端渲染(Server-Side Rendering),这是一种在服务器上渲染React组件并将HTML发送到浏览器的技术。SSR可以提高应用的性能、SEO和可访问性。然而,对于库的作者来说,它也需要特别考虑,如避免使用特定于浏览器的API、处理hydration(水合)和支持流式处理。
不符合SSR安全的代码可能会导致服务器上的Node.js渲染错误或客户端的水合不匹配错误。这些错误可能导致应用运行缓慢,将事件处理程序附加到错误的元素,甚至停止服务器运行。
为了编写一个符合SSR安全的库,你应该遵循以下准则:
- 避免使用特定于浏览器的API,如
window
、document
或localStorage
。这些API在服务器上不可用,可能会导致错误或不一致性。而是使用功能检测或回退来处理不同的环境。例如:
// 允许我们在服务器端使用window
const safeWindow = (typeof window === 'undefined')
? {
addEventListener() {},
removeEventListener() {},
}
: window;
- 避免在服务器端和客户端上渲染不同的视图。有时你可能需要在服务器端和客户端上渲染不同的内容,就像在
useLocalStorage
中的情况一样。在这种情况下,你必须确保服务器和客户端渲染相同的初始内容,并且客户端的内容使用useEffect进行适当的更改。你可以在React文档中关于在服务器端和客户端上显示不同内容的部分了解更多信息。
我们之前的简单实现是不具备SSR安全性的。
首先,它直接使用localStorage而没有提供任何服务器实现,这可能导致服务器渲染失败。
其次,如果我们只在浏览器端从localStorage中读取数据,可能会导致服务器和客户端渲染不同的内容。我们需要解决这些问题,使其具备SSR安全性。
// SSR安全版本
function useLocalStorage(key, initialValue) {
// useState始终返回initialValue,保持一致性
const [storedValue, setStoredValue] = useState(initialValue);
// 在effect处理函数中从localStorage中获取值
useEffect(() => {
const item = localStorage.getItem(key);
if (item) {
setStoredValue(JSON.parse(item));
} else {
setStoredValue(initialValue);
}
}, [key, initialValue]);
// 使用useEffect在值更改时更新localStorage
useEffect(() => {
// 将值转换为JSON字符串
const valueToStore = JSON.stringify(storedValue);
// 将值存储到localStorage中
localStorage.setItem(key, valueToStore);
}, [key, storedValue]);
// 从Hook中返回值和设置器函数
return [storedValue, setStoredValue];
}
请注意,随着引入React Server Components(RSC),确保SSR安全性变得更加复杂。然而,在本文中我们不会深入讨论RSC的安全性。
什么是并发渲染安全?
并发渲染(以前称为Concurrent Mode)是React中的一项新功能,它通过同时渲染多个组件来实现更好的用户体验。然而,并发渲染也为库的作者引入了新的限制和潜在的陷阱,例如避免副作用、可变状态和阻塞渲染。
编写一个并发渲染安全的库应该遵循以下准则:
-
使用函数组件和Hooks而不是类组件和生命周期方法。
-
正确使用
useEffect
,确保:
- 使用
useEffect
Hook执行副作用(副作用可能比你想象得多)。 - 在Hook的返回函数中清理任何资源。
- 在执行同步的DOM读取时使用
useLayoutEffect
。 - 对于只能执行一次的逻辑(如初始化外部库),需要额外挑战依赖项。
-
避免使用全局或共享的可变状态,比如变量、对象或数组。而是使用
useState
或useReducer
来使用本地状态,或者使用useContext
来使用上下文。 -
在渲染过程中,除了初始化之外,不要对
ref.current
进行写入或读取。这样做可能导致组件行为的不可预测性。
现在,让我们回顾一下我们之前讨论的代码,并确定并发渲染是否安全。
原始实现仅在组件更新并将更改应用于DOM后才更新localStorage
,因为更新发生在useEffect
钩子内部。作为一个库的作者,假设用户代码是并发渲染安全的是不安全的。具体来说,用户代码可能在渲染过程中从localStorage
读取,导致库代码和用户代码之间的不一致性。为了解决这个问题,我们需要在调用setStoredValue
之前更新localStorage
。
第二个问题更加微妙:我们同步从localStorage
读取值,但useEffect
不能保证同步更新。这可能导致用户代码中意外的内容闪烁。由于React的并发渲染可能会根据设备性能和渲染时间延迟渲染,因此闪烁可能会或可能不会复现。作为库的作者,最好提供更可靠的渲染。
// 并发渲染安全版本
function useLocalStorage(key, initialValue) {
// useState始终以initialValue一致的方式返回
const [storedValue, setStoredValue] = useState(initialValue);
// 在useLayoutEffect处理函数中从localStorage中获取值
useLayoutEffect(() => {
const item = localStorage.getItem(key);
if (item) {
setStoredValue(JSON.parse(item));
} else {
setStoredValue(initialValue);
}
}, [key, initialValue]);
// 使用useEffect在值更改时更新localStorage
useEffect(() => {
// 将值转换为JSON字符串
const valueTo
Store = JSON.stringify(storedValue);
// 将值设置到localStorage中
localStorage.setItem(key, valueToStore);
}, [key]);
// 使用useCallback在值更改时更新localStorage
const setValue = useCallback((value) => {
// 将值转换为JSON字符串
const valueToStore = JSON.stringify(value);
// 将值设置到localStorage中
localStorage.setItem(key, valueToStore);
setStoredValue(value);
}, [key, setStoredValue]);
// 从Hook中返回值和设置器函数
return [storedValue, setValue];
}
最后,让我们讨论一下最佳依赖项选择。
最优依赖项
在React中,钩子(hooks)具有依赖数组,该数组反映了在渲染过程中的响应式值更新。然而,并不是所有的值都需要始终保持最新状态。例如,在useLocalStorage
中,如果initialValue
发生变化,我们不希望重新渲染。这是因为initialValue
表示当键在本地存储中不存在时要渲染的回退值。如果键保持不变,就没有必要重新渲染组件。
一个高性能的库应该通过优化依赖项来跳过不必要的视图更新。这个概念可能会让有经验的React用户想起Dan Abramov的著名博文,介绍了如何通过React hook来声明式地使用setInterval
(链接:overreacted.io/making-seti…
以下是优化过的useLocalStorage
钩子的版本,它通过使用useRef
来存储initialValue
的最新值,从而跳过了依赖项:
// 并发渲染安全且优化过的版本
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(initialValue);
const initialValueRef = useRef(initialValue);
useLayoutEffect(() => {
const item = localStorage.getItem(key);
if (item) {
setStoredValue(JSON.parse(item));
} else {
setStoredValue(initialValueRef.current);
}
}, [key]);
useEffect(() => {
const valueToStore = JSON.stringify(storedValue);
localStorage.setItem(key, valueToStore);
}, [key, storedValue]);
const setValue = useCallback((value) => {
const valueToStore = JSON.stringify(value);
localStorage.setItem(key, valueToStore);
setStoredValue(value);
}, [key, setStoredValue]);
return [storedValue, setValue];
}
总结
编写一个安全的React库可能会面临一些挑战,但也是有回报的。通过遵循本文中讨论的提示和最佳实践,您可以创建一个在SSR、并发渲染中安全的React库,并具有最佳的依赖项管理。请记住考虑不同的环境,避免使用特定于浏览器的API,正确处理hydration,并优化依赖项以为库的用户提供最佳的体验。
但这并不是发布现代JavaScript库的终点!我们仅仅触及了一些React特定的问题。还有其他关键问题需要解决,比如浏览器兼容性、数据完整性,以及模块系统(模块系统可能是最烦人的了)。
感谢您的阅读!如果您觉得这篇博文有用,请在Medium上关注我。
转载自:https://juejin.cn/post/7251182449400299577