Simple Relay implementation

Introduction

We recently began to use React and fell in love with it. It's not everywhere in the site yet, but we have already built or refactored some major UIs with it.

Components make it pretty easy to build big apps while retaining simplicity. But feeding lots of components with data is hard: who needs what?

How do you keep the props of your components in sync when they can be used in many other components in your code? Here is the story on how we solved this. But first things first, let's introduce you to our API, since everything is related to it.

Our beloved API

We have a pretty powerful API, and using it is pretty straightforward. It lets us query fields on an object, and also sub-fields (fields of a sub-object) in one request.

For example you can request the following fields in one API call:

  • the video's title (video object)
  • the video's preview (video object)
  • the screen name of the video's owner (user object)
  • the avatar of the video's owner (user object)

To do that, we just need to know the object's prefix name this request (owner) and prefix the fields we need with it:

https://api.dailymotion.com/videos?fields=title,views_total,owner.screenname&owner.avatar_120_url&limit=1

{
  "title": "My video",
  "views_total": 15467245,
  "thumbnail_240_url": "http://s1.dmcdn.net/PERVL/x240-kOF.jpg"
  "owner.screenname": "Youpinadi", //sub-object user
  "owner.avatar_120_url": "http://s1.dmcdn.net/AVM/120x120-bnf.png" //sub-object user
}

We can also grab info for any user and her video star title pretty easily:

https://api.dailymotion.com/user/Youpinadi?fields=id,screenname,videostar.title

{
   "id": "x1ho",
   "screenname": "Nadir Kadem",
   "videostar.title":"Johnny Express" //sub-object video
}

Back to React

Here is a simple app example: it displays a list of videos from an API call. When our components represent some objects that are in our API, we decided to give them the same props name as the API fields. Naming things is difficult, so we let our API decide the name of our props ;)

This is pseudo-React (the real components have way more props and functionalities).

class VideoList extends Component {  
    defaultProps = {
       videos: []
    }
    render() {    
        return (
           <div>
              {
                 this.props.videos.map((video) =>                        <VideoItem {...video}/>)
              }
           </div>
         )
    }
}

class VideoTitle extends Component {  
  render() {
      return <div>{this.props.title}</div>
  }
}

class VideoPreview extends Component {  
  render() {
      return <img src={this.props.thumbnail_240_url}/>  
  }
}

class User extends Component {  
    render() {
        return <img src={this.props.avatar_120_url}/>  
    }
} 

class VideoItem extends Component {  
  render() {
      return (
         <div>
            <VideoTitle title={this.props.title}/>
            <VideoPreview thumbnail_240_url={this.props.thumbnail_240_url}/>
            <User avatar_120_url={this.props.avatar_120_url}/>
         </div>
      )
  }
 } 

class App extends Component {  
    state = {
        videos: []
    }
    componentDidMount() {
        //the field list can be very long
        api.get('https://api.dailymotion.com/videos?fields=description...') 
           .then((response) => this.setState({videos: response.list}))
    }
    render() {
        return <VideoList videos={this.state.videos}/>
    }
}

A simple approach

As you can see, the App makes a request, and feeds VideoList with a videos array. The videos are then passed as props to the <VideoItem/>. Then the pass the needed props to <VideoTitle/>, <VideoPreview/> and <User/>.

The data are shared by all components, so one API call is enough, we just need to gather the component's fields and make the call on the top level.

But imagine we want to add a views_total prop to the <VideoPreview/> component?

  • the video object needs to be updated
  • the views_total prop will needs to be provided to <VideoItem/>
  • the views_total prop will needs to be provided to <VideoPreview/>

    If <VideoItem/> is located in many files, we'll have to repeat these 3 steps each time.

Keep in mind this example is pretty simple. In a real world scenario, we're talking about a dozen props at least for the <VideoItem/> component.
It is pretty hard to maintain, not to say impossible.

It can be very tedious to manage lots of sub-components depending on the same API call. Each component should be able to pass the props to its children.

A better approach

Relay by Facebook might be the solution, but let's face it: it has some fairly big prerequisites (GraphQL).

While we will probably implement GraphQL in the future, what can be done with our current API?

Our solution is loosely based on Relay's concept: each component should expose what fields it requires from our API. These fields will then be passed as props to our component.
We called it apiFields (no fancy name sorry!). It's just a static array that we expose in our React classes and which defines the API field names or the components we need.\n\nTo get the fields needed in our API call, we traverse recursively from the top component and add the fields we find on our way. If the field is a string, we add it, if it's a component, we add its apiFields and so on.

For example, when we replace the components by their apiFields values, from these apiFields:

[
    'description', 
    VideoTitle,
    VideoPreview,
    ['owner', User]
]

We get this:

[
    'description', 
    'title',  // <- <VideoTitle/> apiFields
    'thumbnail_240_url', 'views_total', // <- <VideoPreview/> apiFields
    'owner.screenname' // <- <User/> apiFields prefixed
]

To create the API call, we have some helpers in our internal sdk:

api.get('/videos', {fields: VideoItem, limit: 1})  

Which is the same as a call to:
https://api.dailymotion.com/videos?fields=description,title,thumbnail_240_url,views_total,owner.screenname

This call is very useful, because I can change any apiFields in a sub-component of without having to worry. The API call will always be up to date.

We also have another method extractProps, who is able to get only the props needed by a component. It expects the parent object props, and the object itself (prefixed if necessary).

Show me some code!

Here's how it looks at the end:

class VideoTitle extends Component {  
  static apiFields = ['title']    
 ...
}
class VideoPreview extends Component {  
  static apiFields = [
    'thumbnail_240_url',
    'views_total'
  ]\t\t\n  ...
}

class User extends Component {  
  static apiFields = ['screenname']    
  ...
}

class VideoItem extends Component {  
  static apiFields = [
    'description', //simple field  VideoTitle, //VideoTitle's apiFields
  VideoPreview, //VideoPreview's apiFields
  ['owner', User], //User's apiFields, prefixed by \"owner.\"
   ]\n render() {
   return (
     <div>
       <VideoTitle {api.extractProps(this.props, VideoTitle)}/>
       <VideoPreview {api.extractProps(this.props, VideoPreview)}/>
       <User {api.extractProps(this.props, ['owner', User])}/>
    </div>
  )
}

class App extends Component {  
    state = {
        videos: []
    }
    componentDidMount() {
      api.get('/videos', {fields: VideoItem})
           .then((response) => this.setState({videos: response.list}))
    }
    render() {
        return <VideoList videos={this.state.videos}/>
    }
}

This approach has a few benefits:

  • All the developers can create the same API calls when they want a list of the same component. The fields are always in the same order and always up to date. This is good for HTTP caching.
  • If one developer adds an API field in a component, he doesn't check everywhere to ensure its props are delivered.
  • When a developer removes an apiField from a component, and if no other component needs it, it will be removed from the API call.
  • in a full React world we can imagine higher order components, just exposing apiFields and passing props to either DOM React components or native React components, thus leveraging HTTP caching even more.

The End

So with basically 2 simple methods (roughly 40 lines each), we solved our problem. We've been using this solution for a while and are pretty happy with it.

The best thing is it's not even React-related, it can work with any object (an angular directive for example) defining a simple array of apiFields. Yay!