ssr-emotion-react 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,8 +35,9 @@ Now, you can use not only Astro components (`.astro`) but also React JSX compone
35
35
 
36
36
  > **Note:** React JSX components in Astro Islands
37
37
  >
38
- > * **No directive (Server Only)**: Rendered as just static HTML tags. It results in zero client-side JavaScript. (I used to think there wasn't much point in writing static content in JSX components instead of just using Astro components. It seemed like standard Astro components was more than enough. **However, I've realized one major advantage:** SSR Emotion — the ultimate SSR Zero-Runtime CSS-in-JS solution, seamlessly integrated with Astro. By using React JSX components, your styles are automatically extracted into static CSS during the build process. This means you can enjoy the full power of CSS-in-JS while still shipping zero bytes of JS to the browser. In this regard, it's a significant upgrade over standard Astro components.)
39
- > * `client:only="react"` **(Client Only)**: This is the mode where the relationship between React and Astro is the clearest. It skips server-side rendering and runs entirely in the browser.
38
+ > * **No directive (SSR Only)**: Rendered as just static HTML tags. It results in zero client-side JavaScript. (I used to think there wasn't much point in writing static content in JSX components instead of just using Astro components. It seemed like standard Astro components was more than enough. **However, I've realized one major advantage:** SSR Emotion — the ultimate SSR Zero-Runtime CSS-in-JS solution, seamlessly integrated with Astro. By using React JSX components, your styles are automatically extracted into static CSS during the build process. This means you can enjoy the full power of CSS-in-JS while still shipping zero bytes of JS to the browser. In this regard, it's a significant upgrade over standard Astro components.)
39
+ >
40
+ > * `client:only="react"` **(CSR Only)**: As you know, this is the standard mode where Emotion is used, and this plugin does nothing. It skips server-side rendering and runs entirely in the browser.
40
41
  >
41
42
  > * `client:load`(and others like `client:visible` or `client:idle`) **(SSR Hydration)**: Despite its cool and flashy name, "SSR Hydration" is not that complicated: it just creates a static HTML skeleton first, and once the JS is ready, the engine takes over the DOM as if it had been there from the start. If you are particular about the visual transition—like ensuring there is no layout shift by pre-setting an image's height—you might want to take control to make the swap feel completely natural.
42
43
 
@@ -48,24 +49,24 @@ This plugin wasn't originally built for React. It was first conceived as a core
48
49
  While developing [Potate](https://github.com/uniho/potate), I discovered a way to handle SSR [Emotion](https://emotion.sh/docs/@emotion/css) and Hydration that felt more "correct" for the Astro era: **The Full DOM Replacement strategy.** It worked so flawlessly in Potate that I decided to bring this lineage back to the "ancestor," React. By applying Potate's philosophy to React, we've eliminated the historical complexities of Emotion SSR and the fragility of standard React hydration.
49
50
 
50
51
 
51
- ## 💎 The Result
52
+ ## 💎 SSR Only
53
+
54
+ You don't need to learn any special properties or complex setups. It just works with the [Emotion `css()` function](https://emotion.sh/docs/@emotion/css). It feels completely natural, even in Astro's **"No directive" (SSR Only)** mode.
52
55
 
53
56
  * **Zero Runtime by default:** No `Emotion` library is shipped to the browser. It delivers a pure Zero-JS experience.
54
57
  * **Familiar DX:** Use the full expressive power of the [Emotion `css()` function](https://emotion.sh/docs/@emotion/css) that you already know.
55
58
  * **Decoupled Asset Delivery:** Styles are moved to separate `.css` files to allow for flexible cache strategies. By using unique filenames (cache busting), we ensure that updates are immediately reflected even when long-term caching is enabled on the server.
56
- * **Hydration Stability:** No overhead for style re-calculation. Interactive Islands remain stable and fully dynamic without visual flickering during the hydration process.
57
-
58
59
 
59
- ## 🛠 How it looks
60
-
61
- In SSR Emotion, you don't need to learn any special properties or complex setups. It just works with the [Emotion `css()` function](https://emotion.sh/docs/@emotion/css). It feels completely natural, even in Astro's **"No directive" (Server Only)** mode.
60
+ ### Example: SSR Only Styling
62
61
 
63
62
  While you can use [`css()`](https://emotion.sh/docs/@emotion/css) directly, you can also create reusable functions like `flexCol()` (which we call **"The Patterns"**).
64
63
 
65
64
  ```jsx
65
+ // src/components/StaticBox.jsx
66
+
66
67
  import { css } from '@emotion/css'
67
68
 
68
- export const MyComponent = () => (
69
+ export default props => (
69
70
  <div class={flexCol({
70
71
  color: 'hotpink',
71
72
  '&:hover': { color: 'deeppink' }
@@ -81,10 +82,27 @@ const flexCol = (...args) => css({
81
82
 
82
83
  ```
83
84
 
85
+ ```astro
86
+ ---
87
+ // src/pages/index.astro
84
88
 
85
- ## 🌗 Hybrid Styling (SSR + CSR)
89
+ import Layout from '../layouts/Layout.astro'
90
+ import StaticBox from '../components/StaticBox'
91
+ ---
86
92
 
87
- In Astro, Island components (`client:*`) get the best of both worlds.
93
+ <Layout>
94
+ <StaticBox />
95
+ </Layout>
96
+
97
+ ```
98
+
99
+ ## 🌗 SSR Hydration (SSR + CSR)
100
+
101
+ In Astro, Island components (`client:load` and others) get the best of both worlds.
102
+
103
+ * **Hydration Stability:** No overhead for style re-calculation. Interactive Islands remain stable and fully dynamic without visual flickering during the hydration process.
104
+ * **Unlimited Flexibility:** Need to change colors based on user input or mouse position? Just pass the props/state to css() like you always do.
105
+ * **Zero Learning Curve:** If you know how to use useEffect and Emotion, you already know how to build dynamic Islands with React.
88
106
 
89
107
  ### How it works
90
108
 
@@ -103,12 +121,12 @@ You can easily change styles when the component "wakes up" in the browser:
103
121
  ```jsx
104
122
  // src/components/InteractiveBox.jsx
105
123
 
106
- export const InteractiveBox = () => {
107
- const [isLoaded, setIsLoaded] = useState(false);
124
+ export default props => {
125
+ const [isLoaded, setIsLoaded] = useState(false)
108
126
 
109
127
  useEffect(() => {
110
- setIsLoaded(true); // Triggers once JS is active
111
- }, []);
128
+ setIsLoaded(true) // Triggers once JS is active
129
+ }, [])
112
130
 
113
131
  return (
114
132
  <div class={css({
@@ -119,8 +137,8 @@ export const InteractiveBox = () => {
119
137
  })}>
120
138
  {isLoaded ? 'I am Interactive!' : 'I am Static HTML'}
121
139
  </div>
122
- );
123
- };
140
+ )
141
+ }
124
142
 
125
143
  ```
126
144
 
@@ -128,9 +146,9 @@ export const InteractiveBox = () => {
128
146
  ---
129
147
  // src/pages/index.astro
130
148
 
131
- import Layout from '../layouts/Layout.astro';
132
- import StaticBox from '../components/StaticBox';
133
- import InteractiveBox from '../components/InteractiveBox';
149
+ import Layout from '../layouts/Layout.astro'
150
+ import StaticBox from '../components/StaticBox'
151
+ import InteractiveBox from '../components/InteractiveBox'
134
152
  ---
135
153
 
136
154
  <Layout>
@@ -140,13 +158,6 @@ import InteractiveBox from '../components/InteractiveBox';
140
158
 
141
159
  ```
142
160
 
143
- ### Key Benefits
144
-
145
- * **No FOUC (Flash of Unstyled Content):** Since the "Server Side" style is already in the static CSS, there's no flickering.
146
- * **Unlimited Flexibility:** Need to change colors based on user input or mouse position? Just pass the props/state to css() like you always do.
147
- * **Zero Learning Curve:** If you know how to use useEffect and Emotion, you already know how to build dynamic Islands with React.
148
-
149
-
150
161
  ## 🛠 The Patterns
151
162
 
152
163
  We refer to reusable CSS logic as "The Patterns".
@@ -172,7 +183,9 @@ const linkOverlay = (...args) => css({
172
183
  },
173
184
  }, ...args)
174
185
 
175
- export default props => (
186
+ :
187
+
188
+ const MyComponent = props => (
176
189
  // The parent must have position: relative
177
190
  <div class={css({ position: 'relative', border: '1px solid #ccc', padding: '1rem' })}>
178
191
  <img src="https://via.placeholder.com/150" alt="placeholder" />
@@ -198,11 +211,13 @@ const isBP = value => value in BP
198
211
  const _gt = bp => `(min-width: ${isBP(bp) ? BP[bp] : bp})`
199
212
  const _lt = bp => `(max-width: ${isBP(bp) ? BP[bp] : bp})`
200
213
 
201
- const gt = (bp, ...args) => css({[`@media ${_gt(bp)}`]: args})
202
- const lt = (bp, ...args) => css({[`@media ${_lt(bp)}`]: args})
203
- const bw = (min, max, ...args) => css({[`@media ${_gt(min)} and ${_lt(max)}`]: args})
214
+ const gt = (bp, ...args) => css({[`@media ${_gt(bp)}`]: css(...args)})
215
+ const lt = (bp, ...args) => css({[`@media ${_lt(bp)}`]: css(...args)})
216
+ const bw = (min, max, ...args) => css({[`@media ${_gt(min)} and ${_lt(max)}`]: css(...args)})
204
217
 
205
- export default props => (
218
+ :
219
+
220
+ const MyComponent = props => (
206
221
  <div class={css(
207
222
  { color: 'black' }, // default css
208
223
  bw('sm', '75rem', { color: 'blue' }), // between
@@ -230,7 +245,7 @@ export const styled = (Tag) => (style, ...values) => props => {
230
245
  const makeClassName = (style, ...values) =>
231
246
  typeof style == 'function' ? makeClassName(style(props)) : css(style, ...values);
232
247
 
233
- const {sx, className, 'class': _class, children, ...wosx} = props;
248
+ const {as: As, sx, className, 'class': _class, children, ...wosx} = props;
234
249
 
235
250
  // cleanup transient props
236
251
  Object.keys(wosx).forEach(key => {
@@ -242,7 +257,8 @@ export const styled = (Tag) => (style, ...values) => props => {
242
257
  className: cx(makeClassName(style, ...values), makeClassName(sx), _class, className),
243
258
  };
244
259
 
245
- return (<Tag {...newProps}>{children}</Tag>);
260
+ const T = As || Tag;
261
+ return (<T {...newProps}>{children}</T>);
246
262
  };
247
263
 
248
264
  ```
@@ -251,26 +267,23 @@ What is the sx prop? For those unfamiliar with libraries like [MUI, the sx prop]
251
267
 
252
268
  In this implementation, you can pass raw style objects to the sx prop without wrapping them in `css()` or "The Patterns" functions.
253
269
 
254
- However, since the underlying tag of a button is not always `<button>`, and because using something like `createElement()` locally is generally not recommended, I personally prefer the approach shown below. In any case, how you choose to implement this is entirely up to you.
270
+ However, defining a `styled` component inside a render function is **a pitfall** because it creates a new component identity every time, forcing React to re-mount.
271
+ I personally prefer the approach shown below. In any case, how you choose to implement this is entirely up to you.
255
272
 
256
273
  ```js
257
- // the-sx-prop.js
274
+ // the-sx-prop.jsx
258
275
 
259
276
  import {css, cx} from '@emotion/css'
260
277
 
261
- const sx = (props, ...styles) => {
262
- const result = typeof props === 'object' ? {...props} : {};
263
- const _css = [];
264
- for (let arg of styles) {
265
- if (typeof arg === 'function') {
266
- _css.push(arg(result));
267
- } else if (arg) {
268
- _css.push(arg);
269
- }
278
+ export const sx = (props, style, ...values) => {
279
+ let result = (props && typeof props === 'object' ? props : {});
280
+ if (typeof style === 'function') {
281
+ result = {...style(result), ...result};
282
+ result.className = cx(css(result?.$css, ...values), result.className);
283
+ } else {
284
+ result.className = cx(css(style, ...values), result.className);
270
285
  }
271
286
 
272
- result.className = cx(css(_css), result.className);
273
-
274
287
  // cleanup transient props
275
288
  Object.keys(result).forEach(key => {
276
289
  if (key.startsWith('$')) delete result[key];
@@ -279,12 +292,11 @@ const sx = (props, ...styles) => {
279
292
  return result;
280
293
  }
281
294
 
282
- export default sx;
283
-
284
- // Factory for component-scoped sx functions
295
+ // Factory for component-scoped sx functions (adds `.css()` automatically)
285
296
  sx._factory = (genCSS) => {
286
- const f = (props, ...styles) => sx(props, ...styles, genCSS);
287
- f.css = (...styles) => f({}, ...styles); // Styles-only function when you don't need props
297
+ const f = (props, ...styles) => sx(props || {}, genCSS, ...styles);
298
+ f.css = (...styles) => f({}, ...styles); // style only
299
+ // f.curry = (props) => (...values) => f(props || {}, ...values); // currying
288
300
  return f;
289
301
  }
290
302
 
@@ -308,7 +320,7 @@ sx.button = sx._factory(props => {
308
320
  style.boxShadow = 'var(--style-shadows-level1)';
309
321
  }
310
322
 
311
- return [
323
+ return {$css: [
312
324
  css`
313
325
  line-height: 1;
314
326
  display: inline-flex;
@@ -319,13 +331,13 @@ sx.button = sx._factory(props => {
319
331
  }
320
332
  `,
321
333
  style,
322
- ];
334
+ ]};
323
335
  });
324
336
 
325
337
  ```
326
338
 
327
339
  ```jsx
328
- import sx from './the-sx-prop'
340
+ import {sx} from './the-sx-prop'
329
341
 
330
342
  export default props => {
331
343
  return (<>
@@ -340,6 +352,59 @@ export default props => {
340
352
 
341
353
  ```
342
354
 
355
+ Furthermore, by creating a component like the one below, you evolve into a **Super Saiyan** (for those who aren't familiar, it's like a classic Superman).
356
+
357
+ ```js
358
+ // the-sx-prop.jsx
359
+
360
+ :
361
+ :
362
+
363
+ export const As = ({as: Tag = 'div', children, ...props}) => <Tag {...props}>{children}</Tag>;
364
+
365
+ // My list style
366
+ sx.ul = sx._factory(props => ({
367
+ as: 'ul',
368
+ $css: css`
369
+ & > li {
370
+ position: relative;
371
+ padding-left: 1.5rem;
372
+
373
+ &:before {
374
+ content: "\\2022";
375
+ position: absolute;
376
+ width: 1.5rem;
377
+ left: 0; top: 0;
378
+ text-align: center;
379
+ }
380
+ & + li, & > ul {
381
+ margin-top: .25rem;
382
+ }
383
+ }
384
+ & > ul {
385
+ margin-left: 1rem;
386
+ }
387
+ `}));
388
+
389
+ ```
390
+
391
+ ```jsx
392
+ import {sx, As} from './the-sx-prop'
393
+
394
+ const MyComponent = props => {
395
+ return (
396
+ <As {...sx.ul.css({ padding: '20px', border: '1px solid #ccc' })}>
397
+ <li>Is it Flexible?</li>
398
+ <li>Is it Dynamic?</li>
399
+ <li>Is it Polymorphic?</li>
400
+ <li>No, it's <strong>As</strong>!</li>
401
+ </As>
402
+ )
403
+ }
404
+
405
+ ```
406
+
407
+
343
408
  ## 🚫 Won't Do
344
409
 
345
410
  The following features are not planned for the core roadmap (though contributors are welcome to explore them):