Skip to Content
ComposeOverview

Ahead of the release

npm install @evanion/compose gives you 2.0.0. These pages document main, which has changes that release does not.

@evanion/compose

The concept. A React app wraps its tree in providers: a cart, a theme, a currency, an error boundary. Written by hand they nest, one inside the next, and the file that holds them is a staircase eight levels deep whose closing tags you count on your fingers. Adding a provider reindents everything under it.

What you get. The same tree, written as an array. One entry per provider, first entry outermost, and a diff that adds a provider is one line.

Why you want it. The staircase is not hard to read once. It is hard to change. Moving a provider means moving its closing tag too, and the two are forty lines apart; a reviewer reading the diff sees indentation and cannot tell whether the order changed. An array puts the order on screen in three lines, and moving an entry is moving a line.

How the library gets you there. ComposeProvider takes the array on a prop called providers and nests it for you. provider() pairs a component with its props, so TypeScript checks the pair where you write it. There is no context, no hook and no runtime configuration: the package is a component and a helper.

import type { } from 'react'; import { , } from '@evanion/compose'; const = ({ }: ) => ( < ="cart">{}</> ); const = ({ , , }: <{ : 'light' | 'dark' }>) => ( < ={}>{}</> ); const = ({ }: ) => ( < ="currency">{}</> ); export function ({ }: ) { return ( < ={[ , (, { : 'dark' }), , ]} > {} </> ); }

Shop renders CartProvider outermost, ThemeProvider inside it and CurrencyProvider inside that, with whatever Shop was given at the centre. That is the array read top to bottom.

When to reach for compose

The whole package is one component and one helper, so whether it suits you is a short question:

  • Three or more providers at the root of an app, where the staircase has stopped being readable.
  • A provider list that changes — a feature adds one, a platform needs a different one — and you want that change to be a line rather than a reindent.
  • A tree that has to compile inside a React Server Component graph. ComposeProvider uses no hook, no createContext and no class, so it imports into a Next.js root app/layout.tsx without a 'use client' boundary of its own. The providers you hand it are a separate matter: one built on createContext is client code and carries its own directive.

When compose is the wrong tool

  • One or two providers. Two levels of nesting read fine written out, and an array of two is a dependency you did not need.
  • Providers that have to be reordered while the app runs. React reconciles by position, so changing the array’s order or its length remounts everything under it and loses the state in it.
  • Anything that is not a provider. ComposeProvider wraps its children in each entry in turn; it does not render a list, a route or a layout.

What ships in the compose package

The entry point is small enough to list whole:

ExportWhat it is
ComposeProviderthe component; takes providers and children
provider(component, props)pairs a component with its props, checked at the call
Provider, ProviderArrayone entry, and the array of them
AnyComponentany provider component, for a generic constraint
PropsWithoutChildren<T>what an entry has to supply, which is T’s props less children
ValidateProvider<T>the check applied to one entry
ValidateProviders<T>that check across a tuple, positions preserved
ValidatedProviders<T>the type of the providers prop
ComposeProviderProps<T>the props interface, for a wrapper that needs to name it

React 18 or 19 is the only peer dependency, and the package is ESM only: there is no require condition, so a CommonJS consumer gets ERR_REQUIRE_ESM.

Where to go next

  • Getting started — install it, write the array, and move an entry to watch the tree move with it
  • Where the check happens — why the same entry is checked where you write it and silently unchecked one line away
  • API reference — every export and every type

@evanion/react-widget renders a region of components from data, with no provider and no context at all, which is the other way to keep a tree importable from a Server Component.

Last updated on