React has become a word that developers worldwide hear on a regular basis. Since its creation, it has become incredibly popular. One of the best parts of React is that it is not incredibly opinionated on how to use it, which can make it quite powerful and easy to learn. However, too much flexibility also makes it easy to pick up bad habits and poor practices.
Practices and habits can vary from dev to dev, team to team, and company to company - that means, when you go to a new environment, you might be faced with a completely new way of doing things. Being aware of all the ways to approach coding in React can ultimately help any developer find the optimal solution to one. As developers and engineers, our goal should always be to build the best thing we can build within the constraints we have - all while using good practices.
Here are some of the worst practices I’ve seen (and may or may not have done in the past 😅). Hopefully, this will help you avoid making these same mistakes in the future.
1. Prop Drilling
Prop drilling is the practice of passing a single prop down from a parent, multiple layers deep to a child.
Ideally, when passing a prop down from a parent to a child you should avoid going more than 2 layers deep. (Parent -> child (layer 1) -> child (layer 2)). You CAN, in fact, go as many layers as you choose, but be aware that prop drilling commonly causes some performance problems and a lot of headache for React codebases.
Namely, prop drilling can cause unnecessary re-rendering. As a component in React will always render when a prop is changed, intermediary components that are simply passing a prop down the chain will be forced to render when passing the prop down the chain. This can cause performance problems for an application in the long term.
Additionally, it's not fun to track down how data is being passed in an application. Having props pass down multiple layers deep makes finding how data is being used more of a challenge for someone trying to understand an application.
There are plenty of ways to handle this issue. You can try using the react context hook, restructuring your components, or using something like Redux. Unfortunately, Redux tends to be overkill and requires a fair amount of organisation and overhead for simpler applications. However, in scenarios where you need to change multiple things based on a single state change or have one state be the output of another, Redux may be best.
2. Importing more than you need
React is, after all, a frontend framework. It is a library that creates your UI for your user and will be shipped to every browser/mobile device when it is being requested. A React application should be as sleek and slim as possible to avoid sending more than what it is needed to a user. Filling an application with extra dependencies and bloat will decrease performance and increase load times for a user, making your application seem slow.
In order for a user to have a good experience with your application, your First Contentful Paint should be fast (ideally between 0 - 1.8 seconds) so keep your shipped code down in order to simplify your bundle. Most modern app bundlers like Parcel and webpack do a lot of work to minify and compress your code for production, but it's good practice to be aware of what code is being shipped to your client and not just rely on app bundlers to attempt to fix things for you.
3. Not separating business logic from component logic
Generally speaking, it's best to try to keep your components as “UI related” as possible. These components should contain simple code and logic to display what they need to display and manage any state they need to display. While a component may need to make an API call on the first render to retrieve the information it needs to display, it's best to abstract away the logic behind the API call into a separate service utility file or something similar.
Separating out business logic from component logic allows for two things: the separation of concerns and the opportunity to reuse the business logic in other places. A UI component can become complex enough on its own through the logic required to display different parts of the UI. Adding business logic to this can only complicate the component more and make it more of a challenge to understand what is going on.
4. Duplicated work on each render
Rendering can happen often and unexpectedly. This is one of the core tenets of writing resilient components and should be kept in the back of your mind when writing React components. This also unfortunately means that any work that is done when a component is rendering will be re-executed every render. This is where the useMemo and useCallback hooks from React can come in handy. When used properly these hooks can help increase performance by memoizing certain operations that do not need to be repeated every render.
In this example, our component is filtering a list of items based on a prop and displaying them to the user. This type of filtering isn’t always avoidable on the frontend, so this is a great example of when you can use useMemo on a list to prevent it from refiltering every render and only filtering when the items or the filter itself changes.
5. Using the useEffect hook improperly
The useEffect hook is probably one of the first hooks a React developer learns. In the class component days of React, componentDidMount was a common lifecycle function to use to assign event listeners, and in the world of hooks useEffect has taken its place. However, using the useEffect hook improperly means you will end up creating multiple event listeners.
Unless you have linting of some sort enabled with the proper rules, removing the function return here is still valid React usage. Additionally, it's important to use the empty dependency array as the second parameter for the useEffect hook in order to ensure that the useEffect hook will only be run once. It's all too common to see the return function missing in a useEffect hook setting up event listeners and can lead to some tricky bugs to decipher.
6. Incorrect usage of Boolean operators
Now, there are many different approaches to this, and ultimately it can come down to personal preference. Sometimes it will make more sense to break out a conditionally rendered component into its own component, and just return null if the condition isn’t met. Generally, I would suggest that trying to convert numbers/arrays/strings etc. into Booleans for the condition you are checking is the most practical way of conditionally rendering something.
7. Using ternary operators all over the place to conditionally render
A good old ternary expression can be incredibly satisfying when used correctly. But it can be all too tempting to use it to render between choices within a React component. The issue with these expressions is that they can end up wrapping large components and can be challenging to decipher when reading the code.
There is nothing functionally wrong here, but even with three lines of code inside each portion of the ternary expression it looks congested. This can be overwhelming to read as the code expands and even worse as ternarys or other conditional rendering options become nested inside of these. Consider some of these other options as alternatives to this ternary expression.
Option 1: Use a function to abstract your rendering logic
Option 2: Have the components determine how they should render
8. Not defining prop types or not deconstructing props
Additionally, using propTypes on the component gives you more insight into what the component will expect to function properly. In your web browser’s console you will also receive warnings when certain conditions aren’t met on the component’s props. Say in this instance title or content is passed in as undefined or a number instead of the expected string.
9. Not Using Code Splitting for larger applications
Large applications means a large UI bundle. With all the custom components and libraries being used, it can be quite large. Code splitting allows you to essentially ‘split’ your bundle into pieces that can be loaded and requested later, allowing your initial bundle to be smaller and the first piece of code needed to run your app be available faster to your userbase.
Code from react-loadable docs Some good places to consider code splitting is mobile/desktop UIs, routes that a user may not go to initially and modals. There are tons of different use cases here for this, but ultimately it's up to the developers and product teams to understand how users interact with the application and where it would make the most sense to prioritize the app being loaded. React has some examples for code splitting that you can use. On top of that you can also look into React-loadable as an NPM package that works quite well. React-loadable also has some cool patterns that fire a request on hover of an element to fetch required assets. This works quite well on a button that might open a modal that is being split.
‘Write your code today like the next person who will need to read your code is a violent psychopath who knows where you live’.