Migrating Pinterest profiles to React

Imad Elyafi | Pinterest engineer, Core Experience

Since 2012, we’ve scaled our web framework Denzel (named after the greatest actor of all time) on top of Backbone. But nowadays, React is a golden standard. It has a large developer community and enables excellent engineering velocity and performance. Here we’ll look at techniques we tried and challenges we faced while migrating to React, starting with Pinner profiles.

Preparing infrastructure (server-side Nunjucks)

We frequently ship new features and run hundreds of experiments every day, so we couldn’t freeze product development in order to rebuild our whole website in React. While it’s relatively easy to build a new web app in React, migrating a service that’s constantly changing and used by millions of people is a much more complicated challenge. It’s like changing the engines of an airplane while mid-flight.

Before, we used the Jinja templating engine for server-side rendering in Python, and the JavaScript equivalent called Nunjucks for client-side browser rendering. These templating engines are very similar and allow us to have universal rendering (the same templates on both client and server). Since React can’t be rendered in Python, as a very first step, we enabled Nunjucks rendering on a stand-alone NodeJS server. Now, we have pure isomorphic rendering, with JavaScript on the server and on the client.

Denzel-React bridge

In order to empower engineers to incrementally convert core parts of our UI to React, we enabled React rendering inside Denzel. In most cases React.render() can be used, so we added React-specific bindings to Nunjucks’ templating language with a new keyword, component, to represent the “bridge” between Denzel and React. In combination with our A/B testing framework, we can easily measure React components against legacy Denzel components and control for certain metrics such as Pinner wait time, time-to-first-byte and error rate.

Here’s an example of rendering MyReactComponent.js in Nunjucks templates:

{% if in_react %}
{{ component('MyReactComponent', {pinId: '123'}) }}
{% else %}
{{ module('MyDenzelComponent', pinId='123') }}
{% endif %}

Higher order components for adapters

The last step is to supply data to the newly created React components. We created a Higher Order Component (HOC) called withResource to easily fetch data from our API while remaining composable with other HOCs. A simplified version of withResource provides a data prop to the wrapped component:

import ResourceFactory from 'ResourceFactory';

export default withResource = ({ name, options }) => (Subject) => {
return class extends Component {

state = {
data: null
};

componentDidMount() {
const resourceOptions = getOptions(options, this.props);
const resource = ResourceFactory.create(name, resourceOptions);

resource.callGet().then((resourceResponse) => {
this.setState({ data: resourceResponse.data });
});
}

render() {
return (
<Subject
data={this.state.data}
{...this.props}
/>
);
}
};
};

Example

import withResource from 'withResource';

class MyComponent extends Component {
render() {
return (
<div>{'Username:' + this.props.data.name}</div>
);
}
};

export default withResource({
name: 'MyResource',
options: props => {
return { pin_id: props.pinId };
},
})(MyComponent);

Results

In converting Pinners’ profiles to React, we’ve seen consistent performance and engagement improvements. Performance and engagement metrics each have increased 20 percent. While we converted profiles, web product engineers continued making changes to old Denzel components and simultaneously created new React components.

Acknowledgements: The core contributors to the project were Imad Elyafi, Braden Anderson, Chris Lloyd, Kevin Grandon, Jessica Chan along with the rest of the WebCore Experience team. A number of engineers across the company also provided helpful feedback.