next.js - 在 ComponentDidMount 之后将 React.Context 传递给 Nextjs?
问题描述
我有一个问题,我有一个简单的 React.Context在所有组件安装后填充。问题是因为它发生在挂载之后,nextjs 在初始渲染时看不到这个数据,所以有明显的闪烁。
这是设置上下文的简单组件:
export const SetTableOfContents = (props: { item: TableOfContentsItem }) => {
const toc = useContext(TableOfContentsContext);
useEffect(() => {
// Updates the React.Context after the component mount
// (since useEffects run after mount)
toc.setItem(props.item);
}, [props.item, toc]);
return null;
};
这是 React.Context。它使用 React 状态来存储 TOC 项。
export const TableOfContentsProvider = (props: {
children?: React.ReactNode;
}) => {
const [items, setItems] = useState<TableOfContents["items"]>([]);
const value = useMemo(() => {
return {
items,
setItem(item: TableOfContentsItem) {
setItems((items) => items.concat(item));
},
};
}, [items]);
return (
<TableOfContentsContext.Provider value={value}>
{props.children}
</TableOfContentsContext.Provider>
);
};
目前,无法在挂载前设置 React.Context,因为 React 会发出警告---渲染时无法更新状态。
我能想到的唯一解决方法是使用React.state以外的东西作为 React.Context 状态——这样组件可以随时更新它。但是这种方法的问题是上下文消费者将不再知道项目已更改(因为更新存在于 React 生命周期之外)!
那么如何将初始的 React.Context 放入初始的 SSR 渲染中呢?
const items = [];
export const TableOfContentsProvider = (props: {
children?: React.ReactNode;
}) => {
const value = useMemo(() => {
return {
items,
setItem(item: TableOfContentsItem) {
items[item.index] = item;
},
};
// this dep never changes.
// when you call this function, values never change
}, [items]);
return (
<TableOfContentsContext.Provider value={value}>
{props.children}
</TableOfContentsContext.Provider>
);
};
解决方案
这就是我最终做的事情:
- 使用 getStaticProps 渲染应用程序
renderToString
- 用于
useRef
上下文中的状态而不是useState
- 这样做的原因是因为
renderToString
只渲染初始状态。因此,如果您使用 更新 ContextuseState
,它将不会捕获后续渲染 - 出于上述原因更新组件初始化的上下文
- 向上下文传递一个“逃生舱”——我们可以调用一个函数来获取在初始渲染时计算的状态
是的,整个事情看起来就像一个巨大的黑客!:-) 我不确定 React.Context 是否与 SSR 配合得很好 :(
export const TableOfContentsProvider = (props: {
initialItems?: TableOfContentsItem[];
setItemsForSSR?: (items: TableOfContentsItem[]) => void;
children?: React.ReactNode;
}) => {
// use useRef for the reasons mentioned above
const items = useRef(props.initialItems || []);
// Client still needs to see updates, so that's what this is for
const [count, setCount] = useState(0);
const { setItemsForSSR } = props;
const setterValue = useMemo(
() => ({
setItem(item: TableOfContentsItem) {
if (!items.current.find((x) => x.id === item.id)) {
items.current.push(item);
items.current.sort((a, b) => a.index - b.index);
setCount((count) => count + 1);
setItemsForSSR?.(items.current);
}
},
}),
[setItemsForSSR]
);
const stateValue = useMemo(() => ({ items: items.current, count }), [count]);
return (
<TableOfContentsSetterContext.Provider value={setterValue}>
<TableOfContentsStateContext.Provider value={stateValue}>
{props.children}
</TableOfContentsStateContext.Provider>
</TableOfContentsSetterContext.Provider>
);
};
interface TableOfContentsSetterWorkerProps {
item: TableOfContentsItem;
setItem: (item: TableOfContentsItem) => void;
}
export class TableOfContentsSetterWorker extends React.Component<
TableOfContentsSetterWorkerProps,
{}
> {
constructor(props: TableOfContentsSetterWorkerProps) {
super(props);
// Need to do this on init otherwise renderToString won't record it
props.setItem(props.item);
}
render() {
return null;
}
}
/**
* Usage: use this as a child component when the parent needs to set the TOC.
*
* Exists so that a component can set the TOC without triggering
* an unnecessary render on itself.
*/
export function TableOfContentsSetter(props: { item: TableOfContentsItem }) {
const { setItem } = useContext(TableOfContentsSetterContext);
return <TableOfContentsSetterWorker item={props.item} setItem={setItem} />;
export const getStaticProps = async () => {
let initialTableOfContents: TableOfContentsItem[] = [];
const getItems = (items: TableOfContentsItem[]) => {
initialTableOfContents = [...items];
};
const app = () => (
<TableOfContentsProvider setItemsForSSR={getItems}>
<AppArticles />
</TableOfContentsProvider>
);
renderToString(app());
return {
props: {
initialTableOfContents,
},
};
};
推荐阅读
- javascript - 将给定的字符串拆分为数组
- android - 扩展以填充 RecyclerView 宽度的方形网格项目
- javascript - 单击新页面时更改活动类而不刷新
- java - 使用声明式客户端 Micronaut 将多个文件上传到 MultipartBody
- javascript - 如何将菜单更改为响应式引导菜单
- python - 如何将毫秒 Unix 时间戳转换为可读日期?
- android - Flutter_blue 和 Adafruit Bluefruit BLE 特征错误:找不到特征的 CCCD 描述符
- django - 如何在 django 模板中使用 if else
- android - 使用 share 2.0.0 包我想分享我的应用程序的链接,但我还没有发布我的应用程序来玩商店
- qt - 在 Qt 中使用 CMake 的主项目之前如何构建依赖库?