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:
parent
e31dd4a218
commit
ac7607f977
47 changed files with 435 additions and 1085 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
4
examples/with-apollo/next.config.mjs
Normal file
4
examples/with-apollo/next.config.mjs
Normal file
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
BIN
examples/with-apollo/src/app/favicon.ico
Normal file
BIN
examples/with-apollo/src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
107
examples/with-apollo/src/app/globals.css
Normal file
107
examples/with-apollo/src/app/globals.css
Normal 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;
|
||||
}
|
||||
}
|
22
examples/with-apollo/src/app/layout.tsx
Normal file
22
examples/with-apollo/src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
9
examples/with-apollo/src/app/page.tsx
Normal file
9
examples/with-apollo/src/app/page.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { ApolloClientProvider, RepoList } from "@/client-components";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<ApolloClientProvider>
|
||||
<RepoList />
|
||||
</ApolloClientProvider>
|
||||
);
|
||||
}
|
|
@ -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>;
|
||||
}
|
3
examples/with-apollo/src/client-components/index.ts
Normal file
3
examples/with-apollo/src/client-components/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
"use client";
|
||||
export { ApolloClientProvider } from "./apollo-client-provider";
|
||||
export { RepoList } from "./repo-list";
|
17
examples/with-apollo/src/client-components/repo-list.tsx
Normal file
17
examples/with-apollo/src/client-components/repo-list.tsx
Normal 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>;
|
||||
}
|
26
examples/with-apollo/tsconfig.json
Normal file
26
examples/with-apollo/tsconfig.json
Normal 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"]
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export default function App({ children }) {
|
||||
return <main>{children}</main>;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
4
examples/with-graphql-hooks/next.config.mjs
Normal file
4
examples/with-graphql-hooks/next.config.mjs
Normal file
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
BIN
examples/with-graphql-hooks/src/app/favicon.ico
Normal file
BIN
examples/with-graphql-hooks/src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
107
examples/with-graphql-hooks/src/app/globals.css
Normal file
107
examples/with-graphql-hooks/src/app/globals.css
Normal 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;
|
||||
}
|
||||
}
|
22
examples/with-graphql-hooks/src/app/layout.tsx
Normal file
22
examples/with-graphql-hooks/src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
9
examples/with-graphql-hooks/src/app/page.tsx
Normal file
9
examples/with-graphql-hooks/src/app/page.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { ClientContextProvider, RepoList } from "@/client-components";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<ClientContextProvider>
|
||||
<RepoList />
|
||||
</ClientContextProvider>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
"use client";
|
||||
export { ClientContextProvider } from "./client-context-provider";
|
||||
export { RepoList } from "./repo-list";
|
|
@ -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>;
|
||||
}
|
|
@ -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;
|
||||
}
|
26
examples/with-graphql-hooks/tsconfig.json
Normal file
26
examples/with-graphql-hooks/tsconfig.json
Normal 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"]
|
||||
}
|
Loading…
Reference in a new issue