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

Contents
    Try Storyblok

    Storyblok is the first headless CMS that works for developers & marketers alike.

    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 storyblok-editable to make elements clickable by the user:

    $ npm install @storyblok/storyblok-editable
    

    All your components in the folder src/components now needs to include the helper sbEditable.

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

    src/components/Teaser.js
    import { sbEditable } from "@storyblok/storyblok-editable";
    import React from "react";
    
    export default (props) => (
      <div {...sbEditable(props.content)} className="teaser">
        {props.content.headline}
      </div>
    );
    

    After you included sbEditable on all components 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

    Resource Link
    Github repository of this tutorial github.com/storyblok/storyblok-graphql-react-apollo
    Helpful tutorial to get started with Apollo reactgo.com/graphql-react-apollo-client
    Storyblok app app.storyblok.com
    React reactjs.org