React Hooks 在 SSR 模式下常見問題及解決方案
服務(wù)端渲染(Server-Side Rendering),是指由服務(wù)側(cè)完成頁面的 HTML 結(jié)構(gòu)拼接的頁面處理技術(shù)。一般用于解決 SEO 問題和首屏加載速度問題。
由于 SSR 是在非瀏覽器環(huán)境執(zhí)行 JS 代碼,所以會(huì)出現(xiàn)很多問題。本文主要介紹 React Hooks 在 SSR 模式下常見問題及解決方案。
更多關(guān)于 SSR 的介紹可以看 UmiJS 的文檔《服務(wù)端渲染(SSR)[1]》。
問題一:DOM/BOM 缺失
SSR 是在 node 環(huán)境下運(yùn)行 React 代碼,而此時(shí) window、document、navigator 等全局屬性沒有。如果直接使用了這些屬性,就會(huì)報(bào)錯(cuò) window is not defined, document is not defined, navigator is not defined 等。
常見的錯(cuò)誤用法是在 Hooks 執(zhí)行過程中,直接使用了 document 等全局屬性。
- import React, { useState } from 'react';
- export default () => {
- const [state, setState] = useState(document.visibilityState);
- return state;
- }
解決方案
1.將訪問 DOM/BOM 的方法放在 useEffect/useLayoutEffect 中(服務(wù)端不會(huì)執(zhí)行),避免服務(wù)端執(zhí)行時(shí)報(bào)錯(cuò),例如:
- import React, { useState, useEffect } from 'react';
- export default () => {
- const [state, setState] = useState();
- useEffect(()=>{
- setState(document.visibilityState);
- }, []);
- return state;
- }
2.通過 isBrowser[2] 來做環(huán)境判斷
- import React, { useState } from 'react';
- function isBrowser() {
- return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
- }
- export default () => {
- const [state, setState] = useState(isBrowser() && document.visibilityState);
- return state;
- }
問題二 useLayoutEffect Warning
如果使用了 useLayoutEffect,在 SSR 模式下,會(huì)出現(xiàn)以下警告
- ⚠️ Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-SSR for common fixes.
解決方案
- 使用 useEffect 代替 useLayoutEffect(廢話)
- 根據(jù)環(huán)境動(dòng)態(tài)的指定是使用 useEffect 還是 useLayoutEffect。這是來自社區(qū)的一種 hack 解決方案,目前在 react-redux[3]、react-use[4]、react-beautiful-dnd[5] 均使用的這種方案。
- import { useLayoutEffect, useEffect } from 'react';
- const useIsomorphicLayoutEffect = isBrowser() ? useLayoutEffect : useEffect;
- export default useIsomorphicLayoutEffect;
總結(jié):寫 Hooks 時(shí)需要注意
1.不要在非 useEffect/useLayoutEffect 中,直接使用 DOM/BOM 屬性
2.在非 useEffect/useLayoutEffect 使用 DOM/BOM 屬性時(shí),使用 isBrowser 判斷是否在瀏覽器環(huán)境執(zhí)行
3.如果某個(gè) Hooks 需要接收 DOM/BOM 屬性,需要支持函數(shù)形式傳參。以 ahooks 的 useEventListener 舉例,必須支持函數(shù)形式來指定 target 屬性。
- import React, { useState } from 'react';
- import { useEventListener } from 'ahooks';
- export default () => {
- const [value, setValue] = useState(0);
- const clickHandler = () => {
- setValue(value + 1);
- };
- useEventListener(
- 'click',
- clickHandler,
- {
- - target: document.getElemenetById('click-btn')
- + target: () => document.getElemenetById('click-btn')
- }
- );
- return (
- <button id="click-btn" type="button">
- You click {value} times
- </button>
- );
- };
4.使用 useIsomorphicLayoutEffect 來代替 useLayoutEffect
參考資料
- fix: useDocumentVisiblility support SSR[6]
- UmiJS 服務(wù)端渲染[7]
- useLayoutEffect and SSR[8]
參考資料
[1]服務(wù)端渲染(SSR):
https://umijs.org/zh-CN/docs/SSR#服務(wù)端渲染(SSR)
[2]isBrowser:
https://github.com/alibaba/hooks/blob/master/packages/hooks/src/utils/canUseDom.ts
[3]react-redux:
https://github.com/reduxjs/react-redux/blob/d16262582b2eeb62c05313fca3eb59dc0b395955/src/components/connectAdvanced.js#L40
[4]react-use:
https://github.com/streamich/react-use/blob/master/src/useIsomorphicLayoutEffect.ts
[5]react-beautiful-dnd:
https://github.com/atlassian/react-beautiful-dnd/blob/master/src/view/use-isomorphic-layout-effect.js
[6]fix: useDocumentVisiblility support SSR:
https://github.com/alibaba/hooks/pull/935/files
[7]UmiJS 服務(wù)端渲染:
https://umijs.org/zh-CN/docs/SSR#window-is-not-defined-document-is-not-defined-navigator-is-not-defined
[8]useLayoutEffect and SSR:
https://medium.com/@alexandereardon/uselayouteffect-and-SSR-192986cdcf7a
本文轉(zhuǎn)載自微信公眾號(hào)「前端技術(shù)磚家」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端技術(shù)磚家公眾號(hào)。