fix(examples): invalid with-apollo and with-graphql-hooks (#64186)

### Why?

The examples `with-apollo` and `with-graphql-hooks` included outdated
API endpoints and packages.
This resulted in the **failed Vercel Deployment** of the examples.

<details><summary>Screenshot of failed deployments</summary>
<p>

#### with-graphql-hooks

![Screenshot 2024-04-08 at 3 04
05 PM](https://github.com/vercel/next.js/assets/120007119/93be6aca-e408-4b93-bf6c-04d8dfc9b59c)

#### with-apollo

![Screenshot 2024-04-08 at 3 05
26 PM](https://github.com/vercel/next.js/assets/120007119/4dff9e20-714c-4a12-a27d-8fae4fc5c61d)

</p>
</details> 

### How?

- Migrated examples from `pages` to `app` router and removed invalid API
endpoints.
- Refactored the example to a minimal template as possible with
essential features.

Closes #9865 #10253 #36112

---------

Co-authored-by: Sam Ko <sam@vercel.com>
This commit is contained in:
Jiwon Choi 2024-04-08 23:55:39 +09:00 committed by GitHub
parent e31dd4a218
commit ac7607f977
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 435 additions and 1085 deletions

View file

@ -4,12 +4,6 @@
In this simple example, we integrate Apollo seamlessly with [Next.js data fetching methods](https://nextjs.org/docs/basic-features/data-fetching) to fetch queries in the server and hydrate them in the browser.
This example relies on [Prisma + Nexus](https://github.com/prisma-labs/nextjs-graphql-api-examples) for its GraphQL backend.
## Demo
[https://next-with-apollo.vercel.app](https://next-with-apollo.vercel.app)
## Deploy your own
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
@ -18,7 +12,7 @@ Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_mediu
## How to use
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
```bash
npx create-next-app --example with-apollo with-apollo-app

View file

@ -1,47 +0,0 @@
export default function App({ children }) {
return (
<main>
{children}
<style jsx global>{`
* {
font-family: Menlo, Monaco, "Lucida Console", "Liberation Mono",
"DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New",
monospace, serif;
}
body {
margin: 0;
padding: 25px 50px;
}
a {
color: #22bad9;
}
p {
font-size: 14px;
line-height: 24px;
}
article {
margin: 0 auto;
max-width: 650px;
}
button {
align-items: center;
background-color: #22bad9;
border: 0;
color: white;
display: flex;
padding: 5px 7px;
transition: background-color 0.3s;
}
button:active {
background-color: #1b9db7;
}
button:disabled {
background-color: #b5bebf;
}
button:focus {
outline: none;
}
`}</style>
</main>
);
}

View file

@ -1,15 +0,0 @@
export default function ErrorMessage({ message }) {
return (
<aside>
{message}
<style jsx>{`
aside {
padding: 1.5em;
font-size: 14px;
color: white;
background-color: red;
}
`}</style>
</aside>
);
}

View file

@ -1,38 +0,0 @@
import { useRouter } from "next/router";
import Link from "next/link";
export default function Header() {
const { pathname } = useRouter();
return (
<header>
<Link href="/" legacyBehavior>
<a className={pathname === "/" ? "is-active" : ""}>Home</a>
</Link>
<Link href="/about" legacyBehavior>
<a className={pathname === "/about" ? "is-active" : ""}>About</a>
</Link>
<Link href="/client-only" legacyBehavior>
<a className={pathname === "/client-only" ? "is-active" : ""}>
Client-Only
</a>
</Link>
<Link href="/ssr" legacyBehavior>
<a className={pathname === "/ssr" ? "is-active" : ""}>SSR</a>
</Link>
<style jsx>{`
header {
margin-bottom: 25px;
}
a {
font-size: 14px;
margin-right: 15px;
text-decoration: none;
}
.is-active {
text-decoration: underline;
}
`}</style>
</header>
);
}

View file

@ -1,17 +0,0 @@
const InfoBox = ({ children }) => (
<div className="info">
<style jsx>{`
.info {
margin-top: 20px;
margin-bottom: 20px;
padding-top: 20px;
padding-bottom: 20px;
border-top: 1px solid #ececec;
border-bottom: 1px solid #ececec;
}
`}</style>
{children}
</div>
);
export default InfoBox;

View file

@ -1,111 +0,0 @@
import { gql, useQuery, NetworkStatus } from "@apollo/client";
import ErrorMessage from "./ErrorMessage";
import PostUpvoter from "./PostUpvoter";
export const ALL_POSTS_QUERY = gql`
query allPosts($first: Int!, $skip: Int!) {
allPosts(orderBy: { createdAt: desc }, first: $first, skip: $skip) {
id
title
votes
url
createdAt
}
_allPostsMeta {
count
}
}
`;
export const allPostsQueryVars = {
skip: 0,
first: 10,
};
export default function PostList() {
const { loading, error, data, fetchMore, networkStatus } = useQuery(
ALL_POSTS_QUERY,
{
variables: allPostsQueryVars,
// Setting this value to true will make the component rerender when
// the "networkStatus" changes, so we are able to know if it is fetching
// more data
notifyOnNetworkStatusChange: true,
},
);
const loadingMorePosts = networkStatus === NetworkStatus.fetchMore;
const { allPosts, _allPostsMeta } = data;
const loadMorePosts = () => {
fetchMore({
variables: {
skip: allPosts.length,
},
});
};
if (error) return <ErrorMessage message="Error loading posts." />;
if (loading && !loadingMorePosts) return <div>Loading</div>;
const areMorePosts = allPosts.length < _allPostsMeta.count;
return (
<section>
<ul>
{allPosts.map((post, index) => (
<li key={post.id}>
<div>
<span>{index + 1}. </span>
<a href={post.url}>{post.title}</a>
<PostUpvoter id={post.id} votes={post.votes} />
</div>
</li>
))}
</ul>
{areMorePosts && (
<button onClick={() => loadMorePosts()} disabled={loadingMorePosts}>
{loadingMorePosts ? "Loading..." : "Show More"}
</button>
)}
<style jsx>{`
section {
padding-bottom: 20px;
}
li {
display: block;
margin-bottom: 10px;
}
div {
align-items: center;
display: flex;
}
a {
font-size: 14px;
margin-right: 10px;
text-decoration: none;
padding-bottom: 0;
border: 0;
}
span {
font-size: 14px;
margin-right: 5px;
}
ul {
margin: 0;
padding: 0;
}
button:before {
align-self: center;
border-style: solid;
border-width: 6px 4px 0 4px;
border-color: #ffffff transparent transparent transparent;
content: "";
height: 0;
margin-right: 5px;
width: 0;
}
`}</style>
</section>
);
}

View file

@ -1,57 +0,0 @@
import { gql, useMutation } from "@apollo/client";
const UPDATE_POST_MUTATION = gql`
mutation votePost($id: String!) {
votePost(id: $id) {
id
votes
__typename
}
}
`;
export default function PostUpvoter({ votes, id }) {
const [updatePost] = useMutation(UPDATE_POST_MUTATION);
const upvotePost = () => {
updatePost({
variables: {
id,
},
optimisticResponse: {
__typename: "Mutation",
votePost: {
__typename: "Post",
id,
votes: votes + 1,
},
},
});
};
return (
<button onClick={() => upvotePost()}>
{votes}
<style jsx>{`
button {
background-color: transparent;
border: 1px solid #e4e4e4;
color: #000;
}
button:active {
background-color: transparent;
}
button:before {
align-self: center;
border-color: transparent transparent #000000 transparent;
border-style: solid;
border-width: 0 4px 6px 4px;
content: "";
height: 0;
margin-right: 5px;
width: 0;
}
`}</style>
</button>
);
}

View file

@ -1,73 +0,0 @@
import { gql, useMutation } from "@apollo/client";
const CREATE_POST_MUTATION = gql`
mutation createPost($title: String!, $url: String!) {
createPost(title: $title, url: $url) {
id
title
votes
url
createdAt
}
}
`;
export default function Submit() {
const [createPost, { loading }] = useMutation(CREATE_POST_MUTATION);
const handleSubmit = (event) => {
event.preventDefault();
const form = event.target;
const formData = new window.FormData(form);
const title = formData.get("title");
const url = formData.get("url");
form.reset();
createPost({
variables: { title, url },
update: (cache, { data: { createPost } }) => {
cache.modify({
fields: {
allPosts(existingPosts = []) {
const newPostRef = cache.writeFragment({
data: createPost,
fragment: gql`
fragment NewPost on allPosts {
id
type
}
`,
});
return [newPostRef, ...existingPosts];
},
},
});
},
});
};
return (
<form onSubmit={handleSubmit}>
<h1>Submit</h1>
<input placeholder="title" name="title" type="text" required />
<input placeholder="url" name="url" type="url" required />
<button type="submit" disabled={loading}>
Submit
</button>
<style jsx>{`
form {
border-bottom: 1px solid #ececec;
padding-bottom: 20px;
margin-bottom: 20px;
}
h1 {
font-size: 20px;
}
input {
display: block;
margin-bottom: 10px;
}
`}</style>
</form>
);
}

View file

@ -1,86 +0,0 @@
import { useMemo } from "react";
import { ApolloClient, HttpLink, InMemoryCache, from } from "@apollo/client";
import { onError } from "@apollo/client/link/error";
import { concatPagination } from "@apollo/client/utilities";
import merge from "deepmerge";
import isEqual from "lodash/isEqual";
export const APOLLO_STATE_PROP_NAME = "__APOLLO_STATE__";
let apolloClient;
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
if (networkError) console.log(`[Network error]: ${networkError}`);
});
const httpLink = new HttpLink({
uri: "https://nextjs-graphql-with-prisma-simple.vercel.app/api", // Server URL (must be absolute)
credentials: "same-origin", // Additional fetch() options like `credentials` or `headers`
});
function createApolloClient() {
return new ApolloClient({
ssrMode: typeof window === "undefined",
link: from([errorLink, httpLink]),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
allPosts: concatPagination(),
},
},
},
}),
});
}
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient();
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// gets hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract();
// Merge the initialState from getStaticProps/getServerSideProps in the existing cache
const data = merge(existingCache, initialState, {
// combine arrays using object equality (like in sets)
arrayMerge: (destinationArray, sourceArray) => [
...sourceArray,
...destinationArray.filter((d) =>
sourceArray.every((s) => !isEqual(d, s)),
),
],
});
// Restore the cache with the merged data
_apolloClient.cache.restore(data);
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === "undefined") return _apolloClient;
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
}
export function addApolloState(client, pageProps) {
if (pageProps?.props) {
pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
}
return pageProps;
}
export function useApollo(pageProps) {
const state = pageProps[APOLLO_STATE_PROP_NAME];
const store = useMemo(() => initializeApollo(state), [state]);
return store;
}

View file

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

View file

@ -1,18 +1,22 @@
{
"private": true,
"scripts": {
"dev": "next",
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@apollo/client": "3.1.1",
"deepmerge": "^4.2.2",
"lodash": "4.17.20",
"graphql": "^15.3.0",
"next": "latest",
"prop-types": "^15.6.2",
"@apollo/client": "^3.9.10",
"graphql": "^16.8.1",
"next": "^14.1.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^20.12.5",
"@types/react": "^18.2.74",
"@types/react-dom": "^18.2.24",
"typescript": "^5.4.4"
}
}

View file

@ -1,12 +0,0 @@
import { ApolloProvider } from "@apollo/client";
import { useApollo } from "../lib/apolloClient";
export default function App({ Component, pageProps }) {
const apolloClient = useApollo(pageProps);
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
);
}

View file

@ -1,45 +0,0 @@
import App from "../components/App";
import Header from "../components/Header";
const AboutPage = () => (
<App>
<Header />
<article>
<h1>The Idea Behind This Example</h1>
<p>
<a href="https://www.apollographql.com/client/">Apollo</a> is a GraphQL
client that allows you to easily query the exact data you need from a
GraphQL server. In addition to fetching and mutating data, Apollo
analyzes your queries and their results to construct a client-side cache
of your data, which is kept up to date as further queries and mutations
are run, fetching more results from the server.
</p>
<p>
In this simple example, we integrate Apollo seamlessly with{" "}
<a href="https://github.com/vercel/next.js">Next</a> by calling{" "}
<a href="https://nextjs.org/docs/basic-features/data-fetching/get-static-props">
getStaticProps
</a>{" "}
at our Page component. This approach lets us opt out of getInitialProps
and let us use all the niceties provided by{" "}
<a href="https://github.com/vercel/next.js">Next</a>.
</p>
<p>
On initial page load, while on the server and inside{" "}
<a href="https://nextjs.org/docs/basic-features/data-fetching/get-static-props">
getStaticProps
</a>
, we fetch the query used to get the list of posts. At the point in
which the query promise resolves, our Apollo Client store is completely
initialized. Then we serve the initial HTML with the fetched data and
hydrate Apollo in the browser.
</p>
<p>
This example relies on <a href="http://graph.cool">graph.cool</a> for
its GraphQL backend.
</p>
</article>
</App>
);
export default AboutPage;

View file

@ -1,21 +0,0 @@
import App from "../components/App";
import InfoBox from "../components/InfoBox";
import Header from "../components/Header";
import Submit from "../components/Submit";
import PostList from "../components/PostList";
const ClientOnlyPage = (props) => (
<App>
<Header />
<InfoBox>
This page shows how to use Apollo only in the client. If you{" "}
<a href="/client-only">reload</a> this page, you will see a loader since
Apollo didn't fetch any data on the server. This is useful when the page
doesn't have SEO requirements or blocking data fetching requirements.
</InfoBox>
<Submit />
<PostList />
</App>
);
export default ClientOnlyPage;

View file

@ -1,34 +0,0 @@
import App from "../components/App";
import InfoBox from "../components/InfoBox";
import Header from "../components/Header";
import Submit from "../components/Submit";
import PostList, {
ALL_POSTS_QUERY,
allPostsQueryVars,
} from "../components/PostList";
import { initializeApollo, addApolloState } from "../lib/apolloClient";
const IndexPage = () => (
<App>
<Header />
<InfoBox> This page shows how to use SSG with Apollo.</InfoBox>
<Submit />
<PostList />
</App>
);
export async function getStaticProps() {
const apolloClient = initializeApollo();
await apolloClient.query({
query: ALL_POSTS_QUERY,
variables: allPostsQueryVars,
});
return addApolloState(apolloClient, {
props: {},
revalidate: 1,
});
}
export default IndexPage;

View file

@ -1,33 +0,0 @@
import App from "../components/App";
import InfoBox from "../components/InfoBox";
import Header from "../components/Header";
import Submit from "../components/Submit";
import PostList, {
ALL_POSTS_QUERY,
allPostsQueryVars,
} from "../components/PostList";
import { initializeApollo, addApolloState } from "../lib/apolloClient";
const SSRPage = () => (
<App>
<Header />
<InfoBox> This page shows how to use SSR with Apollo.</InfoBox>
<Submit />
<PostList />
</App>
);
export async function getServerSideProps() {
const apolloClient = initializeApollo();
await apolloClient.query({
query: ALL_POSTS_QUERY,
variables: allPostsQueryVars,
});
return addApolloState(apolloClient, {
props: {},
});
}
export default SSRPage;

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,107 @@
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}

View file

@ -0,0 +1,22 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Next.js with Apollo",
description: "Next.js example with Apollo GraphQL",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}

View file

@ -0,0 +1,9 @@
import { ApolloClientProvider, RepoList } from "@/client-components";
export default async function Home() {
return (
<ApolloClientProvider>
<RepoList />
</ApolloClientProvider>
);
}

View file

@ -0,0 +1,16 @@
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
export const client = new ApolloClient({
// URL retrieved from official apollo graphql blog
// See https://www.apollographql.com/blog/using-apollo-client-with-next-js-13-releasing-an-official-library-to-support-the-app-router
uri: "https://main--spacex-l4uc6p.apollographos.net/graphql",
cache: new InMemoryCache(),
});
export function ApolloClientProvider({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <ApolloProvider client={client}>{children}</ApolloProvider>;
}

View file

@ -0,0 +1,3 @@
"use client";
export { ApolloClientProvider } from "./apollo-client-provider";
export { RepoList } from "./repo-list";

View file

@ -0,0 +1,17 @@
import { gql, useQuery } from "@apollo/client";
const getRepo = gql`
query {
launchLatest {
mission_name
}
}
`;
export function RepoList() {
const { loading, error, data } = useQuery(getRepo);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {JSON.stringify(error)}</p>;
return <div>{data.launchLatest.mission_name}</div>;
}

View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View file

@ -1,10 +1,10 @@
# GraphQL Hooks Example
[GraphQL Hooks](https://github.com/nearform/graphql-hooks) is a library from NearForm that intends to be a minimal hooks-first GraphQL client. Providing a similar API to Apollo.
[GraphQL Hooks](https://github.com/nearform/graphql-hooks) is a library from NearForm that intends to be a minimal hooks-first GraphQL client, providing a similar API to Apollo.
You'll see this shares the same [graph.cool](https://www.graph.cool) backend as the Apollo example, this is so you can compare the two side by side. The app itself should also look identical.
You'll see this shares the same layout as the `with-apollo` example, this is so you can compare the two side by side. The app itself should also look identical.
This started life as a copy of the `with-apollo` example. We then stripped out Apollo and replaced it with `graphql-hooks`. This was mostly as an exercise in ensuring basic functionality could be achieved in a similar way to Apollo. The [bundle size](https://bundlephobia.com/result?p=graphql-hooks@3.2.1) of `graphql-hooks` is tiny in comparison to Apollo and should cover a fair amount of use cases.
This started life as a copy of the `with-apollo` example. We then stripped out Apollo and replaced it with `graphql-hooks`. This was mostly as an exercise in ensuring basic functionality could be achieved in a similar way to Apollo. The [bundle size](https://bundlephobia.com/result?p=graphql-hooks) of `graphql-hooks` is tiny in comparison to Apollo and should cover a fair amount of use cases.
## Deploy your own
@ -14,7 +14,7 @@ Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_mediu
## How to use
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
```bash
npx create-next-app --example with-graphql-hooks with-graphql-hooks-app

View file

@ -1,3 +0,0 @@
export default function App({ children }) {
return <main>{children}</main>;
}

View file

@ -1,15 +0,0 @@
export default function ErrorMessage({ message }) {
return (
<aside>
{message}
<style jsx>{`
aside {
padding: 1.5em;
font-size: 14px;
color: white;
background-color: red;
}
`}</style>
</aside>
);
}

View file

@ -1,30 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
export default function Header() {
const { pathname } = useRouter();
return (
<header>
<Link href="/" legacyBehavior>
<a className={pathname === "/" ? "is-active" : ""}>Home</a>
</Link>
<Link href="/about" legacyBehavior>
<a className={pathname === "/about" ? "is-active" : ""}>About</a>
</Link>
<style jsx>{`
header {
margin-bottom: 25px;
}
a {
font-size: 14px;
margin-right: 15px;
text-decoration: none;
}
.is-active {
text-decoration: underline;
}
`}</style>
</header>
);
}

View file

@ -1,81 +0,0 @@
import { useState } from "react";
import { useQuery } from "graphql-hooks";
import ErrorMessage from "./error-message";
import PostUpvoter from "./post-upvoter";
import Submit from "./submit";
export const ALL_POSTS_QUERY = `
query allPosts($first: Int!, $skip: Int!) {
allPosts(orderBy: { createdAt: desc }, first: $first, skip: $skip) {
id
title
votes
url
createdAt
}
_allPostsMeta {
count
}
}
`;
export const allPostsQueryOptions = (skip = 0) => ({
variables: { skip, first: 10 },
updateData: (prevResult, result) => ({
...result,
allPosts: prevResult
? [...prevResult.allPosts, ...result.allPosts]
: result.allPosts,
}),
});
export default function PostList() {
const [skip, setSkip] = useState(0);
const { loading, error, data, refetch } = useQuery(
ALL_POSTS_QUERY,
allPostsQueryOptions(skip),
);
if (error) return <ErrorMessage message="Error loading posts." />;
if (!data) return <div>Loading</div>;
const { allPosts, _allPostsMeta } = data;
const areMorePosts = allPosts.length < _allPostsMeta.count;
return (
<>
<Submit
onSubmission={() => {
refetch({ variables: { skip: 0, first: allPosts.length } });
}}
/>
<section>
<ul>
{allPosts.map((post, index) => (
<li key={post.id}>
<div>
<span>{index + 1}. </span>
<a href={post.url}>{post.title}</a>
<PostUpvoter
id={post.id}
votes={post.votes}
onUpdate={() => {
refetch({ variables: { skip: 0, first: allPosts.length } });
}}
/>
</div>
</li>
))}
</ul>
{areMorePosts ? (
<button className="more" onClick={() => setSkip(skip + 10)}>
{" "}
{loading && !data ? "Loading..." : "Show More"}{" "}
</button>
) : (
""
)}
</section>
</>
);
}

View file

@ -1,37 +0,0 @@
import React from "react";
import { useMutation } from "graphql-hooks";
const UPDATE_POST = `
mutation votePost($id: String!) {
votePost(id: $id) {
id
votes
__typename
}
}
`;
export default function PostUpvoter({ votes, id, onUpdate }) {
const [updatePost] = useMutation(UPDATE_POST);
return (
<button
className="upvote"
onClick={async () => {
try {
const result = await updatePost({
variables: {
id,
},
});
onUpdate && onUpdate(result);
} catch (e) {
console.error("error upvoting post", e);
}
}}
>
{votes}
</button>
);
}

View file

@ -1,56 +0,0 @@
import React from "react";
import { useMutation } from "graphql-hooks";
const CREATE_POST = `
mutation createPost($title: String!, $url: String!) {
createPost(title: $title, url: $url) {
id
title
votes
url
createdAt
}
}`;
async function handleSubmit(event, onSubmission, createPost) {
event.preventDefault();
const form = event.target;
const formData = new window.FormData(form);
const title = formData.get("title");
const url = formData.get("url");
form.reset();
const result = await createPost({
variables: {
title,
url,
},
});
onSubmission && onSubmission(result);
}
export default function Submit({ onSubmission }) {
const [createPost, state] = useMutation(CREATE_POST);
return (
<form onSubmit={(event) => handleSubmit(event, onSubmission, createPost)}>
<h1>Submit</h1>
<input placeholder="title" name="title" type="text" required />
<input placeholder="url" name="url" type="url" required />
<button type="submit">{state.loading ? "Loading..." : "Submit"}</button>
<style jsx>{`
form {
border-bottom: 1px solid #ececec;
padding-bottom: 20px;
margin-bottom: 20px;
}
h1 {
font-size: 20px;
}
input {
display: block;
margin-bottom: 10px;
}
`}</style>
</form>
);
}

View file

@ -1,40 +0,0 @@
import { useMemo } from "react";
import { GraphQLClient } from "graphql-hooks";
import memCache from "graphql-hooks-memcache";
let graphQLClient;
function createClient(initialState) {
return new GraphQLClient({
ssrMode: typeof window === "undefined",
url: "https://nextjs-graphql-with-prisma-simple-foo.vercel.app/api", // Server URL (must be absolute)
cache: memCache({ initialState }),
});
}
export function initializeGraphQL(initialState = null) {
const _graphQLClient = graphQLClient ?? createClient(initialState);
// After navigating to a page with an initial GraphQL state, create a new cache with the
// current state merged with the incoming state and set it to the GraphQL client.
// This is necessary because the initial state of `memCache` can only be set once
if (initialState && graphQLClient) {
graphQLClient.cache = memCache({
initialState: Object.assign(
graphQLClient.cache.getInitialState(),
initialState,
),
});
}
// For SSG and SSR always create a new GraphQL Client
if (typeof window === "undefined") return _graphQLClient;
// Create the GraphQL Client once in the client
if (!graphQLClient) graphQLClient = _graphQLClient;
return _graphQLClient;
}
export function useGraphQLClient(initialState) {
const store = useMemo(() => initializeGraphQL(initialState), [initialState]);
return store;
}

View file

@ -1,30 +0,0 @@
const defaultOpts = { useCache: true };
/**
* Returns the result of a GraphQL query. It also adds the result to the
* cache of the GraphQL client for better initial data population in pages.
*
* Note: This helper tries to imitate what the query hooks of `graphql-hooks`
* do internally to make sure we generate the same cache key
*/
export default async function graphQLRequest(client, query, options) {
const opts = { ...defaultOpts, ...options };
const operation = {
query,
variables: opts.variables,
operationName: opts.operationName,
persisted: opts.persisted,
};
if (opts.persisted || (client.useGETForQueries && !opts.isMutation)) {
opts.fetchOptionsOverrides = {
...opts.fetchOptionsOverrides,
method: "GET",
};
}
const cacheKey = client.getCacheKey(operation, opts);
const cacheValue = await client.request(operation, opts);
client.saveCache(cacheKey, cacheValue);
return cacheValue;
}

View file

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

View file

@ -1,16 +1,21 @@
{
"private": true,
"scripts": {
"dev": "next",
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"graphql-hooks": "^5.1.0",
"graphql-hooks-memcache": "^2.1.0",
"next": "latest",
"prop-types": "^15.7.2",
"graphql-hooks": "^7.0.0",
"next": "^14.1.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^20.12.5",
"@types/react": "^18.2.74",
"@types/react-dom": "^18.2.24",
"typescript": "^5.4.4"
}
}

View file

@ -1,13 +0,0 @@
import "../styles.css";
import { ClientContext } from "graphql-hooks";
import { useGraphQLClient } from "../lib/graphql-client";
export default function App({ Component, pageProps }) {
const graphQLClient = useGraphQLClient(pageProps.initialGraphQLState);
return (
<ClientContext.Provider value={graphQLClient}>
<Component {...pageProps} />
</ClientContext.Provider>
);
}

View file

@ -1,32 +0,0 @@
import App from "../components/app";
import Header from "../components/header";
export default function About() {
return (
<App>
<Header />
<article>
<h1>The Idea Behind This Example</h1>
<p>
<a href="https://github.com/nearform/graphql-hooks">GraphQL Hooks</a>{" "}
is a library from NearForm that intends to be a minimal hooks-first
GraphQL client. Providing it in a way familiar to Apollo users.
</p>
<p>
This started life as a copy of the `with-apollo` example. We then
stripped out Apollo and replaced it with `graphql-hooks`. This was
mostly as an exercise in ensuring basic functionality could be
achieved in a similar way to Apollo.
</p>
<p>
You'll see this shares the same{" "}
<a href="https://www.graph.cool">graph.cool</a> backend as the Apollo
example, this is so you can compare the two side by side. The app
itself should also look identical.
</p>
</article>
</App>
);
}

View file

@ -1,30 +0,0 @@
import { initializeGraphQL } from "../lib/graphql-client";
import graphQLRequest from "../lib/graphql-request";
import App from "../components/app";
import Header from "../components/header";
import PostList, {
ALL_POSTS_QUERY,
allPostsQueryOptions,
} from "../components/post-list";
export default function Home() {
return (
<App>
<Header />
<PostList />
</App>
);
}
export async function getStaticProps() {
const client = initializeGraphQL();
await graphQLRequest(client, ALL_POSTS_QUERY, allPostsQueryOptions());
return {
props: {
initialGraphQLState: client.cache.getInitialState(),
},
revalidate: 1,
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,107 @@
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}

View file

@ -0,0 +1,22 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Next.js with GraphQL Hooks",
description: "Next.js example with GraphQL Hooks",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}

View file

@ -0,0 +1,9 @@
import { ClientContextProvider, RepoList } from "@/client-components";
export default async function Home() {
return (
<ClientContextProvider>
<RepoList />
</ClientContextProvider>
);
}

View file

@ -0,0 +1,15 @@
import { GraphQLClient, ClientContext } from "graphql-hooks";
export const client = new GraphQLClient({
url: "https://main--spacex-l4uc6p.apollographos.net/graphql",
});
export function ClientContextProvider({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ClientContext.Provider value={client}>{children}</ClientContext.Provider>
);
}

View file

@ -0,0 +1,3 @@
"use client";
export { ClientContextProvider } from "./client-context-provider";
export { RepoList } from "./repo-list";

View file

@ -0,0 +1,17 @@
import { useQuery } from "graphql-hooks";
const getRepo = /* GraphQL */ `
query {
launchLatest {
mission_name
}
}
`;
export function RepoList() {
const { loading, error, data } = useQuery(getRepo);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {JSON.stringify(error)}</p>;
return <div>{data.launchLatest.mission_name}</div>;
}

View file

@ -1,104 +0,0 @@
* {
font-family: Menlo, Monaco, "Lucida Console", "Liberation Mono",
"DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace,
serif;
}
body {
margin: 0;
padding: 25px 50px;
}
a {
color: #22bad9;
}
p {
font-size: 14px;
line-height: 24px;
}
article {
margin: 0 auto;
max-width: 650px;
}
form {
border-bottom: 1px solid #ececec;
padding-bottom: 20px;
margin-bottom: 20px;
}
h1 {
font-size: 20px;
}
input {
display: block;
margin-bottom: 10px;
}
section {
padding-bottom: 20px;
}
li {
display: block;
margin-bottom: 10px;
}
div {
align-items: center;
display: flex;
}
a {
font-size: 14px;
margin-right: 10px;
text-decoration: none;
padding-bottom: 0;
border: 0;
}
span {
font-size: 14px;
margin-right: 5px;
}
ul {
margin: 0;
padding: 0;
}
button:before {
align-self: center;
border-style: solid;
content: "";
height: 0;
margin-right: 5px;
width: 0;
}
button {
align-items: center;
background-color: #22bad9;
border: 0;
color: white;
display: flex;
padding: 5px 7px;
}
button:active {
background-color: #1b9db7;
transition: background-color 0.3s;
}
button:focus {
outline: none;
}
button:active {
background-color: transparent;
}
.upvote {
background-color: transparent;
border: 1px solid #e4e4e4;
color: #000;
}
.upvote:before {
border-width: 0 4px 6px 4px;
border-color: transparent transparent #000000 transparent;
}
.more:before {
border-width: 6px 4px 0 4px;
border-color: #ffffff transparent transparent transparent;
}

View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}