Skip to content
Get the beta

Search the docs

Start with the beta
Installation
Build your first page
Thinking in Turtlekit
Apps, websites and embeds
Components or native elements
Colour
Typography
Layout
Spacing, icons and shape
Components
Root
Layout
Typography
Code block
Button
Icon button
Button group
Call to action
Toolbar
Field
Text field
Text area
Select
Combobox
Date input
Save bar
Newsletter signup
File upload
Calendar
One-time code
Checkbox
Radio group
Switch
Segmented control
Toggle
Slider
Badge
Card
Data list
Item
Icon tile
Num
Time
Metric
Accordion
FAQ
Avatar
Media
Scroll area
Resizable
Carousel
Image slider
Logo cloud
Testimonials
Table
Data table
Chart
Site header
Site footer
Tabs
Page header
Breadcrumb
Pagination
Sidebar
Navigation menu
Callout
Error summary
Toast
Empty state
Skeleton
Progress
Spinner
Tooltip
Dialog
Popover
Hover card
Dropdown menu
Context menu
Menubar
Command
Elements area
Visually hidden
Error boundary
Design rules
Staying within the kit
Layout
Icons
Images and media
Typography
Readability
Colour
Composition
Accessibility
Elements
Motion
Interaction
AI pitfalls
Pricing
React design patterns
Single responsibility in React components
Presentational and container components in React
Children, function children and render props in React
The variant pattern in React
Compound components in React
Higher-order components in React
State colocation in React
Lifting state up in React
Deriving state during render
One status instead of many booleans
Adjusting state when a prop changes
Reading context with use() in React 19
Context selectors in React
Custom hooks and hook composition in React
One effect per concern in React
The latest ref pattern in React
Effect Events with useEffectEvent in React
Detecting the first render in React
Exposing methods with useImperativeHandle in React
Debouncing input in React
useDeferredValue in React
Loading components on demand in React
Virtualising long lists in React
Guard clauses and early returns in React
Skeleton placeholders in React
Rendering partial data in React
Empty states in React
Error boundaries in React
Reading async data with use() and Suspense in React
useTransition and Actions in React
Optimistic UI updates with useOptimistic
useActionState and form Actions in React
useSyncExternalStore in React
Ref as a prop and ref cleanup in React 19
Accessible ids with useId in React
Portals in React
Roving focus and keyboard navigation in React
Controlled and uncontrolled components in React
The state reducer and prop getters in React
Headless components and the provider pattern in React
The polymorphic as prop in React

Appearance

These change every page and preview on this site.

Reading context with use() in React 19

Turtlekit Editorial Team

A reviewer left one comment on the invoice page’s actions: return early for a void invoice, before anything else runs. The change moved a single line to the top of the component, and the lint failed on it. The two context reads below the new guard had become hooks called after an early return.

So the guard went back under the two reads, which a void invoice never uses. It is a small thing, and most components that read context have some version of it: the order of the code is set by the rules of hooks rather than by the decision the component makes.

React 19’s use lets the code follow the decision. It reads a context as useContext does, but it can be called inside a condition, a loop or after an early return, so each branch reads only the context it needs.

A guard that had to wait its turn

Here is the component the reviewer read:

// Before: both contexts read first, whatever the invoice
export function InvoiceActions({ invoice }: { invoice: Invoice }) {
  const permissions = useContext(PermissionsContext);
  const account = useContext(AccountContext);

  if (invoice.status === "void") return null;
  if (invoice.status !== "paid")
    return <button type="button">Send reminder</button>;
  if (!permissions.canRefund)
    return <p>Ask an owner to refund this invoice.</p>;
  if (!account) throw new Error("AccountContext needs a provider.");
  return (
    <button type="button">
      Refund {money(account, invoice.total)}
    </button>
  );
}

Both contexts are read for every invoice. A void invoice reads them and returns nothing; a reminder reads them and uses neither. Only the refund needs the account’s money format. With useContext the reads have to stay above every return, because a hook must run on every render, in the same order.

Nothing here is slow or broken. The cost is in reading: the top of the component claims two dependencies that most of its branches don’t have, and the guard that decides everything sits in the middle.

Read the context where it is used

A context read is a lookup: React finds the closest provider above the component and returns its value. use makes that lookup at the line that asks, which can be any line that runs during render.

invoice.statuscontext readvoidnoneopen, overduenonedraftPermissionspaidPermissionsAccount
Each branch reads only the context it uses, after the guards that decide which branch runs. With useContext, every row would read both.

One rule stays: use must be called while a component or a hook renders, never in an event handler or an effect. Inside that render, it goes wherever the decision needs it.

Rewriting the actions with use()

The rewrite starts with the contexts themselves, then moves the guards up and the reads down. The page provides both contexts at the end.

Defaults that fail safe

Each context’s default is what a component sees with no provider above it. Choose it as carefully as any other value:

export interface Permissions {
  canSend: boolean;
  canRefund: boolean;
}

// Outside a provider nobody may do anything: the default fails safe.
export const NO_PERMISSIONS: Permissions = {
  canSend: false,
  canRefund: false,
};
export const PermissionsContext =
  createContext<Permissions>(NO_PERMISSIONS);

export interface Account {
  locale: string;
  currency: string;
}

// No safe default for money, so a missing provider is an error.
export const AccountContext = createContext<Account | null>(null);

/** A plain function, not a hook, so it can sit in any branch. */
export function required<T>(value: T | null, name: string): T {
  if (value === null)
    throw new Error(`${name} needs a provider above this component.`);
  return value;
}

Permissions default to none, so a component rendered outside the provider, in a test or a new page, offers no action it shouldn’t. Money has no safe default, so the account context starts as null, and required turns a missing provider into an error that names it. Because required isn’t a hook, it can sit in any branch.

Guards first

The two cases that need no context return before anything is read:

if (invoice.status === "void") return null;
if (invoice.status === "open" || invoice.status === "overdue") {
  return (
    <button
      type="button"
      onClick={() => onAction("remind", invoice.id)}
    >
      Send reminder
    </button>
  );
}

Each branch reads what it needs

Below the guards, the permissions are read once, and the account only on the way to a refund:

const permissions = use(PermissionsContext);
if (invoice.status === "draft") {
  if (!permissions.canSend)
    return <p>Ask an owner or an admin to send this invoice.</p>;
  return (
    <button
      type="button"
      onClick={() => onAction("send", invoice.id)}
    >
      Send invoice
    </button>
  );
}
if (!permissions.canRefund)
  return <p>Ask an owner to refund this invoice.</p>;
const account = required(use(AccountContext), "AccountContext");
return (
  <button
    type="button"
    onClick={() => onAction("refund", invoice.id)}
  >
    Refund {money(account, invoice.total)}
  </button>
);

The reads now sit beside the code that uses them. A reader sees at once that only a refund needs the money format, and the comment the reviewer left is the component’s first line.

Providing both in React 19

In React 19 a context is its own provider, so the page renders <AccountContext value={account}> where React 18 needs <AccountContext.Provider>:

"use client";
// The invoice page provides both contexts, with React 19's <Context>
// as its own provider.
import { AccountContext, PermissionsContext } from "./account";
import { InvoiceActions } from "./invoice-actions";
import type { Account, Permissions } from "./account";
import type { Invoice, InvoiceAction } from "./invoice";

interface InvoicePageProps {
  invoice: Invoice;
  account: Account;
  permissions: Permissions;
  onAction: (action: InvoiceAction, invoiceId: string) => void;
}

export function InvoicePage({
  invoice,
  account,
  permissions,
  onAction,
}: InvoicePageProps) {
  return (
    <AccountContext value={account}>
      <PermissionsContext value={permissions}>
        <main>
          <h1>Invoice {invoice.id}</h1>
          <InvoiceActions invoice={invoice} onAction={onAction} />
        </main>
      </PermissionsContext>
    </AccountContext>
  );
}

The finished actions

"use client";
// The invoice page's actions. The guards come first, and each branch
// reads the context it needs at the point it needs it.
import { use } from "react";
import {
  AccountContext,
  PermissionsContext,
  money,
  required,
} from "./account";
import type { Invoice, InvoiceAction } from "./invoice";

interface InvoiceActionsProps {
  invoice: Invoice;
  onAction: (action: InvoiceAction, invoiceId: string) => void;
}

export function InvoiceActions({
  invoice,
  onAction,
}: InvoiceActionsProps) {
  if (invoice.status === "void") return null;
  if (invoice.status === "open" || invoice.status === "overdue") {
    return (
      <button
        type="button"
        onClick={() => onAction("remind", invoice.id)}
      >
        Send reminder
      </button>
    );
  }

  const permissions = use(PermissionsContext);
  if (invoice.status === "draft") {
    if (!permissions.canSend)
      return <p>Ask an owner or an admin to send this invoice.</p>;
    return (
      <button
        type="button"
        onClick={() => onAction("send", invoice.id)}
      >
        Send invoice
      </button>
    );
  }
  if (!permissions.canRefund)
    return <p>Ask an owner to refund this invoice.</p>;
  const account = required(use(AccountContext), "AccountContext");
  return (
    <button
      type="button"
      onClick={() => onAction("refund", invoice.id)}
    >
      Refund {money(account, invoice.total)}
    </button>
  );
}

A void invoice shows nothing; an open or overdue one offers “Send reminder”. A paid invoice offers an owner “Refund €1,200.00”, or “Refund 1.200,00 €” on a German account, and tells anyone else that an owner can refund it.

What use() changes, and what it doesn’t

use changes where a read can go. Almost everything else about context stays as it was.

The same re-renders as useContext

When a provider receives a different value, compared with Object.is, every component that reads it re-renders, and memo doesn’t prevent that. Keep provider values stable: pass values that change only when their contents do, or memoise the object you build. When a context’s value changes often, read Context selectors in React.

The freedom belongs to use, not to its wrappers

A custom hook that calls use is still a hook, and the rules of hooks apply to calling it: usePermissions() after an early return fails the lint just as useContext did. The freedom is in use itself. That is why the account’s guard is required, a plain function that takes a value, rather than a useAccount hook: it can follow the read into any branch.

Two contexts, or one

Permissions and the account could share one context. Keeping them apart lets each branch read only what it uses, and means a change to one, such as a role changing mid-session, re-renders only the components that read it. Split contexts by what changes together, not by what sits together on the screen.

Hidden, or explained

The branches decide what people can’t do as well as what they can. An action that never applies, such as refunding a void invoice, is left out. An action someone would expect and can’t take, such as a member refunding a paid invoice, is explained in words, with who can do it. A disabled button with no reason is the version to avoid: it can’t take focus, so someone moving through the page by keyboard passes it without learning why it is there.

Testing with and without a provider

Render through the page to test with real providers, and render the actions alone to test the defaults:

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test, vi } from "vitest";
import { InvoiceActions } from "./invoice-actions";
import { InvoicePage } from "./invoice-page";
import type { Account, Permissions } from "./account";
import type { Invoice } from "./invoice";

const PAID: Invoice = {
  id: "INV-0046",
  status: "paid",
  total: 120000,
};
const EUROPE: Account = { locale: "en-GB", currency: "EUR" };
const OWNER: Permissions = { canSend: true, canRefund: true };
const MEMBER: Permissions = { canSend: true, canRefund: false };

test("an owner can refund a paid invoice", async () => {
  const onAction = vi.fn();
  render(
    <InvoicePage
      invoice={PAID}
      account={EUROPE}
      permissions={OWNER}
      onAction={onAction}
    />,
  );
  await userEvent.click(
    screen.getByRole("button", { name: "Refund €1,200.00" }),
  );
  expect(onAction).toHaveBeenCalledWith("refund", "INV-0046");
});

test("the account's locale decides how the amount reads", () => {
  const germany: Account = { locale: "de-DE", currency: "EUR" };
  render(
    <InvoicePage
      invoice={PAID}
      account={germany}
      permissions={OWNER}
      onAction={() => {}}
    />,
  );
  expect(
    screen.getByRole("button", { name: /^Refund 1\.200,00\s€$/ }),
  ).toBeVisible();
});

test("a member is told who can refund; void offers none", () => {
  const { rerender } = render(
    <InvoicePage
      invoice={PAID}
      account={EUROPE}
      permissions={MEMBER}
      onAction={() => {}}
    />,
  );
  expect(
    screen.getByText("Ask an owner to refund this invoice."),
  ).toBeVisible();
  const voided: Invoice = { ...PAID, status: "void" };
  rerender(
    <InvoicePage
      invoice={voided}
      account={EUROPE}
      permissions={OWNER}
      onAction={() => {}}
    />,
  );
  expect(screen.queryByRole("button")).toBeNull();
});

test("outside a provider the actions fail safe", () => {
  const draft: Invoice = { ...PAID, status: "draft" };
  render(<InvoiceActions invoice={draft} onAction={() => {}} />);
  expect(screen.queryByRole("button")).toBeNull();
  expect(
    screen.getByText(
      "Ask an owner or an admin to send this invoice.",
    ),
  ).toBeVisible();
});

All four pass. Give the permissions context a permissive default and the last one fails: the draft offers “Send invoice” with no provider at all.

Context stops at the server boundary

Reading context with use isn’t supported in Server Components, and neither is createContext; both live in client modules, which is why account.tsx opens with "use client". A Server Component can load the permissions and the account and pass them as props to the page, which provides them to everything below.

React 18, 19 and the Compiler

use is new in React 19. Code that also has to run on React 18, such as a shared library, keeps useContext, which React 19 still supports in full; within an app on 19 the two live side by side. <Context.Provider> still works in React 19, and the React 19 release notes say it will be deprecated in a future version, with a codemod to convert existing providers. The React Compiler memoises a provider’s value for you, so the object a page builds for its provider no longer changes on every render.

When useContext reads just as well

A component that reads its context on every path gains nothing from use; the read sits at the top either way, and either function reads clearly there. Custom hooks that wrap a context read the same with both.

Context itself is the wrong tool for a value one component owns or two siblings share; those belong in state, held where State colocation in React and Lifting state up in React put it.

How Turtlekit’s Root provides formats to every component

Root is a provider at the public API. Its locale, currency and timeZone reach every component inside it, so Metric and DataTable write money in the account’s format without a prop of their own. Button’s unavailable covers the explained case:

// The paid invoice with Turtlekit: Root provides the account's locale
// and currency, which Metric reads wherever it renders, and Button
// says why an action can't be used.
import type { ReactNode } from "react";
import { Button, Metric, Root, Stack } from "turtlekit";

export function AccountRoot({
  locale,
  children,
}: {
  locale: string;
  children: ReactNode;
}) {
  return (
    <Root context="app" locale={locale} currency="EUR">
      {children}
    </Root>
  );
}

interface PaidInvoiceProps {
  total: number;
  canRefund: boolean;
  onRefund: () => void;
}

export function PaidInvoice({
  total,
  canRefund,
  onRefund,
}: PaidInvoiceProps) {
  return (
    <Stack gap="items">
      <Metric label="Paid" value={total / 100} format="currency" />
      <Button
        variant="soft"
        unavailable={
          canRefund
            ? undefined
            : "Only an owner can refund an invoice"
        }
        onClick={onRefund}
      >
        Refund
      </Button>
    </Stack>
  );
}

Inside an AccountRoot with locale="de-DE", the same Metric reads “1.200,00 €”. The button keeps its label and says why it can’t be used, instead of disappearing.

When a read comes back wrong

Three problems account for most of the trouble with reading context through use.

use isn’t exported from react

The project is on React 18. Upgrade, or keep useContext with the reads at the top.

The component gets the default value

The provider isn’t above the reading component. It may be rendered by the same component, which doesn’t count, or the build may include two copies of the module that creates the context, so the provider and the reader hold different contexts.

use(Context) fails in a Server Component

Context can’t be read on the server side of the boundary. Move the read into a Client Component, or pass the value as a prop.

Code in the order of the decision

use is a small API with a narrow effect: it moves a line. The line matters because a component is read far more often than it is written, and a reader follows it from the top. When the first lines are the guards and the reads sit beside their branches, the component says what it decides in the order it decides it. That is worth one more import, and it is why the reviewer’s comment could finally be taken.

Context selectors in React

When the context’s value changes often and every reader re-renders with it.

Compound components in React

When a parent shares state with its parts through context, read with use().

State colocation in React

When the value doesn’t need context at all, because one component reads it.

Lifting state up in React

When two siblings share a value and props are still the simpler route.