In this article, we will discuss the real-world production use of React 18 and how it impacts projects using the "good” and old Create React App (CRA). We will talk about the features coming with the upgrade, and some needed fixes.
React 18 was released on March 2022. This major version of the popular JavaScript library brings a range of exciting new features and improvements to the React ecosystem. React 18 was eagerly anticipated by the development community and has since received positive reviews for its advancements in data rendering and smooth transitions between pages and components.
One of the main changes in React 18 is automatic batching, which improves memory consumption and performance by batching together state updates to re-render only once. However, this feature requires, sometimes, a rethinking of state management in an application.
Much has been written about how batching works, for example, this one on the topic.
When I upgraded React in my projects which are using CRA, I didn't encounter any significant issues, except for one case. I had a dashboard with multiple cards, each containing a button and inputs for performing actions. However, the dashboard had also a countdown timer that caused issues with the state and rendering of the action buttons. It took me a couple of days to understand the problem, which ultimately led me to downgrade the React version.
Upon investigation, I discovered that the problem was caused by the Formik library, which had not been updated or maintained for quite some time. Formik was causing conflicts with the state changes triggered by the timer. The solution was to abandon Formik and replace it with a library like React Hook Form.
Let’s talk now about the changes in the rendering of React components from version 17 to 18, especially about the pros and cons of each approach, and provide examples of production scenarios I have encountered.
React 17 works with client-side rendering as the default and most widely used method. This means that the components are rendered on the user's browser using the virtual DOM to update the UI with changes in data. When a user visits a website or web application built with React 17, the server sends the initial HTML along with the JavaScript code for the React components. The browser then executes the code and renders the components, allowing the user to interact with the UI.
One of the main advantages of client-side rendering is the ability to quickly update and provide interactive experiences. The components can easily be re-rendered on the client without requiring a full page refresh. It also allows for the use of client-side state and APIs, which are not available on the server. However, it can also lead to slower initial load times as the browser must first download and execute the JavaScript before rendering the components, which may be an issue for users on slow connections.
React 18 introduces the option of server-side rendering (SSR), where the initial rendering of the components is done on the server instead of the client. When a user visits a website or web application built with React 18 and server-side rendering, the server will generate the HTML for the initial render of the components and send it to the client. The client then "hydrates" the pre-rendered HTML with the JavaScript code, matching the references.
This results in improved initial load times as the server can send the fully rendered HTML to the client, allowing it to be displayed immediately. It also allows the use of new APIs such as Suspense and useTransition. However, it can also be more complex to implement and may cause issues with components that rely on APIs, as the initial rendering on the server does not have access to these.
Using React 18's SSR with Suspense can pose challenges when it comes to data fetching. Since Suspense relies on client-side states and APIs, it is not supported on the server, resulting in incomplete server-rendered HTML for components that utilize Suspense for data fetching. To address this issue, an alternative data fetching approach must be used. The React team has acknowledged that SSR with data fetching was not designed for use in common Create React App projects and recommends using a framework like NextJS or Remix that implements this feature out of the box. However, in a work-related project, it may not be feasible to overhaul the entire application structure.
In one of my recent projects, I encountered a challenge when upgrading from React 17 to React 18. The issue arose when a page in the application was fetching data from the backend while also displaying a loading spinner. After the upgrade, the default HTML page was displayed to the user before the data was finished being fetched, causing the entire page to refresh and display the correct information only afterward.
To solve this problem, I had to come up with a solution that worked within the constraints of server-side rendering (SSR) and the routing of the entire application. My solution was to create a condition for the rendering of the page component. I set the component to only render when the data was present and displayed a loading spinner if the data was not yet available. This way, during the initial HTML rendering, the condition is not met and the state is false, causing the loading spinner to be displayed. Once the data is fetched, the component can then render with all the necessary information and the correct hydrated HTML.
Suspense is a powerful tool that can improve the user experience in certain situations.
Firstly, it can be particularly beneficial for users with slow internet connections or devices. Suspense allows for a fallback component, such as a loader page or spinner, to be displayed while the JavaScript is being loaded, rather than leaving the user with a blank page.
Secondly, Suspense can also be used to optimize data-heavy parts of the application. By customizing Suspense, it can be applied only to specific components, allowing for almost the entire page to be displayed to the user, except for the loading parts which will be deferred.
It is expected that future versions of React will address the challenges currently faced with utilizing Suspense for server-side rendering (SSR) and data fetching. One solution that is currently being discussed is the introduction of a new hook called "use". This hook will allow developers to access asynchronous data sources with Suspense through a stable API. You can read about it at this link. While this solution is not yet available, the React team is still working on implementing the cache feature for this hook. In the meantime, those using Create React App (CRA) may need to find a workaround and hacks to handle data fetching in their applications.
In order to create a seamless and enjoyable experience for users, it is important to pay attention to the visual effects when transitioning between pages or components in an application. React 18 has introduced a new API called useTransition, which allows developers to easily implement transitions. The useTransition hook returns a tuple containing two objects: isPending, which indicates whether the transition is completed, and startTransition, a method that can be called to initiate the transition.
This hook can be particularly useful when used in combination with Suspense, as it can provide a smooth experience for users while new components or pages are being loaded. For example, if a method is used to handle a click to change a tab on a page, the transition can be started there and the isPending boolean can be used as a loading state for the component.
I’m providing two code sandboxes for you to compare the methods of loading pages and states in React. The first sandbox uses the traditional methods in React 17, while the second uses the new Suspense and useTransition hook in React 18.
The first code sandbox demonstrates manual loading with React 17: react-17-loading-content-switch-tabs - CodeSandbox
The second code sandbox shows how Suspense and useTransition can be used in React 18: react-18-loading-content-suspense-useTransition - CodeSandbox
Both sandboxes simulate a slow API request using setTimeout.
The React 17 sandbox demonstrates manual loading and the use of a loading state, which can lead to delays for users when switching between tabs. In contrast, the React 18 sandbox uses the useTransition hook to allow for a more seamless user experience while new content is being loaded. The loading process is indicated by a progressively coloring top bar, which is less disruptive to the user.
It's worth noting that while Suspense can be useful for loading components, it doesn't work well with data fetching in a vanilla React application. Therefore, the React 18 sandbox also includes a normal loader for data fetching as well as a global loader that uses setTimeout to trigger Suspense while the component is loading, displaying the colored top bar.
Let’s take a look at the new approaches for data rendering in React 18, with the introduction of Server-Side Rendering and Suspense. Two new approaches, "fetch-as-render" and "render-then-fetch," are now available, which differ from the traditional "fetch-then-render" approach that was used in React 17.
Both of these new methods allow for rendering to occur simultaneously with data fetching, rather than in a sequential manner as was previously the case. This allows for a better user experience, as data is fetched in the background while the user is interacting with the page.
To illustrate the difference between these approaches, consider the following image:
In the traditional "fetch-then-render" approach, data fetching occurs sequentially, one API call after another. With the "fetch-as-render" and "render-then-fetch" approaches, multiple API calls can be made simultaneously, while the HTML can be already rendered, resulting in a more efficient overall process.
Moreover, it’s also possible to customize the loading of each component using different Suspense(s) in a SuspenseList:
Migrating to React 18 can be challenging, especially when it comes to implementing new features correctly. It's crucial to understand how React 18 updates will impact your application and to plan and test thoroughly to avoid problems. Here are some common issues and their potential fixes:
React is becoming increasingly complex with each version, leading to tricky bugs. For instance:
Sometimes those breaking changes will force you to change completely the structure of the project because of one thing not working.
The increasing layers of complexity in React are interesting, but tiring at the same time. Bugs happen and it's difficult to understand why. Especially for somebody starting to learn it just now.
In conclusion, React 18 introduces several new features that can enhance the user experience in your applications. However, it is important to be aware of the potential issues that may arise with an incorrect implementation of React 18, such as automatic batching and multiple mounting, and to consider potential solutions for these issues.
Blog posts you may be interested in
New blog posts you may be interested in
We help startups, IT companies and corporations with digital products.