提交 e09eb8d4 编写于 作者: H Henrik Wenz 提交者: Joe Haddad

Update with-apollo-and-redux example (#9270)

close #8830
上级 1e940745
......@@ -39,25 +39,6 @@ now
## The idea behind the example
This example serves as a conduit if you were using Apollo 1.X with Redux, and are migrating to Apollo 2.x, however, you have chosen not to manage your entire application state within Apollo (`apollo-link-state`).
This example serves as a conduit if you were using Apollo 1.X with Redux, and are migrating to Apollo 3.x, however, you have chosen not to manage your entire application state within Apollo (`apollo-link-state`).
In 2.0.0, Apollo serves out-of-the-box support for redux in favor of Apollo's state management. This example aims to be an amalgamation of the [`with-apollo`](https://github.com/zeit/next.js/tree/master/examples/with-apollo) and [`with-redux`](https://github.com/zeit/next.js/tree/master/examples/with-redux) examples.
Note that you can access the redux store like you normally would using `react-redux`'s `connect`. Here's a quick example:
```js
const mapStateToProps = state => ({
location: state.form.location,
})
export default withRedux(
connect(
mapStateToProps,
null
)(Index)
)
```
### Note:
In these _with-apollo_ examples, the `withData()` HOC must wrap a top-level component from within the `pages` directory. Wrapping a child component with the HOC will result in a `Warning: Failed prop type: The prop 'serverState' is marked as required in 'WithData(Apollo(Component))', but its value is 'undefined'` error. Down-tree child components will have access to Apollo, and can be wrapped with any other sort of `graphql()`, `compose()`, etc HOC's.
In 3.0.0, Apollo serves out-of-the-box support for redux in favor of Apollo's state management. This example aims to be an amalgamation of the [`with-apollo`](https://github.com/zeit/next.js/tree/master/examples/with-apollo) and [`with-redux`](https://github.com/zeit/next.js/tree/master/examples/with-redux) examples.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { addCount } from '../lib/store'
class AddCount extends Component {
add = () => {
this.props.addCount()
}
render () {
const { count } = this.props
return (
<div>
<h1>
AddCount: <span>{count}</span>
</h1>
<button onClick={this.add}>Add To Count</button>
<style jsx>{`
h1 {
font-size: 20px;
}
div {
padding: 0 0 20px 0;
}
`}</style>
</div>
)
}
}
const mapStateToProps = ({ count }) => ({ count })
const mapDispatchToProps = dispatch => {
return {
addCount: bindActionCreators(addCount, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(AddCount)
export default ({ lastUpdate, light }) => {
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'
const useClock = () => {
return useSelector(
state => ({
lastUpdate: state.lastUpdate,
light: state.light
}),
shallowEqual
)
}
const formatTime = time => {
// cut off except hh:mm:ss
return new Date(time).toJSON().slice(11, 19)
}
const Clock = () => {
const { lastUpdate, light } = useClock()
return (
<div className={light ? 'light' : ''}>
{format(new Date(lastUpdate))}
{formatTime(lastUpdate)}
<style jsx>{`
div {
padding: 15px;
......@@ -18,7 +37,4 @@ export default ({ lastUpdate, light }) => {
)
}
const format = t =>
`${pad(t.getUTCHours())}:${pad(t.getUTCMinutes())}:${pad(t.getUTCSeconds())}`
const pad = n => (n < 10 ? `0${n}` : n)
export default Clock
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
const useCounter = () => {
const count = useSelector(state => state.count)
const dispatch = useDispatch()
const increment = () =>
dispatch({
type: 'INCREMENT'
})
const decrement = () =>
dispatch({
type: 'DECREMENT'
})
const reset = () =>
dispatch({
type: 'RESET'
})
return { count, increment, decrement, reset }
}
const Counter = () => {
const { count, increment, decrement, reset } = useCounter()
return (
<div>
<h1>
Count: <span>{count}</span>
</h1>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
)
}
export default Counter
export default ({ message }) => (
import React from 'react'
import PropTypes from 'prop-types'
const ErrorMessage = ({ message }) => (
<aside>
{message}
<style jsx>{`
......@@ -11,3 +14,9 @@ export default ({ message }) => (
`}</style>
</aside>
)
ErrorMessage.propTypes = {
message: PropTypes.string.isRequired
}
export default ErrorMessage
export default ({ children }) => (
import React from 'react'
import Nav from './Nav'
import PropTypes from 'prop-types'
const Layout = ({ children }) => (
<main>
<Nav />
{children}
<style jsx global>{`
* {
......@@ -27,8 +32,9 @@ export default ({ children }) => (
background-color: #22bad9;
border: 0;
color: white;
display: flex;
display: inline-flex;
padding: 5px 7px;
margin-right: 1px;
}
button:active {
background-color: #1b9db7;
......@@ -37,6 +43,19 @@ export default ({ children }) => (
button:focus {
outline: none;
}
hr {
height: 1px;
border: none;
background: #ececec;
margin: 20px 0;
}
`}</style>
</main>
)
Layout.propTypes = {
children: PropTypes.node.isRequired
}
export default Layout
import Link from 'next/link'
import { withRouter } from 'next/router'
const Header = ({ router: { pathname } }) => (
const Nav = ({ router: { pathname } }) => (
<header>
<Link href='/'>
<a className={pathname === '/' ? 'is-active' : ''}>Home</a>
......@@ -28,4 +28,4 @@ const Header = ({ router: { pathname } }) => (
</header>
)
export default withRouter(Header)
export default withRouter(Nav)
import { connect } from 'react-redux'
import Clock from './Clock'
import AddCount from './AddCount'
export default connect(state => state)(({ title, lastUpdate, light }) => {
return (
<div>
<h1>Redux: {title}</h1>
<Clock lastUpdate={lastUpdate} light={light} />
<AddCount />
<style jsx>{`
h1 {
font-size: 20px;
}
`}</style>
</div>
)
})
import { graphql } from 'react-apollo'
import { useQuery } from '@apollo/react-hooks'
import { NetworkStatus } from 'apollo-client'
import gql from 'graphql-tag'
import ErrorMessage from './ErrorMessage'
import PostUpvoter from './PostUpvoter'
const POSTS_PER_PAGE = 10
function PostList ({
data: { loading, error, allPosts, _allPostsMeta },
loadMorePosts
}) {
if (error) return <ErrorMessage message='Error loading posts.' />
if (allPosts && allPosts.length) {
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()}>
{' '}
{loading ? '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>
)
}
return <div>Loading</div>
}
export const allPosts = gql`
export const ALL_POSTS_QUERY = gql`
query allPosts($first: Int!, $skip: Int!) {
allPosts(orderBy: createdAt_DESC, first: $first, skip: $skip) {
id
......@@ -93,32 +20,102 @@ export const allPosts = gql`
`
export const allPostsQueryVars = {
skip: 0,
first: POSTS_PER_PAGE
first: 10
}
// The `graphql` wrapper executes a GraphQL query and makes the results
// available on the `data` prop of the wrapped component (PostList)
export default graphql(allPosts, {
options: {
variables: allPostsQueryVars
},
props: ({ data }) => ({
data,
loadMorePosts: () => {
return data.fetchMore({
variables: {
skip: data.allPosts.length
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return previousResult
}
return Object.assign({}, previousResult, {
// Append the new posts results to the old one
allPosts: [...previousResult.allPosts, ...fetchMoreResult.allPosts]
})
}
})
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
}
})
})(PostList)
)
const loadingMorePosts = networkStatus === NetworkStatus.fetchMore
const loadMorePosts = () => {
fetchMore({
variables: {
skip: allPosts.length
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return previousResult
}
return Object.assign({}, previousResult, {
// Append the new posts results to the old one
allPosts: [...previousResult.allPosts, ...fetchMoreResult.allPosts]
})
}
})
}
if (error) return <ErrorMessage message='Error loading posts.' />
if (loading && !loadingMorePosts) return <div>Loading</div>
const { allPosts, _allPostsMeta } = data
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>
)
}
import React from 'react'
import { graphql } from 'react-apollo'
import { useMutation } from '@apollo/react-hooks'
import gql from 'graphql-tag'
import PropTypes from 'prop-types'
const UPDATE_POST_MUTATION = gql`
mutation updatePost($id: ID!, $votes: Int) {
updatePost(id: $id, votes: $votes) {
__typename
id
votes
}
}
`
const PostUpvoter = ({ votes, id }) => {
const [updatePost] = useMutation(UPDATE_POST_MUTATION)
const upvotePost = () => {
updatePost({
variables: {
id,
votes: votes + 1
},
optimisticResponse: {
__typename: 'Mutation',
updatePost: {
__typename: 'Post',
id,
votes: votes + 1
}
}
})
}
function PostUpvoter ({ upvote, votes, id }) {
return (
<button onClick={() => upvote(id, votes + 1)}>
<button onClick={() => upvotePost()}>
{votes}
<style jsx>{`
button {
......@@ -30,29 +60,9 @@ function PostUpvoter ({ upvote, votes, id }) {
)
}
const upvotePost = gql`
mutation updatePost($id: ID!, $votes: Int) {
updatePost(id: $id, votes: $votes) {
id
__typename
votes
}
}
`
PostUpvoter.propTypes = {
id: PropTypes.string.isRequired,
votes: PropTypes.number.isRequired
}
export default graphql(upvotePost, {
props: ({ ownProps, mutate }) => ({
upvote: (id, votes) =>
mutate({
variables: { id, votes },
optimisticResponse: {
__typename: 'Mutation',
updatePost: {
__typename: 'Post',
id: ownProps.id,
votes: ownProps.votes + 1
}
}
})
})
})(PostUpvoter)
export default PostUpvoter
import { graphql } from 'react-apollo'
import { useMutation } from '@apollo/react-hooks'
import gql from 'graphql-tag'
import { allPosts, allPostsQueryVars } from './PostList'
import { ALL_POSTS_QUERY, allPostsQueryVars } from './PostList'
function Submit ({ createPost }) {
function handleSubmit (event) {
event.preventDefault()
const CREATE_POST_MUTATION = gql`
mutation createPost($title: String!, $url: String!) {
createPost(title: $title, url: $url) {
id
title
votes
url
createdAt
}
}
`
const form = event.target
const Submit = () => {
const [createPost, { loading }] = useMutation(CREATE_POST_MUTATION)
const handleSubmit = event => {
event.preventDefault()
const form = event.target
const formData = new window.FormData(form)
createPost(formData.get('title'), formData.get('url'))
const title = formData.get('title')
const url = formData.get('url')
form.reset()
createPost({
variables: { title, url },
update: (proxy, { data: { createPost } }) => {
const data = proxy.readQuery({
query: ALL_POSTS_QUERY,
variables: allPostsQueryVars
})
// Update the cache with the new post at the top of the
proxy.writeQuery({
query: ALL_POSTS_QUERY,
data: {
...data,
allPosts: [createPost, ...data.allPosts]
},
variables: allPostsQueryVars
})
}
})
}
return (
<form onSubmit={handleSubmit}>
<h1>Apollo: Submit</h1>
<h1>Submit</h1>
<input placeholder='title' name='title' type='text' required />
<input placeholder='url' name='url' type='url' required />
<button type='submit'>Submit</button>
<button type='submit' disabled={loading}>
Submit
</button>
<style jsx>{`
form {
border-bottom: 1px solid #ececec;
padding-bottom: 20px;
margin-bottom: 20px;
}
......@@ -38,37 +70,4 @@ function Submit ({ createPost }) {
)
}
const createPost = gql`
mutation createPost($title: String!, $url: String!) {
createPost(title: $title, url: $url) {
id
title
votes
url
createdAt
}
}
`
export default graphql(createPost, {
props: ({ mutate }) => ({
createPost: (title, url) =>
mutate({
variables: { title, url },
update: (proxy, { data: { createPost } }) => {
const data = proxy.readQuery({
query: allPosts,
variables: allPostsQueryVars
})
proxy.writeQuery({
query: allPosts,
data: {
...data,
allPosts: [createPost, ...data.allPosts]
},
variables: allPostsQueryVars
})
}
})
})
})(Submit)
export default Submit
import React from 'react'
import Head from 'next/head'
import { ApolloProvider } from '@apollo/react-hooks'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import fetch from 'isomorphic-unfetch'
let apolloClient = 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]
*/
export function withApollo (PageComponent, { ssr = true } = {}) {
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
const client = apolloClient || initApolloClient(apolloState)
return (
<ApolloProvider client={client}>
<PageComponent {...pageProps} />
</ApolloProvider>
)
}
// Set the correct displayName in development
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())
// Run wrapped getInitialProps methods
let pageProps = {}
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx)
}
// Only on the server:
if (typeof window === 'undefined') {
// 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) {
try {
// Run all GraphQL queries
const { getDataFromTree } = await import('@apollo/react-ssr')
await getDataFromTree(
<AppTree
pageProps={{
...pageProps,
apolloClient
}}
/>
)
} catch (error) {
// Prevent Apollo Client GraphQL errors from crashing SSR.
// Handle them in components via the data.error prop:
// https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
console.error('Error while running `getDataFromTree`', error)
}
// getDataFromTree does not call componentWillUnmount
// head side effect therefore need to be cleared manually
Head.rewind()
}
}
// Extract query data from the Apollo store
const apolloState = apolloClient.cache.extract()
return {
...pageProps,
apolloState
}
}
}
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 (!apolloClient) {
apolloClient = createApolloClient(initialState)
}
return apolloClient
}
/**
* Creates and configures the ApolloClient
* @param {Object} [initialState={}]
*/
function createApolloClient (initialState = {}) {
// Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
return new ApolloClient({
ssrMode: typeof window === 'undefined', // Disables forceFetch on the server (so queries are only run once)
link: new HttpLink({
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', // Server URL (must be absolute)
credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
fetch
}),
cache: new InMemoryCache().restore(initialState)
})
}
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import fetch from 'isomorphic-unfetch'
let apolloClient = null
// Polyfill fetch() on the server (used by apollo-client)
if (typeof window === 'undefined') {
global.fetch = fetch
}
function create (initialState) {
// Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
const isBrowser = typeof window !== 'undefined'
return new ApolloClient({
connectToDevTools: isBrowser,
ssrMode: !isBrowser, // Disables forceFetch on the server (so queries are only run once)
link: new HttpLink({
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', // Server URL (must be absolute)
credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
}),
cache: new InMemoryCache().restore(initialState || {})
})
}
export default function initApollo (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 create(initialState)
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create(initialState)
}
return apolloClient
}
import React from 'react'
import { Provider } from 'react-redux'
import { initializeStore } from '../store'
import App from 'next/app'
export const withRedux = (PageComponent, { ssr = true } = {}) => {
const WithRedux = ({ initialReduxState, ...props }) => {
const store = getOrInitializeStore(initialReduxState)
return (
<Provider store={store}>
<PageComponent {...props} />
</Provider>
)
}
// Make sure people don't use this HOC on _app.js level
if (process.env.NODE_ENV !== 'production') {
const isAppHoc =
PageComponent === App || PageComponent.prototype instanceof App
if (isAppHoc) {
throw new Error('The withRedux HOC only works with PageComponents')
}
}
// Set the correct displayName in development
if (process.env.NODE_ENV !== 'production') {
const displayName =
PageComponent.displayName || PageComponent.name || 'Component'
WithRedux.displayName = `withRedux(${displayName})`
}
if (ssr || PageComponent.getInitialProps) {
WithRedux.getInitialProps = async context => {
// Get or Create the store with `undefined` as initialState
// This allows you to set a custom default initialState
const reduxStore = getOrInitializeStore()
// Provide the store to getInitialProps of pages
context.reduxStore = reduxStore
// Run getInitialProps from HOCed PageComponent
const pageProps =
typeof PageComponent.getInitialProps === 'function'
? await PageComponent.getInitialProps(context)
: {}
// Pass props to PageComponent
return {
...pageProps,
initialReduxState: reduxStore.getState()
}
}
}
return WithRedux
}
let reduxStore
const getOrInitializeStore = initialState => {
// Always make a new store if server, otherwise state is shared between requests
if (typeof window === 'undefined') {
return initializeStore(initialState)
}
// Create store if unavailable on the client and set it on the window object
if (!reduxStore) {
reduxStore = initializeStore(initialState)
}
return reduxStore
}
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
const exampleInitialState = {
lastUpdate: 0,
light: false,
count: 0
}
export const actionTypes = {
ADD: 'ADD',
TICK: 'TICK'
}
// REDUCERS
export const reducer = (state = exampleInitialState, action) => {
switch (action.type) {
case actionTypes.TICK:
return Object.assign({}, state, {
lastUpdate: action.ts,
light: !!action.light
})
case actionTypes.ADD:
return Object.assign({}, state, {
count: state.count + 1
})
default:
return state
}
}
// ACTIONS
export const serverRenderClock = isServer => dispatch => {
return dispatch({ type: actionTypes.TICK, light: !isServer, ts: Date.now() })
}
export const startClock = () => dispatch => {
return setInterval(
() => dispatch({ type: actionTypes.TICK, light: true, ts: Date.now() }),
800
)
}
export const addCount = () => dispatch => {
return dispatch({ type: actionTypes.ADD })
}
export const initStore = (initialState = exampleInitialState) => {
return createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware))
)
}
import { useEffect, useRef } from 'react'
// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
const useInterval = (callback, delay) => {
const savedCallback = useRef()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
const handler = (...args) => savedCallback.current(...args)
if (delay !== null) {
const id = setInterval(handler, delay)
return () => clearInterval(id)
}
}, [delay])
}
export default useInterval
import React from 'react'
import PropTypes from 'prop-types'
import { ApolloProvider, getDataFromTree } from 'react-apollo'
import Head from 'next/head'
import initApollo from './initApollo'
// Gets the display name of a JSX component for dev tools
function getComponentDisplayName (Component) {
return Component.displayName || Component.name || 'Unknown'
}
export default ComposedComponent => {
return class WithData extends React.Component {
static displayName = `WithData(${getComponentDisplayName(
ComposedComponent
)})`
static propTypes = {
serverState: PropTypes.object.isRequired
}
static async getInitialProps (ctx) {
// Initial serverState with apollo (empty)
let serverState = {
apollo: {
data: {}
}
}
// Evaluate the composed component's getInitialProps()
let composedInitialProps = {}
if (ComposedComponent.getInitialProps) {
composedInitialProps = await ComposedComponent.getInitialProps(ctx)
}
// Run all GraphQL queries in the component tree
// and extract the resulting data
if (typeof window === 'undefined') {
const apollo = initApollo()
try {
// Run all GraphQL queries
await getDataFromTree(
<ApolloProvider client={apollo}>
<ComposedComponent {...composedInitialProps} />
</ApolloProvider>,
{
router: {
asPath: ctx.asPath,
pathname: ctx.pathname,
query: ctx.query
}
}
)
} catch (error) {
// Prevent Apollo Client GraphQL errors from crashing SSR.
// Handle them in components via the data.error prop:
// https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
}
// getDataFromTree does not call componentWillUnmount
// head side effect therefore need to be cleared manually
Head.rewind()
// Extract query data from the Apollo store
serverState = {
apollo: {
data: apollo.cache.extract()
}
}
}
return {
serverState,
...composedInitialProps
}
}
constructor (props) {
super(props)
this.apollo = initApollo(this.props.serverState.apollo.data)
}
render () {
return (
<ApolloProvider client={this.apollo}>
<ComposedComponent {...this.props} />
</ApolloProvider>
)
}
}
}
......@@ -7,23 +7,19 @@
"start": "next start"
},
"dependencies": {
"apollo-client": "^2.5.1",
"apollo-client-preset": "^1.0.4",
"graphql": "^14.1.1",
"graphql-anywhere": "^4.0.2",
"graphql-tag": "^2.5.0",
"@apollo/react-hooks": "3.1.3",
"@apollo/react-ssr": "3.1.3",
"apollo-cache-inmemory": "1.6.3",
"apollo-client": "2.6.4",
"apollo-link-http": "1.5.16",
"graphql": "14.5.8",
"graphql-tag": "2.10.1",
"isomorphic-unfetch": "^3.0.0",
"next": "latest",
"next-redux-wrapper": "^2.1.0",
"prop-types": "^15.6.0",
"react": "^16.7.0",
"react-apollo": "^2.0.1",
"react-dom": "^16.7.0",
"react-redux": "^6.0.1",
"redux": "^4.0.1",
"redux-devtools-extension": "^2.13.2",
"redux-thunk": "^2.2.0"
},
"author": "",
"license": "ISC"
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-redux": "^7.1.1",
"redux": "^4.0.1"
}
}
import App from 'next/app'
import React from 'react'
import { Provider } from 'react-redux'
import withRedux from 'next-redux-wrapper'
import { initStore } from '../lib/store'
class MyApp extends App {
static async getInitialProps ({ Component, router, ctx }) {
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
render () {
const { Component, pageProps, store } = this.props
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
}
}
export default withRedux(initStore)(MyApp)
import App from '../components/App'
import Header from '../components/Header'
import Layout from '../components/Layout'
import Submit from '../components/Submit'
import PostList from '../components/PostList'
import withApollo from '../lib/withApollo'
import { withApollo } from '../lib/apollo'
export default withApollo(() => (
<App>
<Header />
const ApolloPage = () => (
<Layout>
<Submit />
<PostList />
</App>
))
</Layout>
)
export default withApollo(ApolloPage)
import React from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { startClock, addCount, serverRenderClock } from '../lib/store'
import App from '../components/App'
import Header from '../components/Header'
import Page from '../components/Page'
import { useDispatch } from 'react-redux'
import { withRedux } from '../lib/redux'
import { compose } from 'redux'
import { withApollo } from '../lib/apollo'
import useInterval from '../lib/useInterval'
import Layout from '../components/Layout'
import Clock from '../components/Clock'
import Counter from '../components/Counter'
import Submit from '../components/Submit'
import PostList from '../components/PostList'
import withApollo from '../lib/withApollo'
class Index extends React.Component {
static getInitialProps ({ store, isServer }) {
store.dispatch(serverRenderClock(isServer))
store.dispatch(addCount())
return { isServer }
}
componentDidMount () {
this.timer = this.props.startClock()
}
componentWillUnmount () {
clearInterval(this.timer)
}
render () {
return (
<App>
<Header />
<Page title='Index' />
<Submit />
<PostList />
</App>
)
}
const IndexPage = () => {
// Tick the time every second
const dispatch = useDispatch()
useInterval(() => {
dispatch({
type: 'TICK',
light: true,
lastUpdate: Date.now()
})
}, 1000)
return (
<Layout>
{/* Redux */}
<Clock />
<Counter />
<hr />
{/* Apollo */}
<Submit />
<PostList />
</Layout>
)
}
const mapDispatchToProps = dispatch => {
return {
addCount: bindActionCreators(addCount, dispatch),
startClock: bindActionCreators(startClock, dispatch)
}
IndexPage.getInitialProps = ({ reduxStore }) => {
// Tick the time once, so we'll have a
// valid time before first render
const { dispatch } = reduxStore
dispatch({
type: 'TICK',
light: typeof window === 'object',
lastUpdate: Date.now()
})
return {}
}
export default withApollo(
connect(
null,
mapDispatchToProps
)(Index)
)
export default compose(
withApollo,
withRedux
)(IndexPage)
import React from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { startClock, addCount, serverRenderClock } from '../lib/store'
import App from '../components/App'
import Header from '../components/Header'
import Page from '../components/Page'
class Index extends React.Component {
static getInitialProps ({ store, isServer }) {
store.dispatch(serverRenderClock(isServer))
store.dispatch(addCount())
return { isServer }
}
componentDidMount () {
this.timer = this.props.startClock()
}
componentWillUnmount () {
clearInterval(this.timer)
}
render () {
return (
<App>
<Header />
<Page title='Redux' />
</App>
)
}
import { useDispatch } from 'react-redux'
import { withRedux } from '../lib/redux'
import useInterval from '../lib/useInterval'
import Layout from '../components/Layout'
import Clock from '../components/Clock'
import Counter from '../components/Counter'
const ReduxPage = () => {
// Tick the time every second
const dispatch = useDispatch()
useInterval(() => {
dispatch({
type: 'TICK',
light: true,
lastUpdate: Date.now()
})
}, 1000)
return (
<Layout>
<Clock />
<Counter />
</Layout>
)
}
const mapDispatchToProps = dispatch => {
return {
addCount: bindActionCreators(addCount, dispatch),
startClock: bindActionCreators(startClock, dispatch)
}
ReduxPage.getInitialProps = ({ reduxStore }) => {
// Tick the time once, so we'll have a
// valid time before first render
const { dispatch } = reduxStore
dispatch({
type: 'TICK',
light: typeof window === 'object',
lastUpdate: Date.now()
})
return {}
}
export default connect(
null,
mapDispatchToProps
)(Index)
export default withRedux(ReduxPage)
import { createStore } from 'redux'
const initialState = {
lastUpdate: 0,
light: false,
count: 0
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'TICK':
return {
...state,
lastUpdate: action.lastUpdate,
light: !!action.light
}
case 'INCREMENT':
return {
...state,
count: state.count + 1
}
case 'DECREMENT':
return {
...state,
count: state.count - 1
}
case 'RESET':
return {
...state,
count: initialState.count
}
default:
return state
}
}
export const initializeStore = (preloadedState = initialState) => {
return createStore(reducer, preloadedState)
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册