Next-Apollo HOC 🚧
next-apollo-hoc is a simple and flexible way to set up apollo on your next.js app. It supports server-rendering and authentication.
- Installation
- Basic usage
- Externalize configuration
- Authentication
- Guards
- Example
- Tips and tricks
- Roadmap
1 - Installation
npm install --save next-apollo-hoc
2 - Basic usage
next-apollo-hoc provides a withData HOC (High-Order Component) that you can easily configure, just by giving your graphql endpoint :
import withData from 'next-apollo-hoc' const MyComponent = props // your page component <div>Component that will load data from a graphql endpoint</div> 'https://myendpoint.com'MyComponent
That's it, you are now able to fetch data from your graphql endpoint in any child component !
You can also set an HttpLink configuration for the Apollo Client (see official documentation) :
endpoint: 'https://myendpoint.com' // can also be set directly in link as uri option link: // can also be an HttpLink object credentials: 'include' MyComponent
The withData HOC integrates apollo by wrapping your Component inside an ApolloProvider Component. The generated ApolloClient is keeping data from the server then this module has a full-universal support.
To work properly, withData uses the getInitialProps method provided by next.js. This method is only callable from a page then you have to setup this HOC on a page.
Error handling
Since the graphql-data of the children components is fetched from the withData component, if errors appears in any query, they will be catched by the HOC and not re-dispatched to the child component.
So, in the server-side rendering of a component, this.props.data.error is always empty.
Because the withData HOC actually catches these errors, you can call this.props.errors in the page component. This property contains an array of all errors that occurred in children queries.
However, your errors will still appear in the client-side rendering, so you can process them.
const MyChildComponentWithData = props if propsdataerror // always empty in the server-side rendering, but could contains errors whil client-processing return <div>There are some errors</div> else // If errors occcurs, the server will even render 'Hello' but the browser will instantly replace with 'There are some errors' after the page loading return <div>Hello !</div>
import withData from 'next-apollo-hoc' const MyComponent = props // your page component if propserrors // contains all errors of children components queries return <div>There are some errors</div> else return <div> <MyChildComponentWithData /> </div> ... MyComponent
Default configuration
endpoint: null // graphql endpoint link: // HttpLink configuration, see ApolloClient API documentation credentials: 'same-origin'
3 - Externalize configuration (recommended)
next-apollo-hoc has a config class to create a global configuration :
// lib/next-apollo-hoc.js import config from 'next-apollo-hoc' config
Then, you just have to import HOCs from your file (to load the configuration) and then you will not need to set an inline configuration anymore :
- import { withData } from 'next-apollo-hoc'+ import { withData } from '../lib/next-apollo-hoc' const MyComponent = (props) => { <div>Component that will load data from a graphql endpoint</div>} - export default withData({- endpoint: 'https://myendpoint.com',- link: {- credentials: 'include'- }- })(MyComponent)+ export default withData(MyComponent)
Even if you set and load a global configuration like above, you are still able to override it inside your HOC call
import withData from '../lib/next-apollo-hoc' ... link: credentials: 'same-origin' // override the configuration set in lib/next-apollo-hoc.js useGETForQueries: true // add option to the configuration MyComponent
4 - Authentication
next-apollo-hoc provide tools to manage authentication (with token authorization) in your app. The HOC withAuth (that you can configure) will inject these tools inside your component props, so you will be able to use them where you want in your code.
To configure withAuth, the config component has an addAuth method. Just like for the withData HOC, you can set a global configuration and override some options inside the HOC call, or set the whole configuration directly inside the HOC (see how to externalize configuration)
Default configuration
defaultToken: null // the token used in header authorization when no user is logged tokenType: 'Bearer' // the authorization token type login: ... // login configuration, see below logout: ... // logout configuration, see below
Example
config
tokenType: 'Basic' login: mutation: gql`{ ... } }})(MyComponent)
4.1 - Login
thisprops
The login function will call a graphql mutation to get a token back. This token will be stored in a cookie and automatically set as the authorization header of each apollo client request.
So, the minimal configuration is :
variables: username: '...' password: '...' mutation: loginMutation // your graphql mutation (gql`{ ... }) dataloginauthToken // the function to get the token in the mutation result data
Usually, the variables parameter is not fixed until the login form submission.
So you can set/override the configuration inside the login function call :
thisprops
Login is an async function, and you can wait for its return to execute code (by example redirection).
However, to make global the entire execution of your login process, you can define a next function in your configuration that will be called after the cookie is set :
... Router
Finally, the login function will reset the apollo store and try to update with the updateStore option.
updateStore function result will be used in the writeQuery method call of the Apollo Client (see official documentation) :
... // updateStore give the data returned by the login mutation query: currentUser data: viewer: dataloginuser
Default configuration
update: // The function called after the cookie is set await apolloClient if updateStore await apolloClient
4.2 - Logout
thisprops
The logout function works pretty much the same as the login function. Instead of calling a mutation, it will directly delete the previously set cookie. Then, all the apollo client requests authorization will not contains the token anymore.
Just like login, you can configure an updateStore, update and next function.
query: currentUser data: viewer: null Router
Default configuration
update: // The function called after the cookie is removed await apolloClient if updateStore await apolloClient
5 - Guards
Once the user is logged and we have an authorization token, we are able to verify the user can access the data before rendering the component.
next-apollo-hoc provides a withGuard HOC. you can define your guards config in the global configuration or directly in the HOC call.
To configure guards in the global configuration, the config component has two methods: addGuard and addGuards.
The minimal configuration for a guard is :
query: currentUser // the graphql query to fetch !data || !dataviewer // the verification to do on the returned data
A 'guard' prop will be injected in your component. The value will be the result of the guard function.
Then, you will be able to render the component depending on the guard result
const MyComponentForLoggedUsers = props if !propsguard return <div>Please log in</div> else return <div>Hello !</div> query: currentUser data && dataviewerMyComponentForLoggedUsers
In the global configuration, you can give a name at a guard with the name option. Then, you will be able to call a guard by its name
config
'logged'MyComponentForLoggedUsers
Override configuration
You can also override a guard configuration by its name directly in the withGuard call :
name: 'logged' data && dataviewer && dataviewerrole = 'ADMIN'MyComponentForLoggedUsers
Combine guards
You can combine multiple guards, like below :
'logged' 'loggedAdmin'MyComponentForLoggedAdminUsers
6 - Example
Example app to come
7 - tips and tricks
7.1 - Use decorators
Instead of wrapping the export of your Component inside HOCs, you can use ES6 decorators. To do such a thing, you have to use the transform-decorators-legacy babel plugin :
npm install --save-dev babel-plugin-transform-decorators-legacy
create or edit a .babelrc file at the root of your project
{ "presets": "next/babel", "plugins": [+ "transform-decorators-legacy" ]}
You can now use ES6 decorators in your project :
import withData from '../lib/next-apollo-hoc' @withData@Component <div>Component that will load data from a graphql endpoint</div>
7.2 - Import your graphql queries/mutations from files
Instead of declaring your graphql queries and mutations directly in your component file with the graphql-tag, you can load them from .gql files :
npm install --save-dev babel-plugin-inline-import-graphql-ast
create or edit a .babelrc file at the root of your project
{ "presets": "next/babel", "plugins": [+ "babel-plugin-inline-import-graphql-ast" ]}
You can now load your queries and mutations from files :
import graphql from 'react-apollo'import myQuery from '../graphql/queries/my_query.gql' @withData@Component <div>Component that will load data from a graphql endpoint</div>
More tips to come
7.3 - Use the starter kit
The next-apollo-starter-kit provides a configuration with all the best practices for starting an universal next.js app based on apollo.
The kit includes the next-apollo-hoc library and all the tips and tricks listed above.
You can download it on : https://github.com/pierrecabriere/next-apollo-starter-kit
8 - Roadmap
- add ability to create middlewares from the config class
set a default token in config for authorize all requests (even unauthenticated)- any idea ?