Skip to content

Apollo Client adapter for next.js with universal support and authentication tools

License

Notifications You must be signed in to change notification settings

pierrecabriere/next-apollo-hoc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

86 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Next-Apollo HOC ⚑️

NPM version Build Status

next-apollo-hoc is a simple and flexible way to set up apollo on your next.js app. It supports server-rendering and authentication.


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>
)

export default withData('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) :

export default withData({
  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.

export default const MyChildComponentWithData = (props) => {
  if (props.data.error) // 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 (props.errors) // contains all errors of children components queries
    return (<div>There are some errors</div>)
  else
    return (
      <div>
        <MyChildComponentWithData />
      </div>
    )
}

export default withData({ ... })(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.add({
  endpoint: 'https://myendpoint.com',
  link: {
    credentials: 'include'
  }
})

export * from 'next-apollo-hoc'

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'

...

export default withData({
  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.addAuth({
  tokenType: 'Basic',
  login: {
    mutation: gql`{ ... }
  }
})
export default withAuth({
  tokenType: 'Basic',
  login: {
    mutation: gql`{ ... }
  }
})(MyComponent)

4.1 - Login

this.props.login()

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`{ ... })
  authToken: data => data.login.authToken // 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 :

this.props.login({
  variables: {
    username: this.state.username,
    password: this.state.password
  }
})

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 :

{
  ...,
  next: data => Router.push('/')
}

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: data => ({ // updateStore give the data returned by the login mutation
    query: currentUser,
    data: { viewer: data.login.user }
  })
}

Default configuration

{
  update: async (apolloClient, data, updateStore) => { // The function called after the cookie is set
    await apolloClient.resetStore()
    if (updateStore)
      await apolloClient.writeQuery(updateStore(data))
  }
}

4.2 - Logout

this.props.logout()

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.

{
  updateStore: () => ({ query: currentUser, data: { viewer: null } }),
  next: () => Router.pushRoute('/')
}

Default configuration

{
  update: async (apolloClient, data, updateStore) => { // The function called after the cookie is removed
    await apolloClient.resetStore()
    if (updateStore)
      await apolloClient.writeQuery(updateStore(data))
  }
}

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
  guard: data => !data || !data.viewer, // 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 (!props.guard)
    return (<div>Please log in</div>)
  else
    return (<div>Hello !</div>)
}

export default withGuard({
  query: currentUser,
  guard: data => data && data.viewer
})(MyComponentForLoggedUsers)

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.addGuard({
  name: 'logged',
  query: currentUser,
  guard: data => data && data.viewer
})
export default withGuard('logged')(MyComponentForLoggedUsers)

Override configuration

You can also override a guard configuration by its name directly in the withGuard call :

export default withGuard({
  name: 'logged',
  guard: data => data && data.viewer && data.viewer.role = 'ADMIN'
})(MyComponentForLoggedUsers)

Combine guards

You can combine multiple guards, like below :

export default withGuard('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
@graphql(myQuery)
export default class extends React.Component {
  render() => (
    <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
@graphql(myQuery)
export default class extends React.Component {
  render() => (
    <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 ?

πŸš€