在ComponentDidMount之后将React.Context传递给Nextjs吗?

时间:2020-05-11 13:44:18

标签: next.js react-context

我有一个问题,我有一个简单的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.Context状态使用 React.state以外的其他东西-这样组件可以随时更新它。但是,这种方法的问题在于,上下文消费者将不再知道项目已更改(因为更新存在于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>
  );
};

1 个答案:

答案 0 :(得分:0)

这就是我最终要做的:

  • 使用renderToString在getStaticProps中呈现应用程序
  • 使用useRef代替上下文中的状态,useState
  • 这样做的原因是因为renderToString仅呈现初始状态。因此,如果您使用useState更新上下文,它将无法捕获后续渲染
  • 出于上述原因更新组件初始化的上下文
  • 将Context作为“转义图案”传递给我们-我们可以调用该函数来获取在初始渲染中计算出的状态

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