Skip to content

Commit 88ac0ab

Browse files
timphandevclaude
andcommitted
feat: let a style recipe turn its cache off
`createStyleRecipe` and `createSlotStyleRecipe` take `cache` in their config, also through `createThemedRecipes`. The docs and the skill advise leaving it on, since an uncached recipe returns a new style on every call. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 732bc5d commit 88ac0ab

9 files changed

Lines changed: 106 additions & 15 deletions

File tree

‎docs/src/content/docs/native-recipe/create-slot-style-recipe.mdx‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ A slot recipe takes the config of a
3030
and with an object of styles keyed by slot name wherever a style recipe
3131
takes a style:
3232

33-
| Property | Description |
34-
| ------------------ | ---------------------------------------------------------------------------------- |
35-
| `slots` | The names of the slots, in the order of the result. |
36-
| `base` | Optional. The style each slot always has. |
37-
| `variants` | For each variant name, the style of each slot for each of its options. |
38-
| `compoundVariants` | Optional. Styles added to some slots, under `styles`, when several variants match. |
39-
| `defaultVariants` | Optional. The option each variant uses when a selection leaves it out. |
33+
| Property | Description |
34+
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
35+
| `slots` | The names of the slots, in the order of the result. |
36+
| `base` | Optional. The style each slot always has. |
37+
| `variants` | For each variant name, the style of each slot for each of its options. |
38+
| `compoundVariants` | Optional. Styles added to some slots, under `styles`, when several variants match. |
39+
| `defaultVariants` | Optional. The option each variant uses when a selection leaves it out. |
40+
| `cache` | Optional. Whether the slot recipe caches its styles. Defaults to `true`. Without the cache, every call returns a new object. |
4041

4142
An option or compound variant gives styles only to the slots it names.
4243
Each slot's style is merged in the order of a style recipe: its base

‎docs/src/content/docs/native-recipe/create-style-recipe.mdx‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ style={badge({ tone })} />`.
1919

2020
## The config
2121

22-
| Property | Description |
23-
| ------------------ | ------------------------------------------------------------------------------------------------ |
24-
| `base` | Optional. The style the element always has. |
25-
| `variants` | For each variant name, the style of each of its options. |
26-
| `compoundVariants` | Optional. Styles added when several variants have particular options at the same time, in order. |
27-
| `defaultVariants` | Optional. The option each variant uses when a selection leaves it out. |
22+
| Property | Description |
23+
| ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
24+
| `base` | Optional. The style the element always has. |
25+
| `variants` | For each variant name, the style of each of its options. |
26+
| `compoundVariants` | Optional. Styles added when several variants have particular options at the same time, in order. |
27+
| `defaultVariants` | Optional. The option each variant uses when a selection leaves it out. |
28+
| `cache` | Optional. Whether the recipe caches its style. Defaults to `true`. Without the cache, every call returns a new object. |
2829

2930
Every style is checked against the styles of React Native, as in
3031
`StyleSheet.create` (see [TypeScript](/recipe/native-recipe/typescript/)).

‎docs/src/content/docs/native-recipe/how-it-works.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ selection merges its style and caches it under that integer; every later
1919
call with the same variants reads the option of each variant and returns
2020
the cached style. Create each recipe once, at the top level of a module,
2121
so that its cache lasts. The cache grows with the selections the recipe is
22-
called with, up to one entry per combination of declared options.
22+
called with, up to one entry per combination of declared options. A recipe
23+
whose config sets `cache: false` builds a new style on every call, which
24+
changes the `style` prop on every render, so leave the cache on.
2325

2426
## The order of styles
2527

‎packages/native-recipe/skills/native-recipe/SKILL.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ function Badge({ tone }: BadgeProps) {
122122
```
123123

124124
The same holds for `createThemedRecipes`: call it once, in one module.
125+
Leave the `cache` of a recipe's config unset: with `cache: false`, the
126+
recipe returns a new object on every call, like a recipe created inside a
127+
component.
125128

126129
### Put every choice a component offers in a variant
127130

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { createSlotStyleRecipe } from "./slot-style-recipe.js";
4+
import { createStyleRecipe } from "./style-recipe.js";
5+
import { createThemedRecipes } from "./themed-recipes.js";
6+
7+
describe("the cache setting of a style recipe's config", () => {
8+
it("builds a new frozen style on every call without the cache", () => {
9+
const uncached = createStyleRecipe({
10+
cache: false,
11+
variants: { size: { sm: { height: 32 } } },
12+
});
13+
const cached = createStyleRecipe({
14+
variants: { size: { sm: { height: 32 } } },
15+
});
16+
17+
expect(uncached({ size: "sm" })).not.toBe(uncached({ size: "sm" }));
18+
expect(uncached({ size: "sm" })).toStrictEqual({ height: 32 });
19+
expect(Object.isFrozen(uncached({ size: "sm" }))).toBe(true);
20+
expect(cached({ size: "sm" })).toBe(cached({ size: "sm" }));
21+
});
22+
23+
it("builds new styles on every call of a slot style recipe", () => {
24+
const card = createSlotStyleRecipe({
25+
cache: false,
26+
slots: ["root"],
27+
variants: { size: { sm: { root: { height: 32 } } } },
28+
});
29+
30+
expect(card({ size: "sm" })).not.toBe(card({ size: "sm" }));
31+
expect(card({ size: "sm" })).toStrictEqual({ root: { height: 32 } });
32+
});
33+
34+
it("applies to the recipe of each theme", () => {
35+
const theme: { readonly radius: number } = { radius: 8 };
36+
const { createStyleRecipe: createThemedStyleRecipe } =
37+
createThemedRecipes<typeof theme>();
38+
const box = createThemedStyleRecipe((tokens) => ({
39+
cache: false,
40+
base: { borderRadius: tokens.radius },
41+
variants: { size: { sm: { height: 32 } } },
42+
}));
43+
44+
expect(box(theme, { size: "sm" })).not.toBe(box(theme, { size: "sm" }));
45+
expect(box(theme, { size: "sm" })).toStrictEqual({
46+
borderRadius: 8,
47+
height: 32,
48+
});
49+
});
50+
51+
it("rejects a setting that is not a boolean", () => {
52+
const box = createStyleRecipe({
53+
// @ts-expect-error cache must be a boolean
54+
cache: "no",
55+
variants: { size: { sm: { height: 32 } } },
56+
});
57+
const card = createSlotStyleRecipe({
58+
// @ts-expect-error cache must be a boolean
59+
cache: "no",
60+
slots: ["root"],
61+
variants: { size: { sm: { root: { height: 32 } } } },
62+
});
63+
64+
expect(box({ size: "sm" })).toStrictEqual({ height: 32 });
65+
expect(card({ size: "sm" })).toStrictEqual({ root: { height: 32 } });
66+
});
67+
});

‎packages/native-recipe/src/compile-slot-style-recipe.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface LooseSlotStyleRecipeConfig {
1717
}[]
1818
| undefined;
1919
readonly defaultVariants?: LooseSelection | undefined;
20+
readonly cache?: boolean | undefined;
2021
}
2122

2223
type LooseSlotStyleRecipe = KindRecipe<
@@ -32,6 +33,7 @@ function buildSlotStyleRecipe(
3233
): LooseSlotStyleRecipe {
3334
return slotStyleRecipe({
3435
base: config.base,
36+
cache: config.cache,
3537
compoundVariants: (config.compoundVariants ?? []).map((compound) => ({
3638
value: compound.styles,
3739
variants: compound.variants,

‎packages/native-recipe/src/compile-style-recipe.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface LooseStyleRecipeConfig {
1616
}[]
1717
| undefined;
1818
readonly defaultVariants?: LooseSelection | undefined;
19+
readonly cache?: boolean | undefined;
1920
}
2021

2122
type LooseStyleRecipe = KindRecipe<LooseSelection, LooseStyle>;
@@ -45,6 +46,7 @@ const styleRecipe = createRecipeKind(styleKind);
4546
function buildStyleRecipe(config: LooseStyleRecipeConfig): LooseStyleRecipe {
4647
return styleRecipe({
4748
base: config.base,
49+
cache: config.cache,
4850
compoundVariants: (config.compoundVariants ?? []).map((compound) => ({
4951
value: compound.style,
5052
variants: compound.variants,

‎packages/native-recipe/src/slot-style-recipe.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ interface SlotStyleRecipeConfig<
124124
/** The option each variant uses when a recipe is called without it. */
125125
readonly defaultVariants?:
126126
DefaultVariants<Variants, DefaultedName> | undefined;
127+
/**
128+
* Whether the recipe caches the styles of each declared selection.
129+
* Defaults to `true`.
130+
*/
131+
readonly cache?: boolean | undefined;
127132
}
128133

129134
/**
@@ -184,6 +189,8 @@ type RecipeSlotStyles<Slot extends string, Variants, Base, Compounds> = {
184189
* object, so calling a slot recipe again with the same variants returns the
185190
* same object, with the same style for each slot. Passing them to the
186191
* `style` prop of each element keeps the props unchanged between renders.
192+
* With `cache: false`, the recipe builds a new object on every call
193+
* instead.
187194
*
188195
* Every declared slot is present in the result, as an empty style when it
189196
* has none. A variant without a default is required, except a boolean

‎packages/native-recipe/src/style-recipe.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ interface StyleRecipeConfig<
7979
/** The option each variant uses when a recipe is called without it. */
8080
readonly defaultVariants?:
8181
DefaultVariants<Variants, DefaultedName> | undefined;
82+
/**
83+
* Whether the recipe caches the style of each declared selection.
84+
* Defaults to `true`.
85+
*/
86+
readonly cache?: boolean | undefined;
8287
}
8388

8489
/** Every style that a recipe's config declares, as a union. */
@@ -125,7 +130,8 @@ type StyleRecipe<Props, Style> = KindRecipe<Props, Style>;
125130
* so calling a recipe again with the same variants returns the same object.
126131
* Passing it to the `style` prop of a component keeps the prop unchanged
127132
* between renders, which lets React skip the style and a memoized child
128-
* skip rendering.
133+
* skip rendering. With `cache: false`, the recipe builds a new object on
134+
* every call instead.
129135
*
130136
* A variant without a default is required, except a boolean variant, whose
131137
* only options are `"true"` and `"false"` and which defaults to `false`. An

0 commit comments

Comments
 (0)