mirror of
https://github.com/zoriya/yoshiki.git
synced 2026-06-03 02:52:14 +00:00
Add SSR support
This commit is contained in:
@@ -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
@@ -19,7 +19,8 @@
|
||||
"**/react-dom",
|
||||
"**/react-dom/**",
|
||||
"**/react-native",
|
||||
"**/react-native/**"
|
||||
"**/react-native/**",
|
||||
"**/typescript/**"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.24",
|
||||
"react": "^18.2.0",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es6",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user