Skip to content

Commit d9432b8

Browse files
authored
Merge pull request #99 from tenphi/fix-ssr-refactoring
fix: ssr improvements
2 parents e9b8111 + b183c93 commit d9432b8

File tree

13 files changed

+43
-124
lines changed

13 files changed

+43
-124
lines changed

.changeset/remove-ssr-context.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tenphi/tasty': patch
3+
---
4+
5+
Remove `TastySSRContext` React context from SSR pipeline. All hooks now discover the SSR collector via the same global getter used by `computeStyles()`, eliminating the need for a React context Provider in `TastyRegistry`. This simplifies the SSR architecture to a single collector discovery mechanism.

docs/ssr.md

Lines changed: 12 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,15 @@ That's it. All `tasty()` components inside the tree automatically get SSR suppor
8282

8383
### How it works
8484

85-
- `TastyRegistry` is a `'use client'` component, but Next.js still server-renders it on initial page load.
86-
- During SSR, `computeStyles()` finds the collector (registered globally by `TastyRegistry`) and collects CSS rules into it.
87-
- `TastyRegistry` uses `useServerInsertedHTML` to flush collected CSS into the HTML stream as `<style data-tasty-ssr>` tags. This is fully streaming-compatible -- styles are injected alongside each Suspense boundary as it resolves.
85+
- `TastyRegistry` is a `'use client'` component, but Next.js still server-renders it on initial page load. The `'use client'` boundary is required solely to access `useServerInsertedHTML`**not** because `tasty()` components need the client.
86+
- During SSR, `TastyRegistry` creates a `ServerStyleCollector` and registers it via a module-level global getter. All style computation — whether from `tasty()` components, `computeStyles()`, `useStyles()`, or other hooks like `useGlobalStyles()` — discovers the collector through this single global getter. No React context is involved.
87+
- `TastyRegistry` uses `useServerInsertedHTML` to flush collected CSS into the HTML stream as `<style data-tasty-ssr>` tags. This is fully streaming-compatible styles are injected alongside each Suspense boundary as it resolves.
8888
- A companion `<script>` tag transfers the `cacheKey → className` mapping to the client.
8989
- When the module loads on the client, `hydrateTastyCache()` runs automatically and pre-populates the injector cache. During hydration, `computeStyles()` hits the cache and skips the entire pipeline.
9090

9191
### Using tasty() in Server Components
9292

93-
`tasty()` components are hook-free and do not require `'use client'`. They can be used directly in React Server Components. Dynamic `styleProps` like `<Grid flow="column">` work normally in server components. During SSR, the `TastyRegistry` registers its collector globally so that `computeStyles()` can discover it without React context.
93+
`tasty()` components are hook-free and do not require `'use client'`. They can be used directly in React Server Components. Dynamic `styleProps` like `<Grid flow="column">` work normally in server components. During SSR, `computeStyles()` discovers the collector via the same global getter registered by `TastyRegistry` — no React context or client boundary needed for this path.
9494

9595
### Options
9696

@@ -197,12 +197,12 @@ Same as Next.js -- call `configure({ nonce: '...' })` before any rendering happe
197197

198198
## Generic Framework Integration
199199

200-
Any React-based framework can integrate using the core SSR API:
200+
Any React-based framework can integrate using `runWithCollector`, which binds a `ServerStyleCollector` to the current async context via `AsyncLocalStorage`. All `computeStyles()` and hook calls within the render automatically discover the collector.
201201

202202
```tsx
203203
import {
204204
ServerStyleCollector,
205-
TastySSRContext,
205+
runWithCollector,
206206
hydrateTastyCache,
207207
} from '@tenphi/tasty/ssr';
208208
import { renderToString } from 'react-dom/server';
@@ -212,10 +212,8 @@ import { hydrateRoot } from 'react-dom/client';
212212

213213
const collector = new ServerStyleCollector();
214214

215-
const html = renderToString(
216-
<TastySSRContext.Provider value={collector}>
217-
<App />
218-
</TastySSRContext.Provider>
215+
const html = await runWithCollector(collector, () =>
216+
renderToString(<App />)
219217
);
220218

221219
const css = collector.getCSS();
@@ -251,11 +249,8 @@ For streaming with `renderToPipeableStream`, use `flushCSS()` instead of `getCSS
251249
```tsx
252250
const collector = new ServerStyleCollector();
253251

254-
const stream = renderToPipeableStream(
255-
<TastySSRContext.Provider value={collector}>
256-
<App />
257-
</TastySSRContext.Provider>,
258-
{
252+
const stream = await runWithCollector(collector, () =>
253+
renderToPipeableStream(<App />, {
259254
onShellReady() {
260255
// Flush styles collected so far
261256
const css = collector.flushCSS();
@@ -270,31 +265,10 @@ const stream = renderToPipeableStream(
270265
const state = collector.getCacheState();
271266
res.write(`<script data-tasty-cache type="application/json">${JSON.stringify(state)}</script>`);
272267
},
273-
}
268+
})
274269
);
275270
```
276271

277-
### AsyncLocalStorage (no React context)
278-
279-
If your framework doesn't support wrapping the React tree with a provider, use `runWithCollector`:
280-
281-
```tsx
282-
import {
283-
ServerStyleCollector,
284-
runWithCollector,
285-
hydrateTastyCache,
286-
} from '@tenphi/tasty/ssr';
287-
288-
const collector = new ServerStyleCollector();
289-
290-
const html = await runWithCollector(collector, () =>
291-
renderToString(<App />)
292-
);
293-
294-
const css = collector.getCSS();
295-
// ... inject into HTML as above
296-
```
297-
298272
---
299273

300274
## API Reference
@@ -303,7 +277,7 @@ const css = collector.getCSS();
303277

304278
| Import path | Description |
305279
|---|---|
306-
| `@tenphi/tasty/ssr` | Core SSR API: `ServerStyleCollector`, `TastySSRContext`, `runWithCollector`, `hydrateTastyCache` |
280+
| `@tenphi/tasty/ssr` | Core SSR API: `ServerStyleCollector`, `runWithCollector`, `hydrateTastyCache` |
307281
| `@tenphi/tasty/ssr/next` | Next.js App Router: `TastyRegistry` component |
308282
| `@tenphi/tasty/ssr/astro` | Astro: `tastyMiddleware`, auto-hydration on import |
309283

@@ -323,10 +297,6 @@ Server-safe style collector. One instance per request.
323297
| `flushCSS()` | Get only CSS collected since the last flush. For streaming SSR. |
324298
| `getCacheState()` | Serialize `{ entries: Record<cacheKey, className>, classCounter }` for client hydration. |
325299

326-
### `TastySSRContext`
327-
328-
React context (`createContext<ServerStyleCollector | null>(null)`). Used by the `useStyles()` hook to find the collector during SSR. Not needed when using `computeStyles()` directly (which discovers the collector via `AsyncLocalStorage` or the global getter registered by `TastyRegistry`).
329-
330300
### `TastyRegistry`
331301

332302
Next.js App Router component. Props:

src/hooks/resolve-ssr-collector.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/hooks/useCounterStyle.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { useContext, useInsertionEffect, useMemo } from 'react';
1+
import { useInsertionEffect, useMemo } from 'react';
22

33
import { getGlobalInjector } from '../config';
44
import { formatCounterStyleRule } from '../counter-style';
55
import type { CounterStyleDescriptors } from '../injector/types';
6-
import { resolveSSRCollector } from './resolve-ssr-collector';
7-
import { TastySSRContext } from '../ssr/context';
6+
import { getRegisteredSSRCollector } from '../ssr/ssr-collector-ref';
87

98
interface UseCounterStyleOptions {
109
name?: string;
@@ -73,8 +72,7 @@ export function useCounterStyle(
7372
depsOrOptions?: readonly unknown[] | UseCounterStyleOptions,
7473
options?: UseCounterStyleOptions,
7574
): string {
76-
const ssrContextValue = useContext(TastySSRContext);
77-
const ssrCollector = resolveSSRCollector(ssrContextValue);
75+
const ssrCollector = getRegisteredSSRCollector();
7876

7977
const isFactory = typeof descriptorsOrFactory === 'function';
8078

src/hooks/useFontFace.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { useContext, useInsertionEffect, useMemo } from 'react';
1+
import { useInsertionEffect, useMemo } from 'react';
22

33
import { getGlobalInjector } from '../config';
44
import { fontFaceContentHash, formatFontFaceRule } from '../font-face';
55
import type { FontFaceDescriptors, FontFaceInput } from '../injector/types';
6-
import { resolveSSRCollector } from './resolve-ssr-collector';
7-
import { TastySSRContext } from '../ssr/context';
6+
import { getRegisteredSSRCollector } from '../ssr/ssr-collector-ref';
87

98
interface UseFontFaceOptions {
109
root?: Document | ShadowRoot;
@@ -48,8 +47,7 @@ export function useFontFace(
4847
input: FontFaceInput,
4948
options?: UseFontFaceOptions,
5049
): void {
51-
const ssrContextValue = useContext(TastySSRContext);
52-
const ssrCollector = resolveSSRCollector(ssrContextValue);
50+
const ssrCollector = getRegisteredSSRCollector();
5351

5452
const inputKey = useMemo(() => JSON.stringify(input), [input]);
5553

src/hooks/useGlobalStyles.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { useContext, useInsertionEffect, useMemo, useRef } from 'react';
1+
import { useInsertionEffect, useMemo, useRef } from 'react';
22

33
import { getConfig } from '../config';
44
import { injectGlobal } from '../injector';
55
import type { StyleResult } from '../pipeline';
66
import { renderStyles } from '../pipeline';
77
import { collectAutoInferredProperties } from '../ssr/collect-auto-properties';
8-
import { resolveSSRCollector } from './resolve-ssr-collector';
9-
import { TastySSRContext } from '../ssr/context';
108
import { formatGlobalRules } from '../ssr/format-global-rules';
9+
import { getRegisteredSSRCollector } from '../ssr/ssr-collector-ref';
1110
import type { Styles } from '../styles/types';
1211
import { resolveRecipes } from '../utils/resolve-recipes';
1312

@@ -35,8 +34,7 @@ import { resolveRecipes } from '../utils/resolve-recipes';
3534
* ```
3635
*/
3736
export function useGlobalStyles(selector: string, styles?: Styles): void {
38-
const ssrContextValue = useContext(TastySSRContext);
39-
const ssrCollector = resolveSSRCollector(ssrContextValue);
37+
const ssrCollector = getRegisteredSSRCollector();
4038

4139
const disposeRef = useRef<(() => void) | null>(null);
4240

src/hooks/useKeyframes.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { useContext, useInsertionEffect, useMemo, useRef } from 'react';
1+
import { useInsertionEffect, useMemo, useRef } from 'react';
22

33
import { keyframes } from '../injector';
44
import type { KeyframesResult, KeyframesSteps } from '../injector/types';
5-
import { resolveSSRCollector } from './resolve-ssr-collector';
6-
import { TastySSRContext } from '../ssr/context';
75
import { formatKeyframesCSS } from '../ssr/format-keyframes';
6+
import { getRegisteredSSRCollector } from '../ssr/ssr-collector-ref';
87

98
interface UseKeyframesOptions {
109
name?: string;
@@ -75,8 +74,7 @@ export function useKeyframes(
7574
depsOrOptions?: readonly unknown[] | UseKeyframesOptions,
7675
options?: UseKeyframesOptions,
7776
): string {
78-
const ssrContextValue = useContext(TastySSRContext);
79-
const ssrCollector = resolveSSRCollector(ssrContextValue);
77+
const ssrCollector = getRegisteredSSRCollector();
8078

8179
// Detect which overload is being used
8280
const isFactory = typeof stepsOrFactory === 'function';

src/hooks/useProperty.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { useContext, useInsertionEffect, useMemo } from 'react';
1+
import { useInsertionEffect, useMemo } from 'react';
22

33
import { getGlobalInjector } from '../config';
4-
import { resolveSSRCollector } from './resolve-ssr-collector';
5-
import { TastySSRContext } from '../ssr/context';
64
import { formatPropertyCSS } from '../ssr/format-property';
5+
import { getRegisteredSSRCollector } from '../ssr/ssr-collector-ref';
76

87
export interface UsePropertyOptions {
98
/**
@@ -81,8 +80,7 @@ export interface UsePropertyOptions {
8180
* ```
8281
*/
8382
export function useProperty(name: string, options?: UsePropertyOptions): void {
84-
const ssrContextValue = useContext(TastySSRContext);
85-
const ssrCollector = resolveSSRCollector(ssrContextValue);
83+
const ssrCollector = getRegisteredSSRCollector();
8684

8785
// Memoize the options to create a stable dependency
8886
const optionsKey = useMemo(() => {

src/hooks/useRawCSS.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { useContext, useInsertionEffect, useMemo, useRef } from 'react';
1+
import { useInsertionEffect, useMemo, useRef } from 'react';
22

33
import { injectRawCSS } from '../injector';
4-
import { resolveSSRCollector } from './resolve-ssr-collector';
5-
import { TastySSRContext } from '../ssr/context';
4+
import { getRegisteredSSRCollector } from '../ssr/ssr-collector-ref';
65

76
interface UseRawCSSOptions {
87
root?: Document | ShadowRoot;
@@ -69,8 +68,7 @@ export function useRawCSS(
6968
depsOrOptions?: readonly unknown[] | UseRawCSSOptions,
7069
options?: UseRawCSSOptions,
7170
): void {
72-
const ssrContextValue = useContext(TastySSRContext);
73-
const ssrCollector = resolveSSRCollector(ssrContextValue);
71+
const ssrCollector = getRegisteredSSRCollector();
7472

7573
// Detect which overload is being used
7674
const isFactory = typeof cssOrFactory === 'function';

src/hooks/useStyles.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import { useContext } from 'react';
2-
31
import { computeStyles } from '../compute-styles';
4-
import type { ServerStyleCollector } from '../ssr/collector';
5-
import { TastySSRContext } from '../ssr/context';
62
import type { Styles } from '../styles/types';
73

84
/**
@@ -21,11 +17,11 @@ export interface UseStylesResult {
2117
}
2218

2319
/**
24-
* Hook to generate CSS classes from Tasty styles.
25-
* Thin wrapper around `computeStyles()` that adds React context-based
26-
* SSR collector discovery for backward compatibility with TastyRegistry.
20+
* Generate CSS classes from Tasty styles.
21+
* Thin re-export of `computeStyles()` kept for backward compatibility.
2722
*
28-
* For hook-free usage (e.g. in server components), use `computeStyles()` directly.
23+
* Unlike a React hook, this is a plain function and can be called
24+
* from both client components and React Server Components.
2925
*
3026
* @example
3127
* ```tsx
@@ -41,10 +37,5 @@ export interface UseStylesResult {
4137
* ```
4238
*/
4339
export function useStyles(styles: UseStylesOptions): UseStylesResult {
44-
const ssrContextValue: ServerStyleCollector | null =
45-
useContext(TastySSRContext);
46-
47-
return computeStyles(styles, {
48-
ssrCollector: ssrContextValue,
49-
});
40+
return computeStyles(styles);
5041
}

0 commit comments

Comments
 (0)