Skip to content

Commit 8a15564

Browse files
committed
first commit
0 parents  commit 8a15564

File tree

9 files changed

+2358
-0
lines changed

9 files changed

+2358
-0
lines changed

.eslintrc.json

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"env": {
3+
"node": true,
4+
"es6": true,
5+
"browser": true
6+
},
7+
"extends": [
8+
"eslint:recommended",
9+
"plugin:prettier/recommended",
10+
"plugin:@typescript-eslint/recommended"
11+
],
12+
"parser": "@typescript-eslint/parser",
13+
"parserOptions": {
14+
"ecmaVersion": 6,
15+
"sourceType": "module",
16+
"ecmaFeatures": {
17+
"modules": true
18+
}
19+
},
20+
"plugins": [
21+
"@typescript-eslint",
22+
"import"
23+
],
24+
"rules": {
25+
"prettier/prettier": "error",
26+
"no-empty": 0,
27+
"@typescript-eslint/explicit-module-boundary-types": 0,
28+
"@typescript-eslint/no-non-null-assertion": 0,
29+
"@typescript-eslint/no-var-requires": 0,
30+
"import/order": [
31+
"error",
32+
{
33+
"groups": [
34+
"builtin",
35+
"external",
36+
"internal",
37+
[
38+
"parent",
39+
"sibling"
40+
],
41+
"object",
42+
"type",
43+
"index"
44+
],
45+
"pathGroupsExcludedImportTypes": [
46+
"builtin"
47+
],
48+
"alphabetize": {
49+
"order": "asc",
50+
"caseInsensitive": true
51+
},
52+
"pathGroups": [
53+
{
54+
"pattern": "@/components/common",
55+
"group": "internal",
56+
"position": "before"
57+
},
58+
{
59+
"pattern": "@/components/hooks",
60+
"group": "internal",
61+
"position": "before"
62+
}
63+
]
64+
}
65+
]
66+
}
67+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dist
2+
node_modules
3+
test

.npmignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.github
2+
/node_modules
3+
/src
4+
.eslintrc.json
5+
.gitignore
6+
tsconfig.json
7+
yarn.lock
8+

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 SoraKumo
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# @react-libraries/next-exchange-ssr
2+
3+
SSR on Next.js using @apollo/client's useSuspenseQuery.
4+
To use it, simply add ApolloSSRProvider under ApolloProvider.
5+
6+
## Sample
7+
8+
- Source
9+
<https://github.com/SoraKumo001/next-apollo-ssr>
10+
- App
11+
<https://next-apollo-ssr-six.vercel.app/>
12+
13+
### src/pages/\_app.tsx
14+
15+
```tsx
16+
import type { AppType } from "next/app";
17+
import { useState } from "react";
18+
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
19+
import { ApolloSSRProvider } from "@react-libraries/apollo-ssr";
20+
21+
const uri = "https://graphql.anilist.co";
22+
23+
const App: AppType = ({ Component }) => {
24+
const [client] = useState(
25+
() =>
26+
new ApolloClient({
27+
uri,
28+
cache: new InMemoryCache({}),
29+
})
30+
);
31+
return (
32+
<ApolloProvider client={client}>
33+
{/* ←Add this */}
34+
<ApolloSSRProvider>
35+
<Component />
36+
</ApolloSSRProvider>
37+
</ApolloProvider>
38+
);
39+
};
40+
41+
// getInitialProps itself is not needed, but it is needed to prevent optimization of _app.tsx
42+
// If you don't include this, it will be executed at build time and will not be called after that.
43+
App.getInitialProps = () => ({});
44+
45+
export default App;
46+
```
47+
48+
### src/pages/index.tsx
49+
50+
```tsx
51+
import { gql, useApolloClient, useSuspenseQuery } from "@apollo/client";
52+
import Link from "next/link";
53+
import { useRouter } from "next/router";
54+
import { Suspense } from "react";
55+
56+
// Retrieving the animation list
57+
const QUERY = gql`
58+
query Query($page: Int, $perPage: Int) {
59+
Page(page: $page, perPage: $perPage) {
60+
media {
61+
id
62+
title {
63+
english
64+
native
65+
}
66+
}
67+
pageInfo {
68+
currentPage
69+
hasNextPage
70+
lastPage
71+
perPage
72+
total
73+
}
74+
}
75+
}
76+
`;
77+
78+
type PageData = {
79+
Page: {
80+
media: {
81+
id: number;
82+
siteUrl: string;
83+
title: { english: string; native: string };
84+
}[];
85+
pageInfo: {
86+
currentPage: number;
87+
hasNextPage: boolean;
88+
lastPage: number;
89+
perPage: number;
90+
total: number;
91+
};
92+
};
93+
};
94+
95+
const AnimationList = ({ page }: { page: number }) => {
96+
const client = useApolloClient();
97+
const { data, refetch } = useSuspenseQuery<PageData>(QUERY, {
98+
variables: { page, perPage: 10 },
99+
});
100+
const { currentPage, lastPage } = data?.Page?.pageInfo ?? {};
101+
return (
102+
<>
103+
<button onClick={() => refetch()}>Refetch</button>
104+
<button onClick={() => client.resetStore()}>Reset</button>
105+
<div>
106+
<Link href={`/?page=${currentPage - 1}`}>
107+
<button disabled={currentPage <= 1}>←</button>
108+
</Link>
109+
<Link href={`/?page=${currentPage + 1}`}>
110+
<button disabled={currentPage >= lastPage}>→</button>
111+
</Link>
112+
{currentPage}/{lastPage}
113+
</div>
114+
{data.Page.media.map((v) => (
115+
<div
116+
key={v.id}
117+
style={{
118+
border: "solid 1px",
119+
padding: "8px",
120+
margin: "8px",
121+
borderRadius: "4px",
122+
}}
123+
>
124+
<div>
125+
{v.title.english} / {v.title.native}
126+
</div>
127+
<a href={v.siteUrl}>{v.siteUrl}</a>
128+
</div>
129+
))}
130+
</>
131+
);
132+
};
133+
134+
const Page = () => {
135+
const router = useRouter();
136+
const page = Number(router.query.page) || 1;
137+
138+
return (
139+
<Suspense fallback={<div>Loading</div>}>
140+
<AnimationList page={page} />
141+
</Suspense>
142+
);
143+
};
144+
145+
export default Page;
146+
```

package.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "@react-libraries/apollo-ssr",
3+
"version": "0.0.1",
4+
"main": "dist/index.js",
5+
"license": "MIT",
6+
"scripts": {
7+
"build": "tsc -b",
8+
"lint:fix": "eslint --fix && prettier -w src"
9+
},
10+
"publishConfig": {
11+
"access": "public"
12+
},
13+
"keywords": [
14+
"GraphQL",
15+
"apollo",
16+
"client",
17+
"Next.js",
18+
"nextjs",
19+
"typescript",
20+
"suspense",
21+
"ssr"
22+
],
23+
"devDependencies": {
24+
"@apollo/client": "^3.8.1",
25+
"@types/react": "^18.2.20",
26+
"eslint": "^8.47.0",
27+
"eslint-config-next": "^13.4.16",
28+
"eslint-config-prettier": "^9.0.0",
29+
"eslint-plugin-import": "^2.28.0",
30+
"prettier": "^2.8.8",
31+
"react": "^18.2.0",
32+
"typescript": "^5.1.6"
33+
}
34+
}

src/index.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { ApolloClient, ObservableQuery, useApolloClient } from "@apollo/client";
2+
import { getSuspenseCache } from "@apollo/client/react/cache";
3+
import { InternalQueryReference } from "@apollo/client/react/cache/QueryReference";
4+
import {
5+
SuspenseCache,
6+
SuspenseCacheOptions,
7+
} from "@apollo/client/react/cache/SuspenseCache";
8+
import { CacheKey } from "@apollo/client/react/cache/types";
9+
import { Fragment, ReactNode, createElement, useRef } from "react";
10+
11+
class SSRCache extends SuspenseCache {
12+
constructor(options: SuspenseCacheOptions = Object.create(null)) {
13+
super(options);
14+
}
15+
getQueryRef<TData = any>(
16+
cacheKey: CacheKey,
17+
createObservable: () => ObservableQuery<TData>
18+
) {
19+
const ref = super.getQueryRef(cacheKey, createObservable);
20+
this.refs.add(ref);
21+
return ref;
22+
}
23+
24+
finished = false;
25+
refs = new Set<InternalQueryReference<any>>();
26+
}
27+
28+
const DATA_NAME = "__NEXT_DATA_PROMISE__";
29+
30+
const DataRender = () => {
31+
const client = useApolloClient();
32+
const cache = getSuspenseCache(client);
33+
if (typeof window === "undefined") {
34+
if (!(cache instanceof SSRCache)) {
35+
throw new Error("SSRCache missing.");
36+
}
37+
if (!cache.finished) {
38+
throw Promise.allSettled(
39+
Array.from(cache.refs.values()).map(({ promise }) => promise)
40+
).then((v) => {
41+
cache.finished = true;
42+
return v;
43+
});
44+
}
45+
}
46+
return createElement("script", {
47+
id: DATA_NAME,
48+
type: "application/json",
49+
dangerouslySetInnerHTML: {
50+
__html: JSON.stringify(client.extract()).replace(/</g, "\\u003c"),
51+
},
52+
});
53+
};
54+
55+
const useApolloCache = <T>(
56+
client: ApolloClient<T> & {
57+
[suspenseCacheSymbol]?: SuspenseCache;
58+
}
59+
) => {
60+
const property = useRef<{ initialized?: boolean }>({}).current;
61+
if (typeof window !== "undefined") {
62+
if (!property.initialized) {
63+
const node = document.getElementById(DATA_NAME);
64+
if (node) client.restore(JSON.parse(node.innerHTML));
65+
property.initialized = true;
66+
}
67+
} else {
68+
if (!client[suspenseCacheSymbol]) {
69+
client[suspenseCacheSymbol] = new SSRCache(
70+
client.defaultOptions.react?.suspense
71+
);
72+
}
73+
}
74+
};
75+
76+
const suspenseCacheSymbol = Symbol.for("apollo.suspenseCache");
77+
78+
export const ApolloSSRProvider = ({ children }: { children: ReactNode }) => {
79+
const client = useApolloClient();
80+
useApolloCache(client);
81+
return createElement(Fragment, {}, children, createElement(DataRender));
82+
};

0 commit comments

Comments
 (0)