Back to website.

Level up your React codebase quality

Iulian-Constantin Marcu
Iulian-Constantin Marcu
Cover Image for Level up your React codebase quality

In the ever-evolving world of web development, React has firmly established itself as a dominant force. Its component-based architecture, rich ecosystem, and performance optimisations make it a preferred choice for building dynamic and interactive web applications.

However, as projects grow in size and complexity, maintaining high code quality in React codebases becomes increasingly challenging.

In this blog post, we will explore strategies and best practices for improving code quality in React applications. Whether you're a seasoned React developer or just getting started, you'll find valuable insights to elevate your codebase and streamline your development process.

Pure components

When developing web applications, developers need to think about the user interface as just another representation of the data flowing through the system. Composition is the main principle of React, and at the end of the day, a JSX representation of the data is no different than JSON or YAML.

Comparison of diferent data structures and UI

Separating the UI into visual components and business logic is beneficial to React codebases because it allows for better organisation, clearer reasoning and maintainability of the code. Everything is logically grouped, inter-dependencies are brought to a minimum, and modifying or adding a feature happens smoothly and without the need for out-of-scope refactoring.

In addition, your code’s reusability increases. Your logic-less UI components can be packed in an npm package and be used for a new project, accelerating its development and providing a seamless experience across the business products. With recent technology, like React Server Components, your business logic can also be packaged and reused across projects to increase the speed of delivery.

Another area that benefits from this approach is testing. When UI components have business logic mixed in, unit tests increase in complexity - they require specialised UI testing libraries to mimic user interactions, following specific paths around the business logic. In contrast, when the business logic is decoupled from the UI components, your unit tests are more focused - the UI tests are data-driven, meaning that we only need to check the UI matches the expected layout based on the data we provide to our component and that the expected user interactions trigger the relevant events.

By adopting pure components, you can also integrate visual regression testing into our projects with ease. As the name implies, visual regression testing checks the components in your project and reports any visual regressions it can find via snapshot diffing. In a project that does not separate UI from business logic, visual testing is not straightforward and usually requires heavy data and interaction mocking.

Lastly, separating UI and business logic can improve the overall performance of the application by reducing the amount of unnecessary re-rendering that can occur when a lot of business logic runs in the same component. The smaller units make it easy for developers to spot performance issues and heavily optimise only the areas that show them.

Relationships between data and UI

Custom hooks

Since their introduction in 2018, React hooks have become a first-class citizen of React applications. In hindsight, this was to be expected: React hooks are functions, and functions are first-class citizens of JavaScript. This makes hooks one of the main tools in React, a UI library built on functional programming principles.

For me, React hooks helped consolidate the functional nature of React, and they have been the perfect replacement for the old OOP patterns used in React applications.

To understand it better, let’s compare hooks with a Javascript class:

React Hook React Class
State Declared using React.useState Declared in the class or via this.state
Actions Anything can be returned by the hook, including state and actions that update the internal state Declared as class methods, and only visible to the same class. Logic can be extracted in a util file for external use
Reusability A hook can be used however many times and in however many components A class component’s internal logic cannot be reused in other components

With these 3 mechanisms available, hooks offered a new pattern that continues to help engineers develop highly maintainable applications that work great for their users.

Generally, any business logic used by a React component can be extracted in custom hooks that can be shared by different parts of the codebase, even by different applications.

A rule of thumb to follow when extracting business logic from React components is to use the use keyword in a function name, thus making it a React hook, only if the function uses React hooks internally.

Predicate functions

Predicate functions are pure functions that return a boolean value, providing some useful information about the arguments.

The most common use case of a predicate function is in the Array.filter method. It iterates over an array, running the provided argument function (the predicate) with each of its elements as argument, and returns another array, containing only the elements for which the predicate returned true.

const numbers = [10, 4, 9, 2, 0];
numbers.filter(element => return element < 5);
// [4, 2, 0]

You probably stumbled across code that used the filter method, and had to have more than a glance at it to understand its logic.

Let’s take an example:

function getHomescreenPlaces() {
	const areaOfInterest = getAreaOfInterest()
	const places = getPlacesFromDB();
	
	return places.filter(place => 
	  place.workHours.start <= '12:00' &&
	  place.workHours.end >= '18:00' &&
	  place.bounds.x1 >= areaOfInterest.x1 &&
	  place.bounds.y1 >= areaOfInterest.y1 &&
	  place.bounds.x2 <= areaOfInterest.x2 &&
	  place.bounds.y2 <= areaOfInterest.y2
	);
}

You might be able to figure out what the filter function does, but I think anyone can agree that this function is a code smell. How could we do better?

const shouldShowOnHomescreen = (place, areaOfInterest) => {
  return place.workHours.start <= '12:00' &&
	  place.workHours.end >= '18:00' &&
	  place.bounds.x1 >= areaOfInterest.x1 &&
	  place.bounds.y1 >= areaOfInterest.y1 &&
	  place.bounds.x2 <= areaOfInterest.x2 &&
	  place.bounds.y2 <= areaOfInterest.y2;
};

function getHomescreenPlaces() {
	const areaOfInterest = getAreaOfInterest()
	const places = getPlacesFromDB();
	
	return places.filter(place => shouldShowOnHomescreen(place, areaOfInterest));
}

This is a bit better, but the name isValidPlace does not communicate anything about the way the filter acts. Let’s try again, but at a finer grain:

const isInAreaOfInterest = (place, areaOfInterest) => {
	return place.bounds.x1 >= areaOfInterest.x1 &&
	  place.bounds.y1 >= areaOfInterest.y1 &&
	  place.bounds.x2 <= areaOfInterest.x2 &&
	  place.bounds.y2 <= areaOfInterest.y2;
};

const isOpenInAfternoon = (place) => {
  return place.workHours.start <= '12:00' &&
	  place.workHours.end >= '18:00';
};

function getHomescreenPlaces() {
	const areaOfInterest = getAreaOfInterest()
	const places = getPlacesFromDB();
	
	return places.filter(place => 
	  isInAreaOfInterest(place, areaOfInterest) && 
	  isOpenInAfternoon(place)
	);
}

Now this is easier to read - we are only looking for places that are inside our area of interest, and that are open in the afternoon.

The isInAreaOfInterest and isOpenInAfternoon methods are pure and can be easily tested and reused.

Predicate functions don’t need to be used only with the filter method. They should be used everywhere across the codebase when checking for a business-specific attribute on your data. In fact, a best practice to improve code readability is to replace most of the places where boolean logic is used, with predicate functions.

Prefer derived values to synchronised state

Note: Kent C Dodds has a great article on this topic. Make sure to check it out!

This is a pattern that is unfortunately common seen in many codebases:

function MyComponent({ prop1, prop2 }) {
	const [aggregatedData, setAggregatedData] = useState();

  useEffect(() => {
		const someResult = calculateResult(prop1, prop2);
		setAggregatedData(someResult);
  }, [prop1, prop2]);

  return (
    <ChildComponent data={aggregatedData} />
  );
}

In this example:

  1. we receive two props in a component
  2. we apply our business logic in useEffect to calculate a result, which is then stored as component state via useState
  3. we give the stored result to a child component as a prop

Here, using useEffect and useState to compute the data is unnecessary and it will, in many cases, degrade the performance of our component. The same result, but without a performance impact, can be achieved by simply removing the hook calls:

function MySimplerComponent({ prop1, prop2 }) {
	const aggregatedData = calculateResult(prop1, prop2);

  return (
    <ChildComponent data={aggregatedData} />
  );
}

Unless our calculateResult method does costly processing when run, the hook-less variant is preferred, as it’s clean and easy to debug.

When calculateResult is does heavy processing, we can use the useMemo hook to memoize its results and reduce its potential performance impact during a re-render.

To understand better how this works, let’s take a look at what happens in both situations when the component is re-rendered.

Firstly, let’s describe the process it takes MyComponent to display an update:

  1. React renders MyComponent by calling our defined function with its props
  2. The component instructions are run step by step:
    1. we call useState → React finds the component state in its hooks “cache” and returns the current value aggregatedData and its setter setAggregatedData
    2. we call useEffect → React compares the current values of prop1 and prop2, then it executes the associated function (effect):
      1. call calculateResult
      2. call setAggregatedData, which enqueues a component re-render with its updated state
    3. we return the resulting virtual DOM for the component
  3. React compares the current DOM of MyComponent with the new DOM (not containing the updated state), and since the state is not updated until the next enqueued re-render, they are likely identical and the result is discarded, not resulting in a DOM update
  4. The enqueued re-render is executed:
    1. we call useState → React finds the new component state in its hooks “cache” and returns the current value aggregatedData and its setter setAggregatedData
    2. we call useEffect → React compares the current values of prop1 and prop2, and because we just had a re-render and they are identical, the effect is skipped
    3. we return the resulting virtual DOM for the component using the new value of aggregatedData
  5. Finally, React compares the old and new component virtual DOM once again, and updates the browser DOM with the new version

Now let’s describe same process for MySimplerComponent:

  1. React renders MySimplerComponent by calling our defined function with its props
  2. The component instructions are run step by step:
    1. We call calculateResult, then return the resulting virtual DOM for the component
    2. we return the resulting virtual DOM for the component using the result of calculateResult
  3. React compares the current DOM of MySimplerComponent with the new DOM (containing the updated result o calculateResult), and updates the browser DOM with the new version

You can easily see why using useState and useEffect can impact a components’ performance. There is a clear overhead in the hooks approach, which not only introduces more operations during a re-render, but also causes a second re-render of the component.

You can test this behaviour yourself in this example playground.

Console showing logs of rerenders highlighting double rendering in MyComponent

Elevating the code quality in your React codebases is not just a matter of following best practices but also an ongoing commitment to maintaining a well-structured and maintainable codebase.

By adhering to the principles and techniques outlined in this blog post, you'll be better equipped to tackle the challenges of complex projects, collaborate effectively with your team, and ensure your React applications remain robust, scalable, and adaptable in the face of changing requirements.

Remember that code quality is a continuous journey, and every improvement you make brings you closer to a more efficient and enjoyable development experience.

Put these strategies into practice, keep learning, and watch your React projects thrive. Happy coding!

Share this article

Other Articles

Cover Image for The Effects of AI Development Assistants

The Effects of AI Development Assistants

AI-powered tools like GitHub Copilot, Cursor, and Windsurf significantly boost developer productivity, potentially turning average developers into 10x developers. While AI can handle basic tasks, developers with domain knowledge are crucial for breaking down complex problems and guiding AI to optimal solutions.

Cover Image for Effortless Offloading: Next.js Meets WebWorkers

Effortless Offloading: Next.js Meets WebWorkers

In this post, we build an image preview page, discover its limitations, and then offload the work to WebWorkers to achieve high performance by using the next-webworker-pool NPM package.

Cover Image for Effective communication for software engineers

Effective communication for software engineers

As a software engineer, discussing technical subjects with colleagues from other departments needs to happen as flawlessly as possible. In this post, I describe my approach to maximize the value of any meeting.

Cover Image for Using i18n programmatically in Angular

Using i18n programmatically in Angular

This blog post describes my approach to using i18n in Angular components, in a programmatical way. The example usecase in this blog post is displaying API error messages in an user-friendly way, but the approach is generic to any usecase.