The first iteration of OfferZen’s frontend stack consisted of server-side rendered html, jquery and handlebar templates. Although this worked quite well for us in the early days, we started running into problems as the complexity of the product increased and the team grew.
We decided that we wanted to move away from server-side rendering and decouple, as far as possible, the frontend from the backend.
This article describes the approach we followed to migrate our frontend to React.
Choosing an incremental approach
The OfferZen platform consists of three products. First, there is OfferZen for developers - this is where developers build their profiles, receive interview requests, respond to messages from companies and claim their R5 000 bonuses once they get placed.
Then, there is OfferZen for companies. This is where companies build their profiles, view and filter a list of the developers currently looking for work on OfferZen, send interview requests, schedule interviews and extend job offers to their chosen candidates.
Finally, there is TARA (Talent Advisor Robot Assistant) - the internal CRM tool used by OfferZen’s Talent Advisors and Account Managers. Talent Advisors use TARA to keep track of all communications with developers, help them build amazing profiles and send them gifts when they accept job offers. Account Managers use the tool to assist companies with building their profiles, manage their hiring pipelines, onboard new users and send gifts to companies when they hire someone through the platform.
As the product team, our aim was to end up with three different single page apps (SPAs) - one for each of the three products mentioned above.
Initially, we considered taking one of these products at a time and completely rebuilding them in React. There were some major concerns with this approach, however. Firstly, it would break our current process of shipping one feature per week. Secondly, we were in the process of redesigning a lot of features and would either have to finish all the designs before moving to React, or implement something we knew we would have to rebuild soon. Finally, the risk of shipping such a large piece of work was just too big.
With these problems in mind, we decided on an incremental approach: We would implement all new and redesigned features in React, mixing in the new React components into the legacy codebase.
Once all of the features for one of the product apps were rewritten, we would combine them into an SPA.
Introducing React into an existing Rails app
When reading up on React and how to integrate it into our Rails app, we came across React on Rails. This gem provides an opinionated framework for exactly this type of integration.
Three things about the gem particularly stood out when we evaluated it. Firstly, the documentation is really thorough and there is a lot of example code to help one get started.
Secondly, the gem makes it trivial to render a React component in a standard Rails template. This would make the incremental approach we settled on very easy because we could just drop new features into existing layouts.
Once we integrated the gem into our codebase and got a little “Hello World!” going, we were ready to start implementing a new feature in React.
Building the first React feature
The first feature we wanted to build in React was the Interview Requests page for companies.
This is where companies manage all the interview requests they have sent to developers, respond to individual messages from developers and schedule online or in-person interviews. The old design didn’t scale well for companies with a large number of interview requests because all the information about every request had to be included in one page without offering any search or filtering functionality. This caused the page to be very heavy and, in some instances, the page could take up to 10 seconds to load.
The new design split the feature into two pages: a list of requests, and a detailed view of a single request along with the corresponding message thread and interview scheduling functionality.
We also implemented ATS (applicant tracking system) features to make it easier for companies to manage their hiring pipeline. Interview requests now have different states (pending, accepted, technical screening, hired, rejected etc.) depending on where the developer is within the company’s hiring process.
The new Interview Request list page also enables users to filter interview requests based on the sender. This is especially useful to companies with people from different departments using OfferZen to hire developers.
An app in React on Rails consists of the following basic components
- One or more React components
- One or more containers. These are wrappers around basic React components that map the global state in the Redux store to the component’s local properties. When the global state changes, the component’s properties are automatically updated which in turn triggers a re-render
- An entry-point app that connects a container to a Redux store
- A Redux store, reducer and actions
For the Interview Requests feature, we created three base components. Firstly, there is the
List component that displays a list of interview requests and allows the user to filter based on request-status and sender. Secondly, we created a
Request component that displays the request details and enables the company to schedule interviews with candidates. The
Request component embeds the
MessageThread component, which contains the message thread between the developer and the company. We will reuse the
MessageThread component once we build the Interview Request page for developers in React.
The entry-point app for Interview Requests has two main functions. It has to connect to the Redux store and render the correct component (
Request) based on the URL.
The two main Interview Request subpages will have different URLs:
/interview_requests/123. When the user's browser is pointing to the first, we need to render
List. For the second URL, we get the specific request id from the URL, and render the
Request component, populated with that request's details. To do this, we used React Router.
How we think about state
We identified three different kinds of state that have to be managed and decided to handle each of them differently. The first and most important is the actual data - a list of interview requests together with their details and associated messages. This data lives in the Redux store, in the user’s browser. Initially, we only retrieve the data that is needed to render the list itself. Once a user clicks on an interview request in the list, we retrieve the interview request’s details and associated messages.
Secondly, there is the state needed for filters applied to the list. The URL is the primary source of truth for the filters, where they are encoded as query parameters.
Finally, there is state relating to display (for example, a field stating whether a widget is expanded or collapsed) which is stored in the component’s internal state (
Rendering the React app in Rails
Replacing the old Interview Request page with the new React app was a straightforward process. All we had to do was provide the data for the Redux store and let React on Rails render the component in the current layout.
The two controller actions we had to change were
show action also includes the details and message thread data for the request that the URL is pointing to.
Example controller code:
# interview_request_controller.rb def index redux_store("InterviewRequestsStore", props: requests) render layout: "company/company_dashboard" end def show redux_store("InterviewRequestsStore", props: requests_with_request_details) render layout: "company/company_dashboard" end
In the view, we could just replace the old templating code with the following:
<%= redux_store_hydration_data %> <%= react_component("InterviewRequestsApp") %>
The first line renders the data for the Redux store as a JSON object. The second line renders the actual React component.
To allow navigation between
Request without a full page-reload, we changed the controller actions to also respond to ajax requests. This way, users can quickly navigate between the list and the request details by simply making ajax calls. This also allows us to periodically update the list and the message threads to make sure that the user always has the latest information.
Where we’re at and what’s next
Apart from the Interview Requests page, we have also converted the Candidate List page to a React app, with both of these currently running in production.
This has already led to a noticeable increase in our productivity. There is a fantastic ecosystem built around React, with a lot of existing packages that we could just use.
The React and Redux developer tools are really powerful and make the code really easy to debug. We especially like the Redux developer tools that allows one to “travel back in time” by letting you replay the state changes.
We’ve also recently added two new members to the team (one who will exclusively focus on frontend work) and they were able to jump straight in.
With the two main company-features (Interview Requests and the Candidate List) already ported to React, we only have the admin pages (account settings, profile builder, team members etc.) left to convert. Once these have been rebuilt in React, we can combine them into an “SPA” for companies.
This incremental approach worked really well for us because it allowed us to ship new features built in React from day one. This means that we could focus on continually improving the OfferZen experience for developers, companies and our internal teams, while keeping the long-term goal of completely switching to React in the back of our minds.