Skip to Content
ComposeGetting Started

Ahead of the release

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

Getting started

The concept. The overview showed a provider stack written as an array. This page is where you write one. The one thing to take from it is that the array is the tree: entry one wraps entry two, entry two wraps entry three, and the order you read is the order React renders.

What you get. A working root component for a shop front end, with three providers in it, and a fourth added by typing one line.

Why you want it. Everything a provider stack does wrong, it does when somebody changes it. A theme that has to sit inside the cart, a currency that has to sit outside both — those are facts about the app, and in a staircase they are indentation. In an array they are line numbers.

How the library gets you there. One import, one component, one prop.

Install compose

npm install @evanion/compose

React 18 or 19 is a peer dependency and nothing else comes with it. The package is ESM only.

Write the compose array

ComposeProvider takes providers and children. Each entry of providers is a provider; the first one ends up outermost, so the array reads in the order the rendered tree nests:

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

Three things are worth naming in that listing. CartProvider is written bare, because every prop it declares is optional. ThemeProvider needs a theme, so it goes through provider(), which pairs the component with the props. And children is never in the props you supply: ComposeProvider fills it in with the next provider down, or with Shop’s own children at the innermost level.

Render Shop at the root of the app and the tree it builds is CartProviderThemeProviderCurrencyProvider → your page.

Move an entry and watch the compose tree move

The array is the tree, which is a claim you can put your hands on. The boxes below are what ComposeProvider returned for the array beside them. Move an entry up and its box moves outward; move it down and the boxes it used to contain close over it.

CartProviderwhat is in the basket
ThemeProviderdark or light
CurrencyProviderkr, £, $

Brass: Birmingham

providersMove an entry and the box it draws moves with it.

providers={[  CartProvider,  ThemeProvider,  CurrencyProvider,]}

Nothing is simulated here — the boxes are the elements the published component built, so what reorders is a real provider tree.

One thing the control deliberately cannot show: React reconciles by position, so every reorder unmounts the subtree and mounts it again. There is no state under these boxes to lose. In an app there is, which is why a providers array whose order or length changes while the app runs remounts everything below it. Keep the array fixed and let a provider handle its own conditional case.

Write a compose entry three ways

provider() is one of three forms an entry can take, and all three type-check the same way:

import type { } from 'react'; import { , } from '@evanion/compose'; const = ({ }: ) => ( < ="currency">{}</> ); const = ({ , , }: <{ : 'light' | 'dark' }>) => ( < ={}>{}</> ); export function ({ }: ) { return ( < ={[ // A bare component, for a provider whose props are all optional. , // A `provider()` call, checked where it is written. (, { : 'dark' }), // A tuple, checked where the array reaches `ComposeProvider`. [, { : 'light' }], ]} > {} </> ); }

A bare component is an entry on its own, as long as every prop it declares is optional. A provider() call pairs a component with props and is checked where you write it, so the editor completes on the component’s prop names while you type the object. A plain [component, props] tuple is the same pair without the call.

All three reject a missing prop, a wrong value and a prop the component does not declare. What separates them is where the compiler notices, and that turns out to matter more than it sounds: Where the check happens is the whole of it.

Where to go next

Last updated on