mandatory xkcd talking about how code readability should be measured

25 Frontend Refactoring Tips and Techniques

by Pranav Joglekar | 2022 June 25th

In this blog post I am going to explain a list of 25 techniques I used to make my TypeScript NextJS Tailwind code easier to understand. Pleae note, most of these are opinionated ideas which may not be applicable to you but it's good to know them nonetheless. Also, I am open to discussions and to add and remove points from this list if you are able to convince me :)

Anyway, here we go

1. Have a Frontend Conventions Document

This helps to document the frontend conventions that you are following for a project - conventions which cannot be ensured by tools like eslint, prettier. You can mention all the important things here - things you feel are important for new contributors to know about before they start contributing. For eg - Folder Structure, Styling conventions, Order of Imports, z-index values for different levels etc

2. Install and use a linter and a formatter

I don't think this requires further explanation. Each developer has her own unique style of formatting code, and you don't want your codebase to have a thousand different ways in which if/else statements are formatted. Having a linter and a formatter also ensures that the typos are caught before a push and the code is formatted to a consistent, uniform standard throughout your project. A bonus advantages of this is that it makes code reviews easier, as the diff directly points you to the lines which actually changed, instead of it containing unimportant formatting changes. Prettier and eslint is the most famous combo and I suggest you use it for your next project, if you haven't used it before.

3. Semantic HTML

You know all those divs you used? Yes! Using them makes your code unreadable. Writing Semantic HTML is a different article in itself - it's a very vast topic. But the most important thing to know is that divs are not the right way of structuring your code. Use <article>, <section>, <p>, <header>, <footer>, <main> to properly structure your code. With the advent of frameworks like React, this art is being lost, but using the right elements would make your HTML structure much more readable and won't require extra comments

4. Add <div>s with extra scrutiny

Not just <div>s , any element for that matter, but especially <div>s because front end developers have a very bad habit of just using divs to enclose an component if they want to apply some style to it. Instead make use of Fragments, use appropriate CSS properties & layouting algorithms and get rid of the unnecessary <div>s. It would reduce your code, making it much more readable and easier to understand

5. Move atomic components to a separate file, even if they are used only once

React developers break down code into components so that a component can be reused across multiple locations, but they don't usually do it if a part of the code is used only once. This results in the top level page components having 200+ lines of JSX.

The solution to this is to breakdown your monolith component into smaller, atomic sections, even if these sectional components are not re-usable. The folders and components can be structured in such a way that they hint which components are reusable and which are not ( instead of dumping everything into components ). This makes your JSX structure self explanatory and easier to go through at a glance.

6. Break down multiple lines of code into smaller functions, even if they are used only once.

Again, this is similar to the above point, but for functions instead of components. Developers usually breakdown code into functions for modularity. But the same can also be used for readability. It makes your code shorter and easier to read instead of creating a long function which has a large number of lines and is impossible to read

7. Move function definitions outside React Components.

As much as possible, try to move your function definitions outside the React components. This makes your React Components smaller and easier to go through. It also adds to the performance of the component as the functions aren't redefined on every render. This also allows you to extract these functions to separate files and reuse them if required resulting in a more maintainable code.

8. Use Guard Clauses and Early Returns

A guard clause is simply a check that immediately exits the function, either with a return statement or an exception. This makes your code shorter, simpler and less deeply indented reducing its complexity

9. Use async-await instead of then-catch

Yes. There's a rule of thumb. The more indented your code, the more complex it is and the more cognitive effort required to understand it. Callback hells created by then & catch are the most common example of this. That's why its better to use async await instead of multi-leveled then-catch. Even at places where you want concurrency, you can use the native Promise functions like Promise.all

10. Use descriptive variable names, function names, component names, classnames etc

Basically any identifier that you name should be such that the name explains what it does - doesn't matter if its long. The more your code reads like english, the quicker it is to undertand and reason about what it does, instead of having to spend time trying to make sense of all those i, j, k, x and y.

11. Comment your code

Now when I say comment your code, it doesn't mean you add a comment explaining what the ' + ' operator does. But what I mean is you use comments to explain some hacks you've used, or use them to explain some hard to understand code or code which breaks the usual conventions of your project. It's also fine to add comments to explain what an function does - This also acts as a form of an documentation. But it's not fine to comment every line of your code. That should be done by your variable and function names ( Refer previous point )

12. Use modern operators like ?, ??, destructuring to shorten your code

It's debatable whether modern operators make your code easier to understand, but what they definitely do is make your code shorter. And a shorter code is developer friendly - the reader doesn't need to spend time scrolling through the function and there's much less branching.

13. Never use constants directly, especially at more than one place

Always use variables to consume values. Not only does it help with keeping the code modular and easier to modify, it also provides more context to what the value does. Interestingly, this not only applies to JS but to CSS as well.

14. Move constants outside the hook

So you followed the previous advice and are using constant variables for consuming values inside your React Component. But you can improve the performance of the app even further by defining these constants outside the Component. This is similar to point 7 in the sense that it prevents unnecessary redefinitions of the variable when state changes. Also, later if you decide to use this variable in multiple components, it is much easier to shift it to a file of its own.

15. Components should not need more than 5 Props.

Excluding some common and design system related components, your other components should not have more than 5 props. If most of your components do, you may not have structured the project correctly. The more props a component has, the complicated it becomes to understand what it does. The way to resolve this is to obviously structure your code in a better way, but also to use React Context or state management libraries like Redux

16. Create and Use Custom Hooks

Instead of defining state variables inside your component, you can abstract away the complex logic of defining and updating state variables to a seperate custom hook. This makes your parent component more readable - the presentation and the computation layers are seperated.

17. Use a custom hook for fetching API Data

Maybe an extension of the previous point, but the need of fetching data from an API is so common that it warrants its own point. The most common way of receiving data from an API is to declare a state, make the api call when the component loads ( using useEffect ) and the set the state to the data received. If there are more than 2 API calls required on a page, a lot of the logic is reused and the component becomes big and difficult to read. The solution is to use a custom API Fetching hooks which fetches the data and directly gives you the result.

18. Avoid using long arrow functions in JSX.

This is commonly observed for different event handlers like onClick and for props of function type. It's fine if it's a 2-3 line function, but bigger functions should be defined outside your JSX and then passed to the appropriate property. This makes your code readable, but a bigger advantage is it improves the app performance by not having to create this function every time the app rerenders.

19. Remove Multi-Level Ternary Operators

Ternary Operators are helpful, but over using them increases code complexity. Using them upto one level is fine, but multiple ternary levels make the code difficult to understand.

20. Use render functions instead of complex logic in JSX

This is also a better solution to the previous point. Instead of having complex logic in the JSX, use functions which return the appropriate components.

21. Use Render Props

The term “render prop” refers to a technique for sharing code between React components using a prop whose value is a function. This makes your components more modular, also reducing prop dilling in your codebase. The official React site has a very helpful tutorial for it ( https://reactjs.org/docs/render-props.html ). I'll just copy their example here for completeness.

[Bonus]: Have a default order for structuring your code

Personally, I like it if all my components have a uniform flow of code. This makes the code easier to read for me as well as it ensures I don't miss reading out some part of the code which can result in bugs. For this, I've a fixed order in which I order all my imports, and there's also a fixed order for the code in my components - the prop derived variables come first, then come the state variables, then come the custom functions, followed by useEffects and finally the return statement containing JSX.

22. If you need to use CSS Hacks, you aren't structuring your HTML in the right way

If you need to use a lot of CSS Hacks to achieve some obvious things, then maybe you should think about structuring your html in such a way that it's easier to recreate the design without using the hacks. It also increases performance and makes the code more readable this way. For example, I needed to do a full width hack, to have a component take up full width of the page. Although this worked most of the time, there were some edge cases in which it didn't work, and it also make the code difficult to understand. Modifying the structure and placing the element at an appropriate positon in the DOM Tree ensured that I was just able to use the width property to recreate the design.

23. Use packages like clsx for classnames.

Very important if you are using CSS-in-JS frameworks like Tailwind. Instead of performing string manipulations to use the right classes, use packages like clsx, which automatically do these things for you, and provided much more features which makes reading you CSS-in-JS code easier.

24. Use CSS-in-JS frameworks carefully.

Yes. CSS in JS frameworks are a boon. They make developing websites much faster, but at the same time, all the CSS in your JS makes understanding your components difficult, so be careful with them. A rule of thumb I follow if that if a element requires classes to do more than 3 things, it's better to use another way of styling the element ( CSS modules etc)

25. Dont use padding and margin for positioning your elements

Margins and paddings have their use, but using them to position your elements makes it difficult to understand the structure, and at the same time results in non-responsive pages. Use CSS positioning, flex & flex gap, grid & grid gap to correctly place your elements

[BONUS]: Don't do the same thing in more than one way

The final piece of general advice is that if you do the same thing in more than one way, it makes it difficult for the reader to understand. He's left wondering why 2 different ways are used, and whether the 2 ways have some different intricacies to them. Also makes it difficult to change it later and it make refactoring more difficult. This is not just true for language related tasks, but more so for the logic involving business context.

That's all. Thank you for reading this blog. I know, some of the strategies may sound unnecessary but they've worked out for me. If you want to discuss some of these suggestions, or if you want me to add some new ideas feel free to drop me an email.