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.
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:
{
"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
}
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?
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.
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.
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:
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).
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.
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!