How to use Storyblok's GraphQL endpoint with React and Apollo

Contents

Introduction

In this tutorial, you will learn how to use Storyblok’s new GraphQL api with React and Apollo - the most famous GraphQL client.

If you are stuck anywhere in this tutorial, then please refer to the final code repository on GitHub.

Setup

Let’s start with setting up the project using the generator create-react-app.

npx create-react-app storyblok-graphql-tutorial
cd storyblok-graphql-tutorial
npm start

Next install the GraphQL client and React bindings.

npm install apollo-boost react-apollo graphql

Initialize the GraphQL client

Open the file src/index.js and initialize the Apollo client with Storyblok’s GraphQL endpoint https://gapi.storyblok.com/v1/api.

Grab the “Preview” token from you Storyblok space and set it as header for every request.

Storyblok Token

Following the additions made in src/index.js:

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';

const client = new ApolloClient({
  uri: 'https://gapi.storyblok.com/v1/api',
  request: operation => {
    operation.setContext({
      headers: {
        token: 'QBmPtomvbW9LsjuHFiUCtgtt',
        version: 'draft'
      }
    });
  }
});

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
);

Make your first GraphQL query

Storyblok’s GraphQL schema is generated from your content types.

For every content type Storyblok generates two fields.

  • One for receiving a single item: [Humanized Name]Item
  • And one for receiving a multiple items: [Humanized Name]Items

So if you have created a content type with the name page you will have the fields PageItem and PageItems in GraphQL.

To get the documented schema definition of your content type we created a GraphQL playground. Exchange the token (YOUR_TOKEN) with your “Preview” token and open the link: http://gapi-browser.storyblok.com/?token=YOUR_TOKEN.

In the following example we query the home content item and output the page name in src/App.js.

src/App.js
import React from 'react';
import './App.css';
import { Query } from 'react-apollo';
import { gql } from 'apollo-boost';

const query = gql`{
  PageItem(id: "home") {
    name
  }
}`

class App extends React.Component {
  constructor(props) {
    super(props)
  }

  render() {
    return (
      <Query query={query}>
        {result => {
          if (result.loading) return <p className="loading">loading...</p>;
          if (result.error) return <p className="loading">{result.error.message}</p>;
          return (
            <div className="app">
              {result.data.PageItem.name}
            </div>
          );
        }}
      </Query>
    );
  }
}

export default App;

How to render nested components

Storyblok has components as first class citizens on board. This means that you can easily create advanced layouts and nest components inside each other. In the next step we will create a few more React components to render the demo content that you get when you create a new Storyblok space.

First add a components folder in src and place the file index.js inside.

src/components/index.js
import React from 'react'
import Teaser from './Teaser'
import Feature from './Feature'
import Page from './Page'
import Grid from './Grid'

const Components = {
  'teaser': Teaser,
  'feature': Feature,
  'page': Page,
  'grid': Grid
}

export default (blok) => {
  if (typeof Components[blok.component] !== 'undefined') {
    return React.createElement(Components[blok.component], {key: blok._uid, content: blok})
  }
  return React.createElement(() => (
    <div>The component {blok.component} has not been created yet.</div>
  ), {key: blok._uid})
}

Now create the missing components:

Create src/components/Page.js

src/components/Page.js
import Components from './index'
import React  from 'react'

export default (props) => (
    <div>
      {props.content.body.map((blok) =>
        Components(blok)
      )}
    </div>
)

Create src/components/Grid.js

src/components/Grid.js
import Components from './index'
import React  from 'react'

export default (props) => (
    <div className="grid">
      {props.content.columns.map((blok) =>
        Components(blok)
      )}
    </div>
)

Create src/components/Feature.js

src/components/Feature.js
import React  from 'react'

export default (props) => (
    <div className="column feature">
      {props.content.name}
    </div>
)

Create src/components/Teaser.js

src/components/Teaser.js
import React  from 'react'

export default (props) => (
    <div className="teaser">
      {props.content.headline}
    </div>
)

Now you add the reference to src/components/index.js to the applications entry point src/index.js.

src/index.js
import React from 'react';
import './App.css';
import { Query } from 'react-apollo';
import { gql } from 'apollo-boost';
import Components from './components';

const query = gql`{
  PageItem(id: "home") {
    id
    slug
    content {
      _uid
      component
      body
    }
  }
}`

class App extends React.Component {
  constructor(props) {
    super(props)
  }

  render() {
    return (
      <Query query={query}>
        {result => {
          if (result.loading) return <p className="loading">loading...</p>;
          if (result.error) return <p className="loading">{result.error.message}</p>;
          return (
            <div className="app">
              {Components(result.data.PageItem.content)}
            </div>
          );
        }}
      </Query>
    );
  }
}

export default App;

At the end (with some CSS added) you should have the following result when opening your app in the browser:

Components Demo

How to add Storyblok’s Visual Editor

Adding Storyblok’s visual editing capability just requires a few steps. Your content editors will thank you going for that little extra effort.

First add the Javascript bridge to the file public/index.html and replace YOUR_TOKEN with your “Preview” token.

public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="//app.storyblok.com/f/storyblok-latest.js?t=YOUR_TOKEN"></script>
  </body>
</html>

Next install the Storyblok binding for React to make elements clickable by the user:

$ npm install storyblok-react

All your components in the folder src/components now needs to be wrapped by the helper component SbEditable.

Following an example how to do that in the file src/components/Teaser.js:

src/components/Teaser.js
import SbEditable from 'storyblok-react'
import React  from 'react'

export default (props) => (
  <SbEditable content={props.content}>
    <div className="teaser">
      {props.content.headline}
    </div>
  </SbEditable>
)

After you have wrapped all components with the SbEditable component you now need the event listener with window.storyblok.on to reload the app after content has been changed in the editor. To force a re-render and re-fetch we add a timestamp as query parameter for the GraphQL query component.

src/App.js
import React from 'react';
import './App.css';
import { Query } from 'react-apollo';
import { gql } from 'apollo-boost';
import Components from './components';

const query = gql`{
  PageItem(id: "home") {
    id
    slug
    content {
      _uid
      component
      body
    }
  }
}`

class App extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      queryVars: {renderTimestamp: 0},
      fetchPolicy: 'cache-first'
    }

    window.storyblok.on(['change', 'published'], () => {
      this.setState({
        queryVars: {renderTimestamp: Date.now()},
        fetchPolicy: 'network-only'
      })
      this.forceUpdate()
    })
  }

  render() {
    return (
      <Query query={query} variables={this.state.queryVars} fetchPolicy={this.state.fetchPolicy}>
        {result => {
          if (result.loading) return <p className="loading">loading...</p>;
          if (result.error) return <p className="loading">{result.error.message}</p>;
          return (
            <div className="app">
              {Components(result.data.PageItem.content)}
            </div>
          );
        }}
      </Query>
    );
  }
}

export default App;

The last step is to configure the preview url in Storyblok and check if it is working. Add the end you should have a clickable teaser element where the content updates if you click “Save”.

Visual Editor

ResourceLink
Github repository of this tutorialgithub.com/storyblok/storyblok-graphql-react-apollo
Helpful tutorial to get started with Apolloreactgo.com/graphql-react-apollo-client
Storyblok appapp.storyblok.com
Reactreactjs.org

About the author

Alexander Feiglstorfer

Alexander Feiglstorfer

Passionate developer and always in search of the most effective way to resolve a problem. After working 13 years for agencies and SaaS companies using almost every CMS out there he founded Storyblok to solve the problem of being forced to a technology, proprietary template languages and the plugin hell of monolithic systems.


More to read...