/simple-relay-starter

A very simple starter for React Relay using Browserify

Primary LanguageJavaScriptMIT LicenseMIT

simple-relay-starter

A simple example of how to get started with Relay using some slightly different approaches to relay-starter-kit that may make it easier to navigate for first-time users, especially Node.js users.

Unlike relay-starter-kit, this project uses Browserify instead of Webpack, does not use a proxy for the GraphQL endpoint and does not require ES6 features for any server-side code, so it can be run directly with node – resulting in less boilerplate and making it easier to understand the code.

Example

$ npm install
$ npm run build
$ npm start

Then navigate to http://localhost:3000 and observe the network request to /graphql that Relay makes to retrieve the data.

For development, you can use:

$ npm run dev

Which will build the schema and then watch for any file changes, rebuilding the schema and/or restarting the server as necessary.

Here are the files involved:

App.js:

var React = require('react')
var Relay = require('react-relay')

// A simple top-level component that illustrates how to render Relay-fetched
// data using props. In this case Relay will populate a `user` property that
// has a collection of `widgets` based on the queries and fragments we give it
// further below.
class App extends React.Component {
  render() {
    return (
      <div>
        <h2>User: {this.props.user.name}</h2>
        <h2>Widgets:</h2>
        <ul>
          {/* In schema/schema.js we define a Connection between users and widgets */}
          {/* Connections use `edges` and `node` to hold paging info and child items */}
          {this.props.user.widgets.edges.map(edge =>
            <li>{edge.node.name} (Global ID: {edge.node.id})</li>
          )}
        </ul>
      </div>
    )
  }
}

// The component we need to export is a Relay wrapper around our App component
// from above. It declares the GraphQL fragments where we list the properties
// we want to be fetched – eg, user.name, user.widgets.edges, etc
exports.Container = Relay.createContainer(App, {
  fragments: {
    // The property name here reflects what is added to `this.props` above.
    // This template string will be parsed by babel-relay-plugin when we browserify.
    user: () => Relay.QL`
      fragment on User {
        name,
        widgets(first: 10) {
          edges {
            node {
              id,
              name,
            },
          },
        },
      }
    `,
  },
})

// The Relay root container needs to know what queries will occur at the top
// level – these configurations are currently called Routes in Relay, but this
// name is misleading and under review so we don't use it here.
exports.queries = {
  name: 'AppQueries', // can be anything, just used as an identifier
  params: {},
  queries: {
    // We can use this shorthand so long as the component we pair this with has
    // a fragment named "user", as we do above.
    user: () => Relay.QL`query { user }`,
  },
}

browser.js:

var React = require('react')
var ReactDOM = require('react-dom')
var Relay = require('react-relay')
var App = require('./App')

// This file is the entry point on the browser – browserify will compile it, as
// well as App.js and any other client-side dependencies and create
// public/bundle.js which will be requested by public/index.html

ReactDOM.render(
  // At the top of a Relay tree is the root container, which we pass our
  // wrapped App component to, as well as the query configuration ("route"). If
  // we need to render a different component, say as a result of a navigation
  // event, then we would update it here.
  // We also illustrate the use of the onReadyStateChange handler in case
  // there's a network error, etc
  <Relay.RootContainer Component={App.Container} route={App.queries}
    onReadyStateChange={({error}) => { if (error) console.error(error) }} />,

  document.getElementById('content')
)

public/index.html:

<!-- include React and Relay scripts (we don't bundle them) -->
<script src=//cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.min.js></script>
<script src=//cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react-dom.min.js></script>
<script src=/relay/relay.min.js></script>
<div id=content />
<!-- now request our browserified bundle which will run the React.render -->
<script src=/bundle.js></script>

server.js:

var express = require('express')
var graphqlHttp = require('express-graphql')
var schema = require('./schema/schema')

// The server is just a simple Express app
var app = express()

// We respond to all GraphQL requests from `/graphql` using the
// `express-graphql` middleware, which we pass our schema to.
app.use('/graphql', graphqlHttp({schema: schema}))

// The rest of the routes are just for serving static files
app.use('/relay', express.static('./node_modules/react-relay/dist'))
app.use('/', express.static('./public'))

app.listen(3000, function() { console.log('Listening on 3000...') })

schema/database.js:

// We use these types to hold data and resolve from GraphQL types in our schema

function User(id, name) {
  this.id = id.toString()
  this.name = name
}

function Widget(id, userId, name) {
  this.id = id.toString()
  this.userId = userId.toString()
  this.name = name
}

// In a realistic system, the get functions below would return objects from a
// datastore like a DB or a REST API instead of an in-memory store like this.
// You can also return promises for async fetching

var users = [new User(1, 'Anonymous')]

var widgets = [
  new Widget(1, 1, 'What\'s-it'),
  new Widget(2, 1, 'Who\'s-it'),
  new Widget(3, 1, 'How\'s-it'),
]

module.exports = {
  User: User,
  Widget: Widget,
  getUser: function(id) { return users.filter(function(u) { return u.id == id })[0] },
  getAnonymousUser: function() { return users[0] },
  getWidget: function(id) { return widgets.filter(function(w) { return w.id == id })[0] },
  getWidgetsByUser: function(userId) { return widgets.filter(function(w) { return w.userId == userId }) },
}

schema/schema.js:

var GraphQL = require('graphql')
var GraphQLRelay = require('graphql-relay')
var db = require('./database')

// This module exports a GraphQL Schema, which is a declaration of all the
// types, queries and mutations we'll use in our system.

// Relay adds some specific types that it needs to function, including Node, Edge, Connection

// Firstly we need to create the Node interface in our system. This has nothing
// to do with Node.js! In Relay, Node refers to an entity – that is, an object
// with an ID.

// To create this interface, we need to pass in a resolving function as the
// first arg to nodeDefinitions that can fetch an entity given a global Relay
// ID. The second arg can be used to resolve an entity into a GraphQL type –
// but it's actually optional, so we'll leave it out and use isTypeOf on the
// GraphQL types further below.

var nodeDefinitions = GraphQLRelay.nodeDefinitions(function(globalId) {
  var idInfo = GraphQLRelay.fromGlobalId(globalId)
  if (idInfo.type == 'User') {
    return db.getUser(idInfo.id)
  } else if (idInfo.type == 'Widget') {
    return db.getWidget(idInfo.id)
  }
  return null
})

// We can now use the Node interface in the GraphQL types of our schema

var widgetType = new GraphQL.GraphQLObjectType({
  name: 'Widget',
  description: 'A shiny widget',

  // Relay will use this function to determine if an object in your system is
  // of a particular GraphQL type
  isTypeOf: function(obj) { return obj instanceof db.Widget },

  // We can either declare our fields as an object of name-to-definition
  // mappings or a closure that returns said object (see userType below)
  fields: {
    id: GraphQLRelay.globalIdField('Widget'),
    name: {
      type: GraphQL.GraphQLString,
      description: 'The name of the widget',
    },
  },
  // This declares this GraphQL type as a Node
  interfaces: [nodeDefinitions.nodeInterface],
})

var userType = new GraphQL.GraphQLObjectType({
  name: 'User',
  description: 'A person who uses our app',
  isTypeOf: function(obj) { return obj instanceof db.User },

  // We use a closure here because we need to refer to widgetType from above
  fields: function() {
    return {
      id: GraphQLRelay.globalIdField('User'),
      name: {
        type: GraphQL.GraphQLString,
        description: 'The name of the user',
      },
      // Here we set up a paged one-to-many relationship ("Connection")
      widgets: {
        description: 'A user\'s collection of widgets',

        // Relay gives us helper functions to define the Connection and its args
        type: GraphQLRelay.connectionDefinitions({name: 'Widget', nodeType: widgetType}).connectionType,
        args: GraphQLRelay.connectionArgs,

        // You can define a resolving function for any field.
        // It can also return a promise if you need async data fetching
        resolve: function(user, args) {
          // This wraps a Connection object around your data array
          // Use connectionFromPromisedArray if you return a promise instead
          return GraphQLRelay.connectionFromArray(db.getWidgetsByUser(user.id), args)
        },
      },
    }
  },
  interfaces: [nodeDefinitions.nodeInterface],
})

// Now we can bundle our types up and export a schema
// GraphQL expects a set of top-level queries and optional mutations (we have
// none in this simple example so we leave the mutation field out)
module.exports = new GraphQL.GraphQLSchema({
  query: new GraphQL.GraphQLObjectType({
    name: 'Query',
    fields: {
      // Relay needs this to query Nodes using global IDs
      node: nodeDefinitions.nodeField,
      // Our own root query field(s) go here
      user: {
        type: userType,
        resolve: function() { return db.getAnonymousUser() },
      },
    },
  }),
})