Skip to content

Instantly share code, notes, and snippets.

@Youpinadi
Last active October 24, 2015 20:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Youpinadi/5c0c609c298bea5cf47a to your computer and use it in GitHub Desktop.
Save Youpinadi/5c0c609c298bea5cf47a to your computer and use it in GitHub Desktop.
simplistic relay

Simplistic Relay implementation

Introduction

We started to use react recently and fell in love with it. It's not all over the site yet, but we already built some quite big UIs with it. Components make it pretty easy to build big apps while still keeping simplicity. But feeding lots of components with data is hard: who needs what? How to keep your components props in sync when they can be used in lots of other components in you code? Here is the story on how we solved this.

Our beloved API

Here is a short introduction of our API: It's a pretty powerful API, but the usage is simple. It allows us to 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 screenname 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 get the infos of any user 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 example:

class App extends Component {
	render() {		
		let video = {
			title: 'my video',
			thumbnail_240_url: 'toto.jpg',
			'owner.screenname': 'Youpinadi'
		}
		return (
			<VideoItem 
				title={video.title} 
				thumbnail_240_url={video.thumbnail_240_url} 
				owner.screenname={screenname}>
				    <VideoPreview thumbnail_240_url={video.thumbnail_240_url}/>
			    <VideoTitle title={video.title}/>
				<User screenname={video.['owner.screenname']}/>
	</VideoItem>

		)
	} 
}

We of course try to do the minimum of API calls, so how do i get the best API call for all our components?

The naive approach (very bad)

Each component makes an API call to fetch its data. The pitfall is evident: if i have 10 <VideoItem/> in a webpage, i'll have: 10 calls for it + 30 calls for the children! Let's dismiss this one.

A better approach

The data are shared by all the components, so one API call is enough, we just need to regroup 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 really tiedous to manage lots of sub-components depending on the same API call. Each component should be able to pass the props to its children.

Best approach

Relay by Facebook might be the solution, but let's face it: it has some fairly big prerequisites (GraphQL). Another issue with GraphQL in its current state is that it doesn't address HTTP caching, and at dailymotion we rely heavily on it. While we may 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 props it requires. 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.

To get the fields needed in our API call, we just go 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.

When we replace the components by their apiFields values, we get this:

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

To create tha 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'
  ]		
  ...
}

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."		
 ]
  componentDidMount() {
	  // => https://api.dailymotion.com/videos?fields=thumbnail_240_url,title,views_total,owner.screenname&limit=1
	  api.get('/videos', {fields: VideoItem, limit: 1})
		  .then((response) => this.setState({video: response}))
  }
  render() (
	<VideoItem {...this.state.video}>
		<VideoTitle {api.extractProps(this.props, VideoTitle)}/>
		<VideoPreview {api.extractProps(this.props, VideoPreview)}/>
		<User {api.extractProps(this.props, ['owner', User])}/>
	</VideoItem>
	)
}

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 apiField in a component, he doesn't have to look everywhere if it gets its props 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 web 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment