Add SSR support

This commit is contained in:
Zoe Roux
2022-11-11 00:39:18 +09:00
parent 3c114f0da5
commit ca503e9db1
9 changed files with 159 additions and 14 deletions
+9 -6
View File
@@ -3,12 +3,15 @@
// Licensed under the MIT license. See LICENSE file in the project root for details.
//
import { StyleRegistryProvider } from "@yoshiki/react/src/registry";
import type { AppProps } from "next/app";
import { useLayoutEffect, useState } from "react";
export default function App({ Component, pageProps }: AppProps) {
const [show, showApp] = useState(false);
const App = ({ Component, pageProps }: AppProps) => {
return (
<StyleRegistryProvider>
<Component {...pageProps} />
</StyleRegistryProvider>
);
};
useLayoutEffect(() => showApp(true));
return show ? <Component {...pageProps} /> : null;
}
export default App;
@@ -0,0 +1,40 @@
//
// Copyright (c) Zoe Roux and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
//
import {
StyleRegistryProvider,
createStyleRegistry,
useStyleRegistry,
} from "@yoshiki/react/src/registry";
import Document, { DocumentContext } from "next/document";
Document.getInitialProps = async (ctx: DocumentContext) => {
const renderPage = ctx.renderPage;
const registry = createStyleRegistry();
ctx.renderPage = () =>
renderPage({
enhanceApp: (App) => (props) => {
return (
<StyleRegistryProvider registry={registry}>
<App {...props} />
</StyleRegistryProvider>
);
},
});
const props = await ctx.defaultGetInitialProps(ctx);
return {
...props,
styles: (
<>
{props.styles}
{registry.flushToComponent()}
</>
),
};
};
export default Document;
+2 -1
View File
@@ -19,7 +19,8 @@
"**/react-dom",
"**/react-dom/**",
"**/react-native",
"**/react-native/**"
"**/react-native/**",
"**/typescript/**"
]
},
"prettier": {
+1
View File
@@ -8,6 +8,7 @@
},
"devDependencies": {
"@types/react": "^18.0.24",
"react": "^18.2.0",
"typescript": "^4.8.4"
}
}
+2
View File
@@ -8,6 +8,8 @@
},
"devDependencies": {
"@types/react": "^18.0.24",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^4.8.4"
},
"peerDependencies": {
+6 -4
View File
@@ -6,6 +6,7 @@
import { Properties } from "csstype";
import { Theme, YoshikiStyle, useTheme, breakpoints, isBreakpoints } from "@yoshiki/core";
import { CSSProperties, useInsertionEffect } from "react";
import { useStyleRegistry } from "./registry";
// TODO: shorthands
export type CssObject = {
@@ -50,10 +51,11 @@ const dedupProperties = (...classes: (string | undefined)[]) => {
export const useYoshiki = () => {
const theme = useTheme();
let classes: string[] = [];
const registry = useStyleRegistry();
useInsertionEffect(() => {
document.head.insertAdjacentHTML("beforeend", `<style>${classes.join("\n")}</style>`);
}, [classes]);
registry.flushToBrowser();
}, [registry]);
return {
css: (
@@ -72,7 +74,7 @@ export const useYoshiki = () => {
},
[[], []],
);
classes = classes.concat(localStyle);
registry.addRules(localClassNames, localStyle);
return {
className: dedupProperties(...localClassNames, className),
style: style,
+96
View File
@@ -0,0 +1,96 @@
//
// Copyright (c) Zoe Roux and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
//
import React, { createContext, ReactNode, useContext } from "react";
class StyleRegistry {
private completed: string[] = [];
private rules: [string, string][] = [];
private styleElement: HTMLStyleElement | null = null;
constructor(isDefault?: true) {
if (isDefault) {
console.warn(
"Warning: Yoshiki was used without a top level StyleRegistry. SSR won't be supported.",
);
}
}
addRule(key: string, rule: string) {
this.rules.push([key, rule]);
}
addRules(keys: string[], rules: string[]) {
// I'm sad that sequence is not a thing...
for (let i = 0; i < keys.length; i++) {
this.rules.push([keys[i], rules[i]]);
}
}
flush(): string[] {
const ret = this.rules.filter(([key]) => !this.completed.includes(key));
console.log(ret);
this.rules = [];
this.completed.push(...ret.map(([key]) => key));
return ret.map(([, value]) => value);
}
flushToBrowser() {
if (!this.styleElement) {
const styles = document.querySelectorAll<HTMLStyleElement>("style[data-yoshiki]");
for (const style of styles) {
this.completed.push(...(style.dataset.yoshiki ?? " ").split(" "));
if (!this.styleElement) {
this.styleElement = style;
style.dataset.yoshiki = "";
} else {
this.styleElement.textContent = [this.styleElement.textContent, style.textContent].join(
"\n",
);
style.remove();
}
}
}
const toFlush = this.flush();
if (!toFlush.length) return;
if (!this.styleElement) {
document.head.insertAdjacentHTML(
"beforeend",
`<style data-yoshiki="">${toFlush.join("\n")}</style>`,
);
} else {
this.styleElement.textContent = [this.styleElement.textContent, ...toFlush].join("\n");
}
}
flushToComponent() {
const toFlush = this.flush();
if (!toFlush.length) return null;
return <style data-yoshiki={this.completed.join(" ")}>{toFlush.join("\n")}</style>;
}
}
const RegistryContext = createContext<StyleRegistry | null>(null);
export const StyleRegistryProvider = ({
registry,
children,
}: {
registry?: StyleRegistry;
children: ReactNode;
}) => {
if (!registry && typeof window === "undefined") return children;
return (
<RegistryContext.Provider value={registry ?? createStyleRegistry()}>
{children}
</RegistryContext.Provider>
);
};
export const useStyleRegistry = () => useContext(RegistryContext) || new StyleRegistry(true);
export const createStyleRegistry = () => new StyleRegistry();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"declaration": true,
"sourceMap": true,
+2 -2
View File
@@ -9509,7 +9509,7 @@ react-dom@18.0.0:
loose-envify "^1.1.0"
scheduler "^0.21.0"
react-dom@18.2.0:
react-dom@18.2.0, react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
@@ -9632,7 +9632,7 @@ react@18.0.0:
dependencies:
loose-envify "^1.1.0"
react@18.2.0:
react@18.2.0, react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==