Migrating your front-end to React, step by step.
Around you, small companies and startups are using React. Taking advantage of React's composable architecture and modern tooling, they put out new features at a speed you could only dream of. You'd like to start using React as well, or perhaps you already introduced it for some small components. But you simply can't afford to rewrite your entire front-end! Your customers are using your front-end daily, and they expect maintenance and new features! Luckily, you don't have to rewrite, and you shouldn't, really! You can migrate to React one small piece at a time.
In this article, I'll demonstrate how you can wrap your existing code in a React component, untangle your application's states and their representation, and open up the potential for further refactoring, without breaking your current application.
Why would I want to do that?
Big rewrites are risky. When you rewrite your application, you'll have to make a difficult trade-off: either you put your new version in front of customers fast, but with far less features than the current application, or you wait until you approach feature-parity, delaying feedback on the final product and shipping an unproven application. Furthermore, the longer you wait, the longer you will maintain two applications.
In order to safely and confidently improve, it is essential to gather feedback as early as possible, by continuously delivering new versions to your customers. If you follow the steps outlined in this article, you will be able to safely introduce your first React component today. This will allow you to validate your approach and figure out fast if this is the right way to go. The obvious downside is that, by working in the existing application, it will take a bit more time before you'll be able to take full advantage of using React. But by repeatedly applying these steps, you'll be able to get to that point, with only a fraction of the risk.
Setting the stage
addEventListener or a library like jQuery), and will update the DOM when these events happen. Usually, the DOM will be manipulated by adding or removing CSS classes, or updating the style attribute.
Probably, your client code also makes calls to your backend service, and updates the current page according to the result. Whether you are updating some classes in the DOM, or rewriting the entire page based on an HTTP request, the migration strategy will be similar.
If you’re working in a legacy codebase, chances are that these DOM manipulations happen directly, or using an imperative DOM manipulation library like jQuery. Often, there is no separate representation of the application’s state, just direct manipulation of the DOM. This makes it difficult to keep track of the different states the application can be in, what transitions are possible at any moment, and how to correctly update the DOM to be consistent with that state.
In the worst case, the code that updates the DOM lives in multiple places, or unrelated code is sprawled together, making it difficult to untangle the different concerns.
Refactoring this 'spaghetti code' into React components will do a few things for you. First, it will introduce a notion of components, which will allow you to group related code together and compose smaller bits into a larger composition. Second, following the steps below will help you to separate the states of your application from their representation in the DOM. This will reduce the chances of getting into an inconsistent state and improve modifiability. Finally, you'll be able to take advantage of other React specific features, such as error boundaries, to improve the stability of your application even further.
So what do I do?
Well, for starters, you’ll have to decide if React is the way to go. I will not try to convince you here, there are many good resources already. However, if you do decide you want to move towards a React application, you can follow along the next steps.
Wrap it up
Our goal is to safely migrate towards React components, so initially we’ll try to change as little code as possible. Imagine that your code looks like the following:
Now, our second step is to introduce React. We’ll wrap this imperative code in a new React component. You can do this for a related piece of code in your codebase. The best place to start would probably be an entire file, or a big block of related functionality if your files are huge. Don’t worry, you’ll be able to extract small pieces out later and clean things up.
Whereas most introductions to React will start with the
render method of a component, we’ll put all of our imperative code in the
componentDidMount lifecycle method, which is called when the component has been mounted into the document. Because a
render method is required, we’ll make it return
null so that React will not modify the DOM.
Then, we immediately render this component using
ReactDOM.render(element, root), where we use an empty container for the root, which is detached from the document, and we end up with something like this:
So, what have we achieved so far? Well not a lot really, what we’ve done so far is roughly equivalent to placing the code in a function and invoking it immediately! But… we did get React in there, and everything is still working, so there’s that. Also, if you had any tests for this code (HINT: you really should!), then they should still pass after this step. If they don't, they are likely too coupled to the DOM, for example because you have too strictly mocked methods of the DOM tree.
One of my colleagues pointed out that this first step is similar to the Strangler Pattern, and he is absolutely right. The approach to working with legacy code always follows similar patterns.
Please state your business
As I’ve mentioned earlier, keeping track of all the states and transitions is usually the hard part in this legacy code, because state transitions and DOM updates are mixed together. Our next step is to separate the two, by introducing state into our component.
Accordions component from the example, there is one piece of state and one transition: the active panel in each accordion, and the activation and deactivation of a panel in an accordion. Note that the imperative code allows for multiple accordions on the page, and we want to maintain this same functionality during our migration.
Map object. Because the imperative code will start in the ‘state’ as described in the DOM (i.e. the panel with class
accordion-expanded is automatically active), we’ll also need this behavior in our component: we’ll read the initial state from the DOM in the
constructor of the component, by looking for a panel with the
accordion-expanded class in every accordion and placing that panel in the accordion's entry in the map. We’ll set the map into
this.state at the end of the constructor.
Then, as we’ve read the state from the DOM, we’ll need to start using it as our update mechanism. We’ll replace the click event handler that updated the DOM with one that simply calls the component’s
setState can take an updater function as the first argument, which is called by React with the current state, and should return a new (partial) state. The object returned from this function is merged with the current state. Anything in
state should be considered immutable: if we want React to pick up on the update, we should replace the current
Map with a new one instead of calling
set on it directly.
Finally, when the state of the component is updated, it will call another lifecycle method called
componentDidUpdate, and this is where the update logic from the click handler should now go. The
componentDidUpdate method receives the previous props and state as arguments, so you can determine what changed. However, it would be even better to depend only on the new state, as this is much less likely to break when adding new transitions and states.
If you’ve taken these steps, you’ll end up with something roughly like the code below:
Now we’ve actually achieved something: we’ve separated the state transition (activating a panel on click) from the representation of that state (active panel has
accordion-expanded class). For a more complex application, doing this repeatedly for multiple states would definitely start to paint a clearer picture of what the code is doing, and make it easier to change.
Size does matter
The component used as a demonstration is still relatively small, but taken the same steps in a bigger application will definitely result (at least initially) in a huge component doing all kinds of things. At this point, it’s probably a good idea to embrace the component model of React and start splitting this up into smaller components with different responsibilities. There are several directions you can take, and the best direction will depend entirely on the requirements of your application. You don’t have to get it right the first time, though, this is an on-going process as you explore and change your application.
We could, for example, extract a controlled
Accordion component and render one for each accordion found in the DOM, so that it can handle the updating of a single accordion:
The approach that we’ve taken has some other benefits too. For example, all errors inside the code in the component’s lifecycle methods, as well as in the
setState updater function, are automatically caught by (recent versions of) React and reported to a parent component. In the ‘old’ code, a small error would simply break your app or leave it in an inconsistent state. Using the React component, we’ll have a way to handle this error. In the example below, expanding the first two panels should work as normal. The third one will trigger a fake bug, causing an error. We’ll block further input and indicate an error has occurred:
This example will block all accordions, as that is the granularity at which the code is now. Clearly, creating more modular components will also afford you the opportunity to handle the error at a more granular level.
The road ahead
Now that we’ve migrated our code to be usable within a React application, the future is bright. Once larger amounts of code are ported to React, you can start composing trees of components and communicate with props and callbacks between them. You can add error boundaries as shown previously and share globally relevant data through React context. Finally, you can start rendering HTML from your
render method as well. If your
componentDidUpdate doesn’t need previous props or state, translating it to the
render function should be quite straightforward. And don’t worry, components that render
null won’t get in your way.
If you have any questions, or a case you can't figure out, please leave a comment below and we'll get back to you!