5 Tips to writing cleaner and manageable react applications

As it stands, react is definitely the leading javascript library for building SPA, at least in terms of adoption, especially in recent years. If you are heading into a website development career, it is definitely a good skill to have under your belt. Depending on who you talk to, this is for different reasons. Personally, I enjoy writing react apps because it leaves a lot of room for exploration, it’s unopinionated about how your code is structured and most importantly it has a very good community and documentation. These are very valuable pros but these could very easily become cons if you are not attentive. For example, because react does not mind how you structure your code, developers are allowed to be as wild as they can get, codebases get so large they span over hundreds of lines. A lot of state and props fly around in the component and make it hard to read for anyone other than the original developer(s), sometimes the original developer even has problem reading the code. In this article, I am going to mention some cool tricks I use to make my react app less overwhelming and more collaborative. Collaboration is one thing developers do a lot of, on projects, hackathons, etc. Developers rarely write code for themselves and that means that care has to be taken so the inheritors of your code do not waste hours trying to get onboarded or get so frustrated that they have to start a new project.

Prerequisites

This is a react code management article and therefore, experience with these tools is definitely well required;

  • React fundamentals including concepts like hooks, context, state, props etc. Refer to the react docs

  • Understanding of javascript concepts

  • Object-oriented programming paradigms

Getting Started

As mentioned before, it is pretty easy for a React codebase to lose manageability and cleanliness because there’s nothing officially enforced. This is not a comparison, but consider a framework like angular. It offers you tools such as HttpClientModule, FormsModule and many more. But react doesn’t, alright. So you get to make a choice between various routing libraries (react-router, reach-router), and various forms implementation libraries and there are technically no standards. Well, at least that’s what many people believe. But there are some unwritten rules that React applications still need to stick to, to ensure that the code can be easily worked on by anyone that picks it up. The most important steps happen right after initializing create react app (CRA). Let us start with just that.

This guide is also useful with React frameworks, like NextJS or Remix. From the moment the app is created, it is important to set up an organized style and formatting for the codebase using common tools like prettier and eslint. Prettier helps with code formatting while ESlint is useful for enforcing syntax and practices. There are a lot of guides on setting up these two tools and they both have great documentation. We won’t talk about how to set them up but know that having them can remove a lot of inconsistencies. You could set a max line on your files and this will let you know when it is time to move some logic to a new file. Now, on to the first trick.

The logic that a component directly relies on should be kept close. A component here is basically the JSX that is returned from a function. If you can keep each file to a single component, it would be best. There is no reason to force 2 or more components into a single file, unless one totally and only depends on the other. The logic that it directly relies on would be the variables that it has declared. The useState, useEffect, function calls, etc. These can be saved to a custom hook that lives right next to the component. Obviously, for react you want to keep all logic in the src directory. One way I structure my src folder is shown below. You might decide to move the ‘api' directory inside ‘utils’. The routes folder might also be moved inside pages, it’s usually just one or two files inside it. If you’re using react-router, you likely won’t need this. You also want to keep shared components (those components that are used around the app by other components) in the same place. Components like buttons, inputs, dropdowns, tables e.t.c.

react application folder structure

a little more expanded directory

One more thing to observe is the image to the bottom, see how all the directories have an index.js file. Declaring an index.js file like that makes it easy to import files from within the folder, use them outside the directory without worries about where they come from. The code below is the content of the services directory;

export * from './auth.service';
export * from './campaigns.service';
export * from './product.service';
export * from './storage.service';
export * from './user.service';
export * from './util.service';

Now when you import a class from this any file in this directory, it’s as straightforward as

import { UtilService } from '../services';

If we decide to rename the stroage.service file from above to the correct spelling. We only need to update the index.js file that exports it and that’s all. If we didn’t have this index.js file, we would need to update every file that imports StorageService, a very difficult and error-prone situation. So, always export files in a folder from an index.js file. This would also mean that you use less default export. I don’t see why most of your files should have a default export anyway.

Use the available Hooks, also make yours

Hooks are nice and very easy to work with. Learn to work with react hooks, use the built-in hooks like; useReducer, useCallback, useMemo. Write your own hooks, they are pretty easy to write. If used well, they will save you a lot of stress. Hooks can be used to extract logic from a component into its own file. Custom hooks are basically React function components that do not return JSX, but instead return a value. All state defined in a hook is reset as soon as the component that uses it unmounts, which makes it a local state. I have a hook that converts the meta-object into metadata, useful for paginating a list or table.

import React, { useState, useCallback, useMemo } from 'react';

export const useMetaData = (meta: any) => {
  const [searchParams, setSearchParams] = useState<URLSearchParams | null>(null);
  const pageIndex = Number(searchParams?.get('page')) || 1;

  const gotoPage = useCallback((page: number) => {
    setSearchParams(new URLSearchParams({ page: page.toString() }));
  }, []);

  const pageOptions = useMemo(() => {
    const pages = Array.from({ length: meta?.pages }, (_, i) => i + 1);
    const midpoint = Math.floor(pages.length / 2);

    if (pages.length <= 10) {
      return [pages];
    }

    const head = pages.slice(0, 4);
    const tail = pages.slice(pages.length - 4);
    let body = pageIndex === 1 || pageIndex === pages.length
      ? pages.slice(midpoint - 1, midpoint + 2)
      : pages.slice(pageIndex - 2, pageIndex + 1);

    if (head.includes(body[0])) {
      body = body.slice(1);
    }
    if (tail.includes(body[body.length - 1])) {
      body = body.slice(0, -1);
    }

    return [head, body, tail];
  }, [pageIndex, meta]);

  return { pageOptions, pageIndex, searchParams, setSearchParams, gotoPage };
};

This hook can be used anywhere as long as the right meta is passed to the hook. This is useful, unlike a regular function. A regular function will not allow us to use other hooks that react has defined. The useMetadata hook above shows how a hook can be used to write custom logic, manipulate state and handle events. Two hooks are very important, but underused when writing custom hooks. The useMemo and useCallback hooks. The former is useful for making variables in the hook react to events and the latter does the same job except for functions. Whenever the custom hook depends on a parameter, the parameter has to be included in the dependency array of useMemo or useCallback that uses it. When you start extracting the logic of your application into hooks, your codebase will look professional and above all, you’ll be able to approach the code confidently and with peace.

Do not be scared of classes

Classes are your tools, you should use them well. One thing classes are very good at is encapsulating logic. You can have a class that is called AuthThunk where you keep all the redux-thunk functions related to the authStore. You can define the methods as static using the static keyword (which allows you to use the methods without initializing the class). And then, when you import them into your components, you only have to import the class. That’s one import, instead of all the functions that might span multiple lines. Let’s see some examples of how to use classes to encapsulate logic and write cleaner code. Let’s define an AuthService class and export it from its module/file, it’s recommended to define classes in upper case.

export class AuthService {}

Once you have your class defined like this, you can add multiple methods (we call functions defined inside a class methods) as needed. In a real use case, you won’t be having an empty class like this. Considering our AuthService class, it can be used to encapsulate logically related to authentication (and maybe authorization) and possible methods it can have are related to signing in and out, user signup, and related. So a proper definition would be;

export class AuthService {
  static login(user: LoginModel) {}
  static signup(user: SignupModel) {}
  static logout() {}
}

// To use these methods in another file
import { AuthService } from './services';

const LoginPage = () => {
  const login = (model: LoginModel) => {
    AuthService.login(model);
  }
}

See how we can easily manage these methods from one single class. This is how powerful classes are. Basically, we defined a class with static methods, that way it can be used without needing to initialize the class. A couple of things you can use a class for, best way is to experiment with them, to be honest. I consider this a cleaner way to define these methods than having them written out in the component, or defining functions for each one of them (in the same file, or separate files). Consider using classes to define the logic that will be related and need to be kept together.

Be careful with state management

This is probably one of the most challenging tasks you will have developing a react app. There are severalstate management tools available to you and how you use them is important. State in React can be managed globally or locally. Local state management is done using useState or useReducer and inside the component. They are defined when the component mounts, they can be passed down to children as props and they reset when the component unmounts. Global states on the other hand are well, global. They can be accessed by any component in the app that is covered by their provider. They do not reset when a component that uses them unmounts, so you have to reset the state if need be. You can create global states using redux, context API, mobx, react-query.

When you define a local state, then you have to pass it down to children components that need the same state, also passing down the setter function. This is useful if it’s a very small component and no other part of the app will need that state. When you define a global state, you don’t need to pass the state down as props, because every component that needs it can just use it. The important thing to note is theposition of the provider. This is very powerful but tracking the state is getting more complicated. In both cases, the concept of state lifting is important. This means, defining the state in the closest ancestor component so that it can be passed down easily to children components that need it.

Be intentional about styling

The way you style your code is important also. Personally, I think the best way to style a react app is to use a CSS-in-JS solution (like styled-components or emotion) or use SCSS. These are soft to work with because if your components are written well enough, you can create a tree-like structure for your styles, easily encapsulating them and putting them right next to the component they style. If at any point a component seems to need the exact same style and structure, you can extend that style and use it. This is a good choice as opposed to having a lot of classNames in your JSX structure. There are a number of ways I have seen CSS-in-JS used in react apps, like;

import styled from 'styled-components';

const Container = styled.div``;
const InnerContainer = styled.div``;
const Header = styled.h1``;
const Button = styled.button``;

const MyComponent = () => {
  return (
    <Container>
      <InnerContainer>
        <Header>My Header</Header>
        <Button>Click Me</Button>
      </InnerContainer>
    </Container>
  );
};

The above creates a style for each JSX component, or at least for every important one. While this is a popular approach, I find it overwhelming, especially because you have to write new styles. If there is another component with a similar structure, then we have to import each style. There is a very important feature of CSS-in-JS, one that they share with SCSS and that is nested styles. With this, we can define a nested structure for our CSS that makes it super easy to reuse elsewhere. Apart from ease of reuse, it also makes the code cleaner avoiding needless exports and imports.

import styled from 'styled-components';

const Wrapper = styled.div`
  & > div {
    h1 {}
    button {}
  }
`;

const MyComponent = () => {
  return (
    <Wrapper>
      <div>
        <h1>My Header</h1>
        <button>Click Me</button>
      </div>
    </Wrapper>
  );
};

Heading

The code above uses the nested style pattern to style the component. If we need to use this Wrapper elsewhere, we can import only that and use the same structure as we have here. One thing this method grants us is the ability to write modular styles. Personally, I believe this approach to be a very fast and efficient approach.

Libraries like tailwind, and bootstrap are also useful in styling, especially tailwind (because of the degree of customization possible). You should use the utility classes they provide sparingly, the moment you notice yourself having to define more than 5 classNames on a single element, then maybe it’s time to consider using styled-components then.

Conclusion

If you have ever worked on a react app that felt difficult to handle, I am sure you must have experienced frustrations like these. React will work any way you write it, as long as you return valid JSX but that does not mean it should not be written in a way to make it readable and clean. There are a few things you need to do to ensure you have a good codebase and these are just some of them. You should ask questions like, can this type of logic be used elsewhere? The way your directories are structured is also crucial, you need to create directories intentionally. Functions that are not directly related but handle logic over the app need to be in a utils folder. API calls are generally contained in an api directory to give the codebase structure and order. This also helps teammates to easily know where to look if there’s a problem and will speed up development time much more.

This article and codebase were inspired by Jeff Oghenerukevwe Ofobrukweta (Medium), a senior full-stack engineer at Proxify and an ex-domain leader at Outliant.