Home / Articles

Dropping the Provider Wrapper: Rendering Context from Server Components

Learn how React 19.3 lets a Server Component render a client Context directly, how Flight serializes it, and which limits on reading and values still apply.

Publisher: REACTAPP.TOP7 min read1602 words

This article is published in English.

Sources: React 19.3: Render a Context Directly in a Server Component, No Provider Wrapper

Almost every React Server Components codebase contains a tiny 'use client' file whose only job is to create a Context and export a provider component that forwards a single prop. That wrapper existed because a Server Component was not allowed to render the Context itself. React 19.3 removes that restriction, so the server can now render the Context element directly and the wrapper can be deleted.

This guide walks through the old pattern, the new one, what the Flight protocol actually does under the hood, and the gotchas that decide whether the change works in your framework today. The examples need a real Server Components runtime (a framework with RSC bundler support and a server), so treat them as complete reference code rather than something to paste into a sandbox. For background on how the server and client halves of an RSC app fit together, see our overview of the architecture behind React Server Components.

The old pattern: a component that only forwards a prop

Server Components cannot call createContext, because a Context is client-side state machinery. Yet the client tree below them regularly needs data the server already has, such as the signed-in user, a tenant's feature flags or the locale of the request. The only route across the boundary was the props of a Client Component, and that Client Component then rendered the provider.

The client module starts with the usual directive and import.

'use client';
import { createContext } from 'react';

It then creates the Context and exports a provider whose body does nothing except pass currentUser through as value.

export const UserContext = createContext(null);// exists only to forward one prop
export function UserProvider({ currentUser, children }) {
  return (
    <UserContext value={currentUser}>
      {children}
    </UserContext>
  );
}

The Server Component side

The layout, a Server Component, imports the wrapper instead of the Context.

import { UserProvider } from './user-context';

It fetches the user on the server and hands it to the wrapper as a prop.

export async function Layout({ children }) {
  const currentUser = await getCurrentUser();
  return (
    <UserProvider currentUser={currentUser}>
      {children}
    </UserProvider>
  );
}

If you tried to skip the wrapper on React 19.2, you hit a confusing runtime error that begins with "Element type is invalid" and then complains that a lazy element type resolved to a Context rather than to a class or function. It looks like a bundler problem, but the cause is in the renderer. A Context imported across the client boundary reaches the server renderer as a client reference that is resolved lazily, and 19.2 could only turn those references into components. A Context object was never an accepted result.

The new pattern: render the Context itself

On 19.3, the client module shrinks to the directive, the import and a single export.

'use client';
import { createContext } from 'react';

There is no component anymore, just the Context object.

export const UserContext = createContext(null);

Rendering it from the layout

The layout now imports UserContext directly.

import { UserContext } from './user-context';

The JSX is the same shape as before, except that the element is the Context and the prop is called value.

export async function Layout({ children }) {
  const currentUser = await getCurrentUser();
  return (
    <UserContext value={currentUser}>
      {children}
    </UserContext>
  );
}

Consuming it on the client

Nothing changes for consumers. A Client Component anywhere below the layout imports the same Context and reads it with use.

'use client';
import { use } from 'react';
import { UserContext } from './user-context';

Here the avatar menu pulls the user from the Context and renders the image.

export function AvatarMenu() {
  const user = use(UserContext);
  return <img src={user.avatar} alt={user.name} />;
}

Pay attention to the React 19 syntax in the layout: it renders <UserContext value> rather than <UserContext.Provider value>. The .Provider form keeps working on the client, but the server renders the Context object itself, so stick with the shorter form in server code.

What Flight does, and what this feature is not

When a Server Component renders <UserContext value={currentUser}>, no Context logic runs on the server at all. The Flight serializer emits an element typed as a client reference, pointing at the module user-context.js and its UserContext export. The element carries { value: currentUser, children } as its props. Those props are serialized exactly like any other props crossing to the client. Once the browser loads that module, the client reference turns into the actual Context object, and React sets up a provider holding the transmitted value. The renderer fix, tracked in facebook/react #35675, taught the lazy-type code path to accept a Context object in addition to functions, memo and forwardRef.

Two consequences follow from that design.

The value must be serializable

The Context value obeys the same rules as any prop crossing into the client. Allowed types include primitives such as strings and numbers, plain objects and arrays, Date, Map and Set instances, promises, JSX elements and Server Functions. Ordinary functions, class instances and anything with a custom prototype are not. A plain currentUser record works; a UserService instance with methods does not. The 'use client' reference documents the full list.

It is not server-side Context

The pull request is explicit that you still cannot call use(Context) inside a Server Component. Server Components have no way to read a Context, because no provider exists on the server for them to read. The feature is purely a shorter path for handing a value to the client tree.

Why removing the wrapper is worth it

Each shared value used to cost one file and one component. That seems cosmetic until you count them: an app with contexts for the user, theme, locale, feature flags, tenant and analytics carries six nearly identical wrappers, and each is an opportunity to forget a prop or mistype a default. With the new form, the provider sits right where the data is fetched, and the mental model of "which parts run on the server and which on the client" loses one special case.

Community reaction to the change was mostly relief rather than excitement. Developers commented that it felt odd for this to have been unsupported in the first place, since a Context value is really just data held in memory. That intuition is correct, and it explains why the fix in React itself was small.

Gotchas before you migrate

  • Your framework must ship the 19.3 Flight packages. The change lives in react-server-dom-webpack, -turbopack and -parcel plus the reconciler, not only in react. Check the React version your framework pins; a mismatched canary produces the same "Element type is invalid" error described earlier.
  • Define the Context in a file marked 'use client'. A Context defined in server code cannot be rendered this way, because the whole mechanism depends on the object existing in the client bundle.
  • Reading on the server is still impossible. A Server Component that needs the value should receive it as an argument or fetch it itself; use(Context) throws there.
  • Only serializable values work. If the old value was an instance of a class or had methods attached, the wrapper was hiding it, and the problem now appears as a serialization error.
  • A promise is a legal value. Since promises serialize across the boundary, writing <UserContext value={getCurrentUser()}> without await streams the value, and consumers must unwrap it twice with use(use(UserContext)). That can be a useful streaming technique, but it is also an easy mistake to make by dropping an await.
  • Default values remain a client concern. The default passed to createContext(null) applies wherever no provider is above a consumer, and rendering the provider on the server does not alter that.

Staying on the wrapper for now

If your framework has not adopted 19.3 yet, keep the wrapper. The UserProvider pattern works on every React 19 release and in every framework. When you do upgrade, the migration is mechanical:

  • Delete the wrapper function from the client module.
  • Import the Context in the Server Component instead of the wrapper.
  • Rename the forwarded prop to value.

You can migrate file by file as your framework moves to 19.3. There is no need to convert everything at once, and once both forms work there is no runtime difference between them.

Key takeaways

  • On React 19.3, a Server Component can render a Context exported from a 'use client' module, so single-purpose provider wrappers become unnecessary.
  • Under the hood, Flight sends a client reference plus serialized props; no Context behavior runs on the server.
  • The value must satisfy the normal serialization rules, and Server Components still cannot read Context.
  • Confirm that your framework bundles matching Flight packages before deleting wrappers, and watch for accidental promise values.
  • For details, read the React 19.3 release notes on react.dev and the Server Components reference.