首页 > 解决方案 > 在 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>
  );
};

标签: next.jsreact-context

解决方案


这就是我最终做的事情:

  • 使用 getStaticProps 渲染应用程序renderToString
  • 用于useRef上下文中的状态而不是useState
  • 这样做的原因是因为renderToString只渲染初始状态。因此,如果您使用 更新 Context useState,它将不会捕获后续渲染
  • 出于上述原因更新组件初始化的上下文
  • 向上下文传递一个“逃生舱”——我们可以调用一个函数来获取在初始渲染时计算的状态

是的,整个事情看起来就像一个巨大的黑客!:-) 我不确定 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,
    },
  };
};

推荐阅读