How to Make a Production-Ready React App
Posted on: 25 April 2024 | Last updated: 25 April 2024 | Read: 14 min read | In: React, JavaScript
Description
Full blog
Hello world 👋, today I wanted to share with you some of the things I learned when making a React Application. We can call these tips "production ready" because they will help us ensure that we build software that is scalable, secure, and maintainable. I won't cover security points as it's a broad subject.
We will cover different concepts related to React and I will share my tips on how to make it production-ready. Please note that there's always more to making a production application but this could be a good starting point.
Component:
Components are a core part of React, so writing them properly is important. When making components we should keep a few things in mind:
One Component, One Task:
A component should do only one task. Components in react should be as simple as possible and shouldn't tackle more than one task. For example, if you are making a component for a button, it should only handle the button logic and shouldn't handle any other logic like API fetching or state management. This makes the code more readable and also makes it easy to debug. When at it, also make sure your component is reusable but not too generic. Often, people try to make components generic so that they can reuse them, but at times it can lead to one component handling multiple tasks.
I will explain what I mean by making component re-usable not not too generic. Let's suppose you want to make a button component, there's how to must code it up:
const buttonClassName = "button";
const Button = (props) => {
const { text, onClick, className ...rest } = props;
return (
<button onClick={onClick} className={`${buttonClassName} ${className}`} {...rest}>
{text}
</button>
);
};
Nice and simple, right? This is a good example of a reusable component. It's simple and does one thing and that's rendering a button with some styling that you might have defined. But not let's make it 'too generic', suppose now you want to accept a icon and render it on the button. You might do something like this:
const Button = (props) => {
const { text, onClick, className, icon, ...rest } = props;
return (
<button
onClick={onClick}
className={`${buttonClassName} ${className}`}
{...rest}
>
{icon && icon}
{text}
</button>
);
};
Ops! A new design just came in and in that design where a logo which is rendered on the right side of your text. Now, you change your button to accept rightIcon
and leftIcon
and that will look something like this:
const Button = (props) => {
const { text, onClick, className, rightIcon, leftIcon, ...rest } = props;
return (
<button
onClick={onClick}
className={`${buttonClassName} ${className}`}
{...rest}
>
{leftIcon && leftIcon}
{text}
{rightIcon && rightIcon}
</button>
);
};
It looks alright for now but now there's a requirement to add a spinner in some cases instead of the text and the icon. You can still do that using the same component and a few more props and if/else conditions but things will start to get messy & will impact readability. This is what I meant by making components reusable but not too generic. Our button component which started out as a nice and simple component is now too generic and is handling multiple tasks. Here's what I would recommend you do to
- Keep your button component same as the first example.
- Instead of passing left-icon, right-icon and spinner as individual props, pass them as children. This allows you to render anything inside the button however you like, without making button component too generic.
<Button onClick={handleClick}>
<Icon name="left" />
Click Me
<Icon name="right" />
</Button>
<Button>{isLoading ? <Spinner /> : "Click Me"}</Button>
Keep the state as local as possible:
Data or state should be as local as possible. If a piece of state is required in one component, it should be handled in that component only. We will discuss why this is important after this example.
You shouldn't do this 👎
const Parent = () => {
const [counter, setCounter] = useState(0);
return <Child counter={counter} setCounter={setCounter} />;
};
const Child = ({ counter, setCounter }) => {
return; // JSX
};
Rather you should do this:
const Parent = () => {
return <Child />;
};
const Child = () => {
const [counter, setCounter] = useState(0);
return; // logic
};
This not only makes the code readable but also avoids re-render in components that don't need to be re-rendered. In the first example, both the parent and child components will re-render when the state changes, although the parent component doesn't need to be re-rendered. This can cause performance issues in large-scale applications. In the second example, only the child component will re-render when the state changes.
Avoid props-drilling:
Props drilling means passing props from the parent component to the child component to its child component and so forth. This is bad for multiple reasons:
- It re-renders all the components through which the props are being passed. Which can cause performance issues.
- It makes code look bad, which can make it harder to debug, maintain and understand.
- It doesn't follow the above principle which is to keep the state as local as possible.
If you find yourself doing props drilling, consider using state management or re-structuring components. Passing props 3(max) level deep should be fine but anything beyond that should be avoided.
Component should be dumb:
The components should be dumb. In an ideal world, your component should only return JSX and have few states or just handle simple logic. If you find yourself handling complex logic in your component, you should divide those logical functions into utility functions or should use 'custom hooks'.
Remember, how the button component started became messy as it became too 'smart'.
hooks
Hooks are a big part of functional components in react. You can use predefined hooks in React or make your custom hooks to achieve certain tasks. In this section, we will discuss how to use hooks properly.
useEffect
is low-key magical:
useEffect
is one of the most useful hooks in React. useEffect
takes a function as an argument and runs that function when the component is mounted, unmounted, or as a side-effect to state changes. Optionally, useEffect
takes an array(AKA dependency array) as a second argument which is used to tell React when to run the function. If the array is empty, the function will only run when the component is mounted or unmounted. If the array contains some variables, the function will run when the component is mounted, or unmounted, or when one of the variables inside the dependency array changes. If the array is not provided, the function will run on every re-render. This 'dependency array' can be very useful but can also cause some weird issues and make code less explicit. I like to call useEffect
magical sometimes because sometimes as a side-effect it can do things that don't make sense.
I will show you an example, where useEffect
shouldn't have been used:
const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(null);
useEffect(() => {
// get the value from local storage
const item = window.localStorage.getItem(key);
setValue(item ? JSON.parse(item) : initialValue);
}, [key, initialValue]);
// if any change happens in value, set the value in the local storage
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [value, key]);
return [value, setValue];
};
In this our side-effect is the second useEffect
which is setting the value in local storage. It may be clear to us right now but it's not explicit thus can cause confusion to other developers or even to us in the future.
We can easily make this more explicit by creating a dedicated function for setting data in local storage. You may not appreciate this example right now, but if your custom hooks or components get bigger, you will appreciate this.
const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(null);
useEffect(() => {
// get the value from local storage
const item = window.localStorage.getItem(key);
setValue(item ? JSON.parse(item) : initialValue);
}, [key, initialValue]);
const setItem = (newValue) => {
// set value to both state and local storage
setValue(newValue);
const valueToStore = JSON.stringify(newValue);
window.localStorage.setItem(key, valueToStore);
};
return [value, setItem];
};
I fear useEffect because of many reasons like:
- It can cause infinite loops if not used properly.
- Post React 18, useEffect runs twice when the component is mounted. A lot of people don't know this and it can cause confusion.
- They are not always explicit and can cause confusion.
but they can't be avoided because they are very useful. So, use them with caution.
Memoization using useMemo
and useCallback
:
useCallback
& useMemo
are often used to memoize functions and variables. This can help avoid re-initialization of functions and variables. This gives a significant performance boost if your function or variable is computationally heavy. But you should be mindful of when to use useCallback
& useMemo
as they can consume a lot of memory, cause components to not re-render when needed, or even make code look unreadable. My general rule of thumb is to use useCallback
& useMemo
only when ESLint tells me.
When I first learned about useCallback
& useMemo
, I used them everywhere, thinking that's how we make 'production-ready' applications. But that's not the case! Use them only when necessary.
Custom hooks:
You can create custom hooks to handle your logic or state. Custom hooks help you to keep your component clean and dumb. You can create custom hooks for a lot of purposes. Like, fetching data, getting the user's location, handling theming, getting/setting data in local storage (like we did above), etc. Custom hooks are wonderful and I love them because they abstract the logic and make the component clean and dumb. I will probably write a blog on custom hooks in the future.
Folder Structure:
I stress a lot on folder structure because I believe it's the one of the most important part of not just React but any project. As it makes navigating through the project easier. Thus, structuring your project is important. This not only makes the code base looks good but also make it easier for future you or any other developer to understand what is being handled where. I recommend you go through MVC, and other design patterns to understand how to structure code well. Although React is not MVC you can still learn about MVC to understand how to structure your React code-base.
Other tips
Following are some other tips that I would like to share:
Third-party package:
Knowing when to use 3rd party package is important. You don't want to download a 3rd party package for everything but you also shouldn't refrain from using packages to achieve certain tasks. When deciding which package to use, you should go through its GitHub stars, its activity, and downloads. And if you are feeling pro-ish, you can also go through its code. Going through the source code of a package not only helps you to understand how it works but also helps you debug if something goes wrong. Some beginners refrain from using 3rd party packages because they think it's always better to write your own code, I thought the same when I started out but I couldn't have been more wrong. We use 3rd party packages because they are well-tested, well-maintained, and have a lot of features that we can't build in a short amount of time. The code you write is liability they waste your time and can potentially break your application. So, use 3rd party packages but use them wisely.
Some of the 3rd party packages that I use are:
- SWR: SWR is a React Hooks library for remote data fetching. It is very easy to use and has a lot of features. I use SWR for fetching data from API.
- Tailwind CSS: Tailwind CSS is a utility-first CSS framework. It is very easy to use and has a lot of features. I use Tailwind CSS for styling my components.
- React Hook Form: React Hook Form is a performant, flexible and extensible forms with easy-to-use validation. I use React Hook Form for handling forms.
- Axios: Axios is a promise based HTTP client for the browser and node.js. I use Axios for making HTTP requests.
Use typescript:
Typescript is a superset of JavaScript. It adds types to JavaScript which makes code more explicit and understandable. Typescript is among those things that you don't think are useful until you start using them. Many people refrain from using Typescript because of various reasons but the most prominent are:
- It's hard to learn. No, it's not! I often see people in Reddit community say it takes minutes to learn Typescript but it saves hours of debugging. I agree with this statement.
- I don't need it. Yes, you don't need it but it's good to have it.
Use ESLint:
ESLint is a static code analysis tool for identifying problematic patterns found in JavaScript code. It's very useful and will force you to write clean code. Use Airbnb's ESLint configuration, if you are not sure which configuration to use. I absolutely love ESLint or Linters in general because they force me to write code which is accepted by the community of that language.
Bootstrapping react project:
I don't recommend using CRA not only because it is officially deprecated by Facebook but also because other libraries and frameworks like Next.js, Gatsby, etc provides more features with React.
There you have it, some of the tips that I learned over time. I would love to thank my colleagues, mentors, community, and the internet for helping me learn these things. Lastly, I would like to thank you for giving me your precious time and reading this blog. I hope to see you again! Jai Hind!