mirror of
https://github.com/sergeyyarkov/next-apollo-graphql-setup.git
synced 2026-10-11 10:19:43 +03:00
🤖 pattern
This commit is contained in:
parent
143d6dc399
commit
d845bf1672
3
.gitignore
vendored
3
.gitignore
vendored
@ -23,3 +23,6 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# cms
|
||||
cms
|
||||
|
||||
80
README.md
80
README.md
@ -1,80 +1,6 @@
|
||||
### This is a next-apollo-graphql setup to start develop your web site. This setup uses server side rendering.
|
||||
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
To start the development server, you need to run the following commands below.
|
||||
|
||||
### Using Node
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
Open http://localhost:3000 with your browser to see the result.
|
||||
|
||||
Our application runs on port `3000`, and GraphQL server also runs on `localhost:3000/api/graphql`.
|
||||
|
||||
### Using Nginx
|
||||
```bash
|
||||
docker-compose build && docker-compose up
|
||||
```
|
||||
Our application runs on port `80`, and GraphQL server also runs on `localhost:80/api/graphql`.
|
||||
|
||||
## ⚡ Using Query
|
||||
|
||||
You can easily use `Query` component to send queries to the GraphQL server. Just pass your query to the `Query` component.
|
||||
```jsx
|
||||
<Query query={ARTICLES_QUERY}>
|
||||
{({ data: { articles } }) => {
|
||||
return <Articles articles={articles} />;
|
||||
}}
|
||||
</Query>
|
||||
```
|
||||
|
||||
Use the `withData` HOC to wrap your pages.
|
||||
```jsx
|
||||
|
||||
// ./frontend/pages/articles.js
|
||||
import Layout from '../components/Layout/Layout'
|
||||
import SEO from '../components/SEO/SEO'
|
||||
import Query from '../components/Query/query'
|
||||
import ARTICLES_QUERY from '../apollo/queries/articles/articles';
|
||||
import withData from '../lib/apollo';
|
||||
|
||||
|
||||
const Articles = () => {
|
||||
return (
|
||||
<>
|
||||
<SEO pageTitle='Articles page' />
|
||||
<Layout>
|
||||
<h1>Articles page</h1>
|
||||
<div>
|
||||
<Query query={ARTICLES_QUERY}>
|
||||
{({ data: { articles } }) => {
|
||||
return articles.map(art => {
|
||||
return (
|
||||
<div key={art.id}>
|
||||
<p>
|
||||
title: {art.title} <br />
|
||||
id: {art.id}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}}
|
||||
</Query>
|
||||
</div>
|
||||
</Layout>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default withData(Articles)
|
||||
|
||||
```
|
||||
## License:
|
||||
[MIT](https://github.com/sergeyyarkov/next-apollo-graphql-setup/blob/master/LICENSE)
|
||||
### This is a next-strapi-mongo setup to start develop your web site. This setup uses server side rendering.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
18
apolloClient.js
Normal file
18
apolloClient.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { ApolloClient } from 'apollo-client'
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory'
|
||||
import { HttpLink } from 'apollo-link-http'
|
||||
import fetch from 'isomorphic-unfetch'
|
||||
|
||||
export default function createApolloClient (initialState, ctx) {
|
||||
// The `ctx` (NextPageContext) will only be present on the server.
|
||||
// use it to extract auth headers (ctx.req) or similar.
|
||||
return new ApolloClient({
|
||||
ssrMode: Boolean(ctx),
|
||||
link: new HttpLink({
|
||||
uri: 'https://next-blog-example.herokuapp.com/graphql', // Server URL (must be absolute)
|
||||
credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
|
||||
fetch
|
||||
}),
|
||||
cache: new InMemoryCache().restore(initialState)
|
||||
})
|
||||
}
|
||||
@ -12,3 +12,29 @@ services:
|
||||
build: nginx
|
||||
ports:
|
||||
- '80:80'
|
||||
strapi:
|
||||
container_name: strapi
|
||||
image: strapi/strapi
|
||||
environment:
|
||||
- DATABASE_CLIENT=mongo
|
||||
- DATABASE_HOST=db
|
||||
- DATABASE_PORT=27017
|
||||
- DATABASE_NAME=strapi
|
||||
- DATABASE_USERNAME=
|
||||
- DATABASE_PASSWORD=
|
||||
- DATABASE_AUTHENTICATION_DATABASE=strapi
|
||||
ports:
|
||||
- '1337:1337'
|
||||
volumes:
|
||||
- ./cms:/srv/app
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
container_name: mongo
|
||||
image: mongo
|
||||
environment:
|
||||
- MONGO_INITDB_DATABASE=strapi
|
||||
ports:
|
||||
- '27017'
|
||||
restart: always
|
||||
@ -1,11 +0,0 @@
|
||||
const articles = [
|
||||
{ id: 1, title: 'Article 1' },
|
||||
{ id: 2, title: 'Article 2' },
|
||||
{ id: 3, title: 'Article 3' },
|
||||
{ id: 4, title: 'Article 4' },
|
||||
{ id: 5, title: 'Article 5' }
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
articles
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import { makeExecutableSchema } from 'graphql-tools'
|
||||
import { typeDefs } from './typeDefs'
|
||||
import { resolvers } from './resolvers'
|
||||
|
||||
export const schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers
|
||||
})
|
||||
@ -1,11 +0,0 @@
|
||||
const { articles } = require('../data')
|
||||
|
||||
class Article {
|
||||
static all () {
|
||||
return articles
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Article
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
const { Article } = require('./Article')
|
||||
|
||||
module.exports = {
|
||||
Article
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
const { Article } = require('../models')
|
||||
|
||||
const articlesResolvers = {
|
||||
Query: {
|
||||
articles: () => Article.all()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
articlesResolvers
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
const { articlesResolvers } = require('./articlesResolvers')
|
||||
|
||||
const resolvers = [articlesResolvers]
|
||||
|
||||
module.exports = {
|
||||
resolvers
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
const { query } = require('./query')
|
||||
const { articleType } = require('./types')
|
||||
|
||||
const typeDefs = [query, articleType]
|
||||
|
||||
module.exports = {
|
||||
typeDefs
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
const { gql } = require('apollo-boost')
|
||||
|
||||
const query = gql`
|
||||
type Query {
|
||||
articles: [Article]
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = {
|
||||
query
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
const { gql } = require('apollo-boost')
|
||||
|
||||
const articleType = gql`
|
||||
type Article {
|
||||
id: ID
|
||||
title: String
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = {
|
||||
articleType
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
const { articleType } = require('./articleType')
|
||||
|
||||
module.exports = {
|
||||
articleType
|
||||
}
|
||||
198
lib/apollo.js
198
lib/apollo.js
@ -1,22 +1,95 @@
|
||||
import React from 'react'
|
||||
import App from 'next/app'
|
||||
import Head from 'next/head'
|
||||
import { ApolloProvider } from '@apollo/react-hooks'
|
||||
import { ApolloClient } from 'apollo-client'
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory'
|
||||
import createApolloClient from '../apolloClient'
|
||||
|
||||
// On the client, we store the Apollo Client in the following variable.
|
||||
// This prevents the client from reinitializing between page transitions.
|
||||
let globalApolloClient = null
|
||||
|
||||
/**
|
||||
* Creates and provides the apolloContext
|
||||
* to a next.js PageTree. Use it by wrapping
|
||||
* your PageComponent via HOC pattern.
|
||||
* @param {Function|Class} PageComponent
|
||||
* @param {Object} [config]
|
||||
* @param {Boolean} [config.ssr=true]
|
||||
* Installs the Apollo Client on NextPageContext
|
||||
* or NextAppContext. Useful if you want to use apolloClient
|
||||
* inside getStaticProps, getStaticPaths or getServerSideProps
|
||||
* @param {NextPageContext | NextAppContext} ctx
|
||||
*/
|
||||
export function withApollo (PageComponent, { ssr = true } = {}) {
|
||||
export const initOnContext = ctx => {
|
||||
const inAppContext = Boolean(ctx.ctx)
|
||||
|
||||
// We consider installing `withApollo({ ssr: true })` on global App level
|
||||
// as antipattern since it disables project wide Automatic Static Optimization.
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (inAppContext) {
|
||||
console.warn(
|
||||
'Warning: You have opted-out of Automatic Static Optimization due to `withApollo` in `pages/_app`.\n' +
|
||||
'Read more: https://err.sh/next.js/opt-out-auto-static-optimization\n'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize ApolloClient if not already done
|
||||
const apolloClient =
|
||||
ctx.apolloClient ||
|
||||
initApolloClient(ctx.apolloState || {}, inAppContext ? ctx.ctx : ctx)
|
||||
|
||||
// We send the Apollo Client as a prop to the component to avoid calling initApollo() twice in the server.
|
||||
// Otherwise, the component would have to call initApollo() again but this
|
||||
// time without the context. Once that happens, the following code will make sure we send
|
||||
// the prop as `null` to the browser.
|
||||
apolloClient.toJSON = () => null
|
||||
|
||||
// Add apolloClient to NextPageContext & NextAppContext.
|
||||
// This allows us to consume the apolloClient inside our
|
||||
// custom `getInitialProps({ apolloClient })`.
|
||||
ctx.apolloClient = apolloClient
|
||||
if (inAppContext) {
|
||||
ctx.ctx.apolloClient = apolloClient
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
/**
|
||||
* Always creates a new apollo client on the server
|
||||
* Creates or reuses apollo client in the browser.
|
||||
* @param {NormalizedCacheObject} initialState
|
||||
* @param {NextPageContext} ctx
|
||||
*/
|
||||
const initApolloClient = (initialState, ctx) => {
|
||||
// Make sure to create a new client for every server-side request so that data
|
||||
// isn't shared between connections (which would be bad)
|
||||
if (typeof window === 'undefined') {
|
||||
return createApolloClient(initialState, ctx)
|
||||
}
|
||||
|
||||
// Reuse client on the client-side
|
||||
if (!globalApolloClient) {
|
||||
globalApolloClient = createApolloClient(initialState, ctx)
|
||||
}
|
||||
|
||||
return globalApolloClient
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a withApollo HOC
|
||||
* that provides the apolloContext
|
||||
* to a next.js Page or AppTree.
|
||||
* @param {Object} withApolloOptions
|
||||
* @param {Boolean} [withApolloOptions.ssr=false]
|
||||
* @returns {(PageComponent: ReactNode) => ReactNode}
|
||||
*/
|
||||
export const withApollo = ({ ssr = false } = {}) => PageComponent => {
|
||||
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
|
||||
const client = apolloClient || initApolloClient(apolloState)
|
||||
let client
|
||||
if (apolloClient) {
|
||||
// Happens on: getDataFromTree & next.js ssr
|
||||
client = apolloClient
|
||||
} else {
|
||||
// Happens on: next.js csr
|
||||
client = initApolloClient(apolloState, undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<PageComponent {...pageProps} />
|
||||
@ -28,49 +101,53 @@ export function withApollo (PageComponent, { ssr = true } = {}) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const displayName =
|
||||
PageComponent.displayName || PageComponent.name || 'Component'
|
||||
|
||||
if (displayName === 'App') {
|
||||
console.warn('This withApollo HOC only works with PageComponents.')
|
||||
}
|
||||
|
||||
WithApollo.displayName = `withApollo(${displayName})`
|
||||
}
|
||||
|
||||
if (ssr || PageComponent.getInitialProps) {
|
||||
WithApollo.getInitialProps = async ctx => {
|
||||
const { AppTree } = ctx
|
||||
|
||||
// Initialize ApolloClient, add it to the ctx object so
|
||||
// we can use it in `PageComponent.getInitialProp`.
|
||||
const apolloClient = (ctx.apolloClient = initApolloClient())
|
||||
const inAppContext = Boolean(ctx.ctx)
|
||||
const { apolloClient } = initOnContext(ctx)
|
||||
|
||||
// Run wrapped getInitialProps methods
|
||||
let pageProps = {}
|
||||
if (PageComponent.getInitialProps) {
|
||||
pageProps = await PageComponent.getInitialProps(ctx)
|
||||
} else if (inAppContext) {
|
||||
pageProps = await App.getInitialProps(ctx)
|
||||
}
|
||||
|
||||
// Only on the server:
|
||||
if (typeof window === 'undefined') {
|
||||
const { AppTree } = ctx
|
||||
// When redirecting, the response is finished.
|
||||
// No point in continuing to render
|
||||
if (ctx.res && ctx.res.finished) {
|
||||
return pageProps
|
||||
}
|
||||
|
||||
// Only if ssr is enabled
|
||||
if (ssr) {
|
||||
// Only if dataFromTree is enabled
|
||||
if (ssr && AppTree) {
|
||||
try {
|
||||
// Run all GraphQL queries
|
||||
// Import `@apollo/react-ssr` dynamically.
|
||||
// We don't want to have this in our client bundle.
|
||||
const { getDataFromTree } = await import('@apollo/react-ssr')
|
||||
await getDataFromTree(
|
||||
<AppTree
|
||||
pageProps={{
|
||||
...pageProps,
|
||||
apolloClient
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
// Since AppComponents and PageComponents have different context types
|
||||
// we need to modify their props a little.
|
||||
let props
|
||||
if (inAppContext) {
|
||||
props = { ...pageProps, apolloClient }
|
||||
} else {
|
||||
props = { pageProps: { ...pageProps, apolloClient } }
|
||||
}
|
||||
|
||||
// Take the Next.js AppTree, determine which queries are needed to render,
|
||||
// and fetch them. This method can be pretty slow since it renders
|
||||
// your entire AppTree once for every query. Check out apollo fragments
|
||||
// if you want to reduce the number of rerenders.
|
||||
// https://www.apollographql.com/docs/react/data/fragments/
|
||||
await getDataFromTree(<AppTree {...props} />)
|
||||
} catch (error) {
|
||||
// Prevent Apollo Client GraphQL errors from crashing SSR.
|
||||
// Handle them in components via the data.error prop:
|
||||
@ -84,65 +161,16 @@ export function withApollo (PageComponent, { ssr = true } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract query data from the Apollo store
|
||||
const apolloState = apolloClient.cache.extract()
|
||||
|
||||
return {
|
||||
...pageProps,
|
||||
apolloState
|
||||
// Extract query data from the Apollo store
|
||||
apolloState: apolloClient.cache.extract(),
|
||||
// Provide the client for ssr. As soon as this payload
|
||||
// gets JSON.stringified it will remove itself.
|
||||
apolloClient: ctx.apolloClient
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return WithApollo
|
||||
}
|
||||
|
||||
/**
|
||||
* Always creates a new apollo client on the server
|
||||
* Creates or reuses apollo client in the browser.
|
||||
* @param {Object} initialState
|
||||
*/
|
||||
function initApolloClient (initialState) {
|
||||
// Make sure to create a new client for every server-side request so that data
|
||||
// isn't shared between connections (which would be bad)
|
||||
if (typeof window === 'undefined') {
|
||||
return createApolloClient(initialState)
|
||||
}
|
||||
|
||||
// Reuse client on the client-side
|
||||
if (!globalApolloClient) {
|
||||
globalApolloClient = createApolloClient(initialState)
|
||||
}
|
||||
|
||||
return globalApolloClient
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and configures the ApolloClient
|
||||
* @param {Object} [initialState={}]
|
||||
*/
|
||||
function createApolloClient (initialState = {}) {
|
||||
const ssrMode = typeof window === 'undefined'
|
||||
const cache = new InMemoryCache().restore(initialState)
|
||||
|
||||
// Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
|
||||
return new ApolloClient({
|
||||
ssrMode,
|
||||
link: createIsomorphLink(),
|
||||
cache
|
||||
})
|
||||
}
|
||||
|
||||
function createIsomorphLink () {
|
||||
if (typeof window === 'undefined') {
|
||||
const { SchemaLink } = require('apollo-link-schema')
|
||||
const { schema } = require('../graphql')
|
||||
return new SchemaLink({ schema })
|
||||
} else {
|
||||
const { HttpLink } = require('apollo-link-http')
|
||||
return new HttpLink({
|
||||
uri: '/api/graphql',
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
# Main
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
@ -15,6 +16,46 @@ server {
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
||||
# CMS
|
||||
server {
|
||||
listen 80;
|
||||
server_name dashboard.localhost;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_pass http://strapi:1337;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
# GraphQL
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.localhost;
|
||||
|
||||
location /graphql {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_pass http://strapi:1337/graphql;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
||||
972
package-lock.json
generated
972
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,8 +14,10 @@
|
||||
"apollo-boost": "^0.4.7",
|
||||
"apollo-link-schema": "^1.2.4",
|
||||
"apollo-server-micro": "^2.10.1",
|
||||
"graphql-import": "^1.0.0-beta.2",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"graphql-tools": "^4.0.7",
|
||||
"isomorphic-unfetch": "^3.0.0",
|
||||
"next": "9.2.2",
|
||||
"node-sass": "^4.13.1",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import { ApolloServer } from 'apollo-server-micro'
|
||||
import { schema } from '../../graphql'
|
||||
|
||||
const apolloServer = new ApolloServer({ schema })
|
||||
|
||||
export const config = { api: { bodyParser: false } }
|
||||
export default apolloServer.createHandler({ path: '/api/graphql' })
|
||||
@ -31,4 +31,4 @@ const Articles = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default withApollo(Articles)
|
||||
export default withApollo({ ssr: true })(Articles)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user