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 +120 -55
- package/dist/astro-integration.js +48 -48
- package/package.json +4 -4
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 (
|
|
39
|
-
>
|
|
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
|
-
## 💎
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
89
|
+
import Layout from '../layouts/Layout.astro'
|
|
90
|
+
import StaticBox from '../components/StaticBox'
|
|
91
|
+
---
|
|
86
92
|
|
|
87
|
-
|
|
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
|
|
107
|
-
const [isLoaded, setIsLoaded] = useState(false)
|
|
124
|
+
export default props => {
|
|
125
|
+
const [isLoaded, setIsLoaded] = useState(false)
|
|
108
126
|
|
|
109
127
|
useEffect(() => {
|
|
110
|
-
setIsLoaded(true)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
274
|
+
// the-sx-prop.jsx
|
|
258
275
|
|
|
259
276
|
import {css, cx} from '@emotion/css'
|
|
260
277
|
|
|
261
|
-
const sx = (props, ...
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
|
287
|
-
f.css = (...styles) => f({}, ...styles); //
|
|
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):
|