Knowledge Base/Next.js App Router Infinite Load Issue

Next.js App Router Infinite Load Issue

known issueDeveloperNextJS app routerNextJS

Since the release of React 19 and Next.js 15, we’ve observed a certain scenario with client components that can result in a page infinitely spinning and never load.

There is a currently an issue open in the React repository to address this issue: https://github.com/facebook/react/pull/32316.

The core of this issue is marking entire Uniform components as client components with use client.

Following Next.js best practices outlined here: https://nextjs.org/docs/app/getting-started/server-and-client-components#reducing-js-bundle-size, it is likely not the best choice to mark entire Uniform components with use client. Try to push client components as far down in the tree as possible and only pass parameters from the composition to the client component that are needed to power that specific functionality. Any parameters that are passed from a server to client component will increase page size.

Scenarios

Scenario 1

Suppose you have a component that has some client functionality, simulated by this copy to clipboard button:

"use client"; import { ComponentProps, UniformText, } from "@uniformdev/canvas-next-rsc/component"; export const HeroComponent = ({ title, component, context, }: ComponentProps<HeroProps>) => { return ( <> <UniformText component={component} context={context} parameterId="title" as="h1" className="title" placeholder="Enter hero title" /> <button onClick={() => { navigator.clipboard.writeText(title); alert("Title copied to clipboard"); }} > Copy Title </button> </> ); }; export type HeroProps = { title: string; description: string; };

The whole component doesn’t have client side functionality, only the button. We can isolate this functionality by extracting the button to a new file and just passing the required parameters.

button.tsx

"use client"; export const Button = ({ text }: { text: string }) => { return ( <button onClick={() => { navigator.clipboard.writeText(text); alert("Title copied to clipboard"); }} > Copy Title </button> ); };

hero.tsx

"use client"; import { ComponentProps, UniformText, } from "@uniformdev/canvas-next-rsc/component"; import { Button } from "./button"; export const HeroComponent = ({ title, component, context, }: ComponentProps<HeroProps>) => { return ( <> <UniformText component={component} context={context} parameterId="title" as="h1" className="title" placeholder="Enter hero title" /> <Button text={title} /> </> ); }; export type HeroProps = { title: string; description: string; };

Structuring the component this way will minimize the amount of data that gets sent across the server and client boundary.

Scenario 2

If you have a component that has multiple interactive elements and there could be come dependencies between the client components, you can explore wrapping the component in a context and accessing values from it:

copy-context.tsx

"use client"; import { createContext, useContext, useState } from "react"; export type CopyContextProps = { text: string; interest: string | null; setInterest: (interest: string) => void; }; export const CopyContext = createContext<CopyContextProps>({ text: "", interest: null, setInterest: () => {}, }); export const CopyContextProvider = ({ children, text, }: { children: React.ReactNode; text: string; }) => { const [interest, setInterest] = useState<string | null>(null); return ( <CopyContext.Provider value={{ text, interest, setInterest }}> {children} </CopyContext.Provider> ); }; export const useCopyContext = () => { return useContext(CopyContext); };

Then we can have individual components which use this context:

interest-picker.tsx

import { useCopyContext } from "./copy-context"; export const InterestPicker = () => { const { interest, setInterest } = useCopyContext(); return ( <select value={interest ?? ""} onChange={(e) => setInterest(e.target.value)} > <option value="">Select an interest</option> <option value="react">React</option> <option value="vue">Vue</option> <option value="angular">Angular</option> <option value="svelte">Svelte</option> </select> ); };

button.tsx

"use client"; import { useCopyContext } from "./copy-context"; export const Button = () => { const { text, interest } = useCopyContext(); return ( <button onClick={() => { let textToCopy = text; if (interest) { textToCopy = text + " and I'm interested in " + interest; } navigator.clipboard.writeText(textToCopy); alert("Text copied to clipboard"); }} > Copy Title </button> ); };

Then the Uniform component would remain a server component, only pass parameters that are needed to the client context:

hero.tsx

import { ComponentProps, UniformText, } from "@uniformdev/canvas-next-rsc/component"; import { Button } from "./button"; import { CopyContextProvider } from "./copy-context"; import { InterestPicker } from "./interest-picker"; export const HeroComponent = ({ title, component, context, }: ComponentProps<HeroProps>) => { return ( <CopyContextProvider text={title}> <UniformText component={component} context={context} parameterId="title" as="h1" className="title" placeholder="Enter hero title" /> <InterestPicker /> <Button /> </CopyContextProvider> ); }; export type HeroProps = { title: string; description: string; };

Scenario 3

If all else fails and the component can not easily be reworked, pull the entire contents of the component into a new file and mark that component as use client and then use the Uniform component to just pass data to the client component:

hero-client.tsx

"use client"; import { ComponentProps, UniformText, } from "@uniformdev/canvas-next-rsc/component"; export const HeroClientComponent = ({ title, component, context, }: Pick<ComponentProps<HeroProps>, "title" | "component" | "context">) => { return ( <> <UniformText component={component} context={context} parameterId="title" as="h1" className="title" placeholder="Enter hero title" /> <button onClick={() => { navigator.clipboard.writeText(title); alert("Title copied to clipboard"); }} > Copy Title </button> </> ); }; export type HeroProps = { title: string; description: string; };

The Uniform component would look like this:

hero.tsx

import { ComponentProps } from "@uniformdev/canvas-next-rsc/component"; import { HeroClientComponent, HeroProps } from "./hero-client"; export const HeroComponent = ({ title, component, context, }: ComponentProps<HeroProps>) => { return ( <HeroClientComponent title={title} component={component} context={context} /> ); };

The scenarios are presented in preference for solving this issue. The end goal is ultimately just reducing the amount of data passing between the server/client boundary.

Published: June 2, 2025