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/composeReact 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
CartProvider → ThemeProvider → CurrencyProvider → 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.
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
- Where the check happens — the tuple assigned to a variable, and why it stops being checked
- API reference — every export and every type