Early in 2017, Wayfair made the decision to adopt React as our primary frontend framework of choice. Rather than cover the reasons why we chose React, more interestingly, we'd like to focus on some of the architecture challenges we faced during the conversion.
When the project officially commenced in June 2017, we had over 500 software engineers with very little React experience. On top of that, we boasted over 1,500 backbone models built on our Tungsten VDOM rendering framework, and dozens of new features to complete on tight timelines, with hundreds of A/B feature tests planned or in flight. Our JavaScript test coverage was also very low, hovering at around approximately 2%.
As one engineer put it: "This is like replacing every part of a plane while in flight, except there are actually two planes and you don't know what part goes to which plane."
Nevertheless, we were determined to make it work. At Wayfair, we never consider ourselves done, and this conversion was recognized as a critical step towards reusable visual components and a natural evolution of our frontend architecture.
Tungsten to React
We have a massive codebase with several highly interactive components, all essential to provide our customers with an optimal shopping experience. Given the size and complexity of our codebase, it would be impossible to convert entire pages to React. We needed a way for Tungsten components and React components to play nicely together so that parts of a page could be converted and deployed.
Our conversion plan saw us starting at leaf nodes of the component tree and recursively converting up the tree. When all children were successfully converted to React, we would then convert the parent. More on that later, but first, let’s dive into some details.
Tungsten components have “exposed functions” and “exposed events” to form a kind of API, or contract, between components. Any exposed events will bubble up from the component to the parent, which has access to call any exposed functions on the child directly. React communicates via props passed down from parent to child. What we needed was a way to embed a React component in a Tungsten component.
// HOC function passing model exposed events and functions
// through to the wrapped component
function getBoundReactComponent(WrappedComponent, tungstenModel) {
const exposedEvents = getExposedEvents(WrappedComponent, tungstenModel);
const exposedFunctions = getExposedFunctions(WrappedComponent, tungstenModel);function BoundReactComponent(props) {
return <WrappedComponent {...exposedFunctions} {...exposedEvents} {...props} />;
}return {
BoundReactComponent,
};
}
This Higher Order Component (HOC) function takes the exposed functions and exposed events off the Tungsten model instance and passes them as props to the root React component.
const el = opts.el
const doRender = function() {
const modelData = tungstenModelInstance.toJSON();
ReactDOM.render(<BoundReactComponent {...modelData} />, el);
};
doRender();
tungstenModelInstance.on('change render', doRender);
When constructing the Tungsten component, we simply render the React root at the Tungsten root and wire up to re-render any time the Tungsten wrapper changes or re-renders.
destroy() {
model.off('change render', doRender);
ReactDOM.unmountComponentAtNode(el);
},
Finally, Tungsten components have a destroy function, which we hook up to unmount the root React component and unbind the event handlers. This bit of code allowed us to cut off branches of the DOM tree which let them be purely React:
But what about allowing Tungsten components inside of React? Many engineers wanted this functionality, and we thought about it hard. It was certainly feasible: Simply return false from `componentShouldUpdate` and et voilà, you have a branch of DOM that you can let Tungsten take over (or JQuery, or any other library you want). This would work fine for a secluded branch of DOM, however Wayfair components are highly interactive. Communication between Tungsten -> React -> Tungsten would become very difficult to manage. Also, the fundamental architectural patterns between these two frameworks do not mix well.
If we wanted hundreds of engineers to start “thinking in React”, we needed a clean break.
For such a large React codebase, data management was bound to come up. After exhaustive investigation, deserving of a blog post of its own, we decided to go with Redux. This presented us with a new problem: How do we let separate React/Redux “apps” share a store and receive all the awesome benefits of a flux architecture?
// global provider action type
const ADD_ROOT_KEY = '@@global/provider/add_root_key';// add data to the global store action creator
const globalAddData = payload => ({type: ADD_ROOT_KEY, payload});
const globalProviderReducer = (state = {}, {type, payload}) => {
switch (type) {
case ADD_ROOT_KEY:
return {...state, ...payload};
default:
return state;
}
};let globalStore = createStore(() => {}, enhancer);
let globalReducers = {};// composes new reducers with global reducers
const composeNewReducer = reducers => (state = {}, action) =>
combineReducers(reducers)(globalProviderReducer(state, action), action);class GlobalProvider extends Component {
componentWillMount() {
const state = globalStore.getState();this.register(
this.props.reducers,
resolveData(state, this.props.initialData),
state
);
}register(newReducer, newData, oldData) {
globalReducers = {
...globalReducers,
...newReducer
};
globalStore.replaceReducer(composeNewReducer(globalReducers));
globalStore.dispatch(globalAddData(newData));
}render() {
return <Provider store={globalStore}>{this.props.children}</Provider>;
}
}
This is a simplified version of our global provider which works similarly to Redux `Provider` but instead of passing a new store, it hooks you up to the global store. This allowed us to reuse Redux selectors across separate “apps” as we converted up the tree.
Our inhouse React component library also played a major role in the speed of conversion. In just six months, 100% of our core mobile path and nearly 90% of all mobile pages were officially converted to React. Reflecting on this massive project, let's explore a few clear success factors and lessons that emerged.
Success factors
- React! It was great to learn React and since it made code so easy to write, several teams were able to not only convert but also take this as an opportunity to clean up some of their code debt.
- Global buy-in! Product managers and the overall business had complete understanding of the need for the refactor and allowed us the time to work, even though it would have no visible effect on the website. Having all teams, including the business, on the same page for this effort made all of the required pieces fall into place.
- Trail blazers! The JavaScript Platforms team here at Wayfair, plus a couple of early adopters, cleared quite a lot of roadblocks. When any issues came up, someone was always there to help.
Lessons and struggles
- Blockers across teams were definitely evident. Coordinating such a massive and interdependent project is bound to have some unexpected timing issues, especially taking into account the size of our Engineering department. Several teams were blocked from progress by features that depended on another team’s necessary pattern library components that needed to be converted.
- Interaction between Tungsten and React worked, but was rough. The quicker the system can be entirely in one architecture, the easier it is to make any needed updates to the system.
Conclusion
Overall, this project was a huge success for Wayfair. Adopting React quickly opened doors in our hiring pipeline, allowing us to recruit great engineers. It also brought cohesiveness and expertise to our codebase by shortening the time to test and deploy new features going forward. We’re excited to have been able to pull off such a feat, and the insights listed above are incredibly valuable.
What about you? Do you enjoy using things like React, Redux, and GraphQL to solve complex problems? Check out our open roles in engineering today!