React Best Practices: How to Write Better React Apps
A summary of modern React best practices, with tips that can be applied to real-world applications.
What is React and why use it?
While web development isn’t new, some aspects of it are. In particular, frontend engineering is somewhat of a new term. Back in the day, you would see “web designers” and “webmasters”, but rarely frontend developers.
There’s a good reason for it - there was no need! Websites were simple, mostly static, and the bigger complexity was around designs, not functionality.
Needless to say, we are way past that now. Websites evolved into web apps, browsers became more powerful and the era of desktop applications is over, disrupted by the eruption of mobile devices. While it took some time for the consensus to establish in web apps and not in mobile apps, it seems to be fairly understandable now that web apps provide better availability, portability, and ease of use than native applications. And as mobile devices become more powerful, and optimizing for the web becomes a sexy engineering problem, the end result is an outburst of well-engineered, optimized, and resourceful applications right here, on the web.
But as we have learned from the history books, there’s no power vacuum. The web was set to reign, but who shall be the king? Enter React – a JavaScript library for building user interfaces.
This library maintained by the fine folks at Facebook took the market share and ended once and for all the JavaScript fatigue: that feeling everyone around in 2015 had. Every day a new frontend framework is competing for the top spot. Then eventually React appeared, people liked it, and the problem was settled.
This (not so) short introductory story is the answer to what React is and why you need it. Because React is a JavaScript library for the ever-growing frontend environment, you need it because it is popular, way maintained, liked, and used throughout the globe, in FAANGs and local shops. You can’t go wrong choosing React.
What makes React hard to learn for beginners?
React doesn’t have a particularly hard abstraction or technical conception. On the contrary, its single source of truth mantra has been a breeze for seasoned web developers.
However, this change in paradigm (for the better) can hit harder if you are used to using vanilla JavaScript or the last king, jQuery. Communication is rarely event-based, as opposed to what JavaScript typically enforces by design or with its backend counterpart, NodeJS.
Componentization is a concept that is easy to grasp but demands time to master. Hooks brought the functional flavor that seems to be the current trend, despite component lifecycles that may be more familiar to Java developers.
In short, what makes React hard for beginners is that it is React, not an adaption, a fork of a known framework, or an abuse of the JavaScript language. It’s a new paradigm, focused on User Interfaces, simplicity, and opinionated state management.
React best practices
Every language and framework has the so-called “best practices”, i.e., a community-accepted set of practices when using the tool. The following isn’t an exhaustive list of them, but an attempt to list the ones that typically concern the average developer.
Security
Every software engineer that had code shipped to production had to go over this drill. Is the code secure? Are there flaws? Are they exploitable? Given the architecture of the web, this matter is even more flagrant.
However, I’d argue that React doesn’t pose any special security threat that any web application doesn’t have. Be aware of XSS attacks; store authentication tokens safely; sanitize HTML, and so forth.
Sanitize HTML
By having its own templating engine that allows dynamic variables to be inserted into HTML, React exposes a security issue that every web developer should be aware of: injecting malicious code in HTML.
As the classical example goes, we don’t want users to pass any kind of script that can be rendered on an HTML page. Because of that, there’s a process called sanitization, where we allow only specific tags to be rendered on a page.
Imagine you have a CMS like ButterCMS and render the content generated by the content creator without any kind of filter. Well, React exposes the property dangerouslySetInnerHTML that serves this very purpose: get a raw HTML for an API, and render it in an element.
But the API isn’t named like that for nothing. Suppose this content contains a valid script that collects user information and posts it back to the attacker database: you’re essentially adding a trojan horse to your website!
In order to prevent such cases, you can use libraries like sanitize-html to allow only specific tags to be rendered. For example, if your goal is to render a blog post, you can allow only the traditional <div>, <p>, <b>, <i>, <img>, etc tags, and block suspicious ones like <script>.
Secure APIs
Although this isn’t a problem specific to React, this is a common problem faced by any front-end developer. We typically need to connect to our brothers in blood, the backend engineers, who have carefully crafted an interface for us to communicate.
However, this API often is secured, meaning that you need proper credentials to access it. The authentication process occurs mostly in the backend but is the front-end's job to keep the access token safe and sound.
There is a lot of debate about whether you should store it in the Local Storage or in cookies, and this matter warrants a post in itself. But while this is a personal choice and both can be used under the right circumstances, I find it crucial that you, as a developer, understand what the tradeoffs of each solution are.
I have used both, and suggest using cookies if you can. They provide more security layers and control, and if used with a compatible library in the backend, like Python’s Flask, the implementation is a breeze.
In any case, before judging your friend who stores a JWT in the Local Storage, try to understand where the flaw is. Saying “if your app JavaScript is compromised you are screwed” isn’t really an argument; once you’re compromised, well, you are screwed anyway. Understand your app’s needs first, and the rest should come naturally.
Install linters
I mentioned before how good practices are nothing more than a community set of accepted practices, but as you have probably thought, this is subjective and hard to enforce.
Linters come to the rescue. There are awesome linters that will encapsulate a set of JavaScript or React-oriented best practices and complain whenever you drift off. This is great for ending an argument – people will often disagree on how to best handle a particular scenario. A linter, or a programmatic way to warn about an unexpected code usage, will act as the mighty judge, settling the matter once and for all (you need to agree on the linter first though).
Linters are also great for big teams, where many people touch a single file repeatedly. If everybody uses a linter, you cut off most of the inconsistencies, save development time and make reviewing code a breeze.
When coding, you shouldn’t focus too much on aesthetics. Sure, they are nice. But the less you need to worry about it, the better. Let developers focus on what they like, namely, code.
Testing
Think about that: you are already a developer, why would you do something manually? Programmers of course thought about that, and resisting the temptation of overengineering, also agreed on writing code for testing their own code. Isn’t it beautiful?
Leverage React debugging utilities
There are a set of React practices that are often neglected. Error Boundaries are one of them.
When developing React applications, an interface is typically composed of several components in a tree that can escalate quickly. For that reason, if there’s an error in the application, it’s common to be lost in the hierarchy of components.
Error Boundaries can help you by raising the red flag on the exact node that experienced the flaw. More modern frameworks like React Remix leverage this concept to enhance user and developer experience. Give them a shot and caught the issues in the act.
Another way to add better-debugging tooling to your web app is to convert to TypeScript. Fortunately, TypeScript is widely adopted and integrated into the React ecosystem now. With TypeScript, you get top-notch auto-complete, caught errors on the act, and reduce runtime errors by leveraging features like type-checking. Pairing the typed JavaScript version with a good text editor like VSCode is a game changer for serious web development.
Invest in test-driven development
Beware when getting into the world of testing – you risk becoming addicted to it. In fact, some people like it so much that they develop backward! First, they create the test, then they write the code.
While this coding style isn’t for everyone, this is particularly interesting when developing a functional application or library. Suppose that you are working on a calculator. There are some well-defined cases that you want to handle, and it’s just fair that you work from there: you start by checking that addition works, then you move to multiplication, and handle edge cases.
For more complex applications, I suggest treating your tests as user stories. Suppose you are working on a feature that needs to do X, Y, and Z. They are valid, strict metrics that you will be testing against. Once you established all the roadblocks, traps, and edge cases, it becomes way easier to just sit down and do what you already know.
Don’t know if this is for you? Well, test first, and then let me know (pun intended).
Install React Testing Library
Everything I mentioned before about React taking over the power void can be applied to React Testing Library or RTL. Designed for React and later adapted to other frameworks, the Testing Library took over hacky approaches like Enzyme, where developers would spend more time fixing tests than actually testing their applications.
React Testing Library has a simple API, a big community, and good principles, and should be your go-to library when unit testing your code.
Configure user events
One of the main advantages of React Testing Library is abstracting away user events in single lines of code. For example: what if a user presses a specific key? Or if they move their mouse out of the window? Or focus on an input?
All those are events that can happen in a real app and oftentimes have a direct impact on what should happen next. For example, you can assert that a bouncing modal pops up if the user tries to leave the screen (it’s annoying but effective… what are you gonna do?). A clear and simple API like the user events API from RTL is all you need in order to test like user stories.
Components
The heart of React, components are the best friend of the frontend developer. They encapsulate logic, UI blocks, and functionality. But how should you approach designing them?
Keep component creation to a minimum
Component creation should be understood as building a LEGO. You design the parts and glue them together. You start small, where each part is focused on its own job. It’s the UI equivalent to functional programming, where each function has a single purpose. In fact, components are functions! So the comparison holds even stronger.
When designing an interface, it’s often useful to draw it on a piece of paper. Using the same principle as in the test-driven approach, you have a component-driven approach. Start by designing your interface and what constitutes each part of it – maybe you have a side panel, a list of results in which every result has a call-to-action button… design them as an architect would design a living room, and the rest will flow naturally.
Introduce state
Using React for building user interfaces is nice, but if they are static there is nothing really exciting about them. Or at least nothing our predecessors, web designers, wouldn’t do with tools like Photoshop.
What transforms React into a really powerful library is its power to, well, react to users' interactions. That’s the heart and soul of frontend engineering – develop a nice UI, sure, but what good is a sleek-looking UI that isn't easy to use?
What solution does React brings to the table? The app or component state will dictate how your UI should behave. For example, it can display an error message for invalid input, disable a button for an incomplete form, share context and variables across many parts of the app, count how many times you have performed an action, tell whether you should render a modal or not… there are endless possibilities that are exactly the reason why frontend development is so exciting: the user input is a new dimension in your variable matrix.
A stateful component is a component that renders differently based on its state. An input component that renders an error message or is disabled is a good example. On the other hand, a stateless component is a plain static UI, but is still useful. Suppose you want to render your website footer. Instead of duplicating code left and right, you can develop it once and call it anywhere, pretty much like a function, generating one source of truth, best for maintainability and consistency.
Use Props to pass data to a children component
Sometimes your component is not stateful but still depends on outside variables. Think about a template, for example. It has a recipe, a specific form, but with “gaps” that are dynamically filled (variables).
Properties, or props, are precisely a way to customize a component that should always behave the same but can accept different data. Think about your driving license: everyone has the same, they look the same, and their layout is the same, but the name, number, expiration date, and whatnot are different. Those are the props filling up the gaps.
In React, props are always passed from father to child. We saw already that componentization transforms your app into a huge tree of code, and the communication needs to be direct, and top-down (there are some exceptions like contexts, though).
While this can be annoying occasionally, it really gives predictability to your code. If a variable is such, it must be because it is like so in the parent. And hence you can trace back that value to its origin, helping to debug a lot.
I remember my old days working on CakePHP where I would spend hours trying to decipher where that variable had come from. It was hard because communication wasn’t direct, top-down. It was buried somehow in contexts and controllers. Call it pragmatic, but it will make your life easier.
Use different component patterns
As always, there is never one single way to tackle a problem. Before hooks took off on the 16th version of React, there was a famous React pattern called HOC, or Higher-Order Components.
In this pattern, you encapsulate a particular component with another one, like a sandwich. But this gives it superpowers, or in less exciting terms, new props for it to use. For example, you could wrap your component in a HOC that would monitor the screen size in order to pass to your child component the width and height as props.
In some sense, Higher-Order Components worked as decorators, injecting logic into a given component. As mentioned, though, React Hooks was a great breakthrough and replaced this behavior with a nicer, more reusable API. Hooks work similarly to plugins, where you call them in your component to get a specific behavior.
Also before hooks, React components would be class-based, bringing more of the objected-oriented world to the frontend community. They would provide lifecycle hooks instead, where you could act when a specific event occurred, like the component mounting, or a specific prop changing.
The React team replaced this paradigm with a new one, using hooks, called useEffect. Now, you can monitor everything that makes a component re-render, the act of being updated because something in the tree changed. It’s up to you to monitor and act accordingly to whatever state or prop change happened.
While more flexible, arguably some developers miss the straightness of componentDidMount or componentWillUpdate. If that’s who you are, you should check class-based components.
Styling
A hot topic with a myriad of alternatives, styling a web app is never a consensus. Constructed on top of a controversial piece of technology, we shall explore how you can approach styling in your React applications.
Try inline CSS
The simples solution, by and large, is to embrace tradition and use an inline style like everyone was doing with HTML in the 90s. There’s nothing really wrong with it – with the exception that is hard to reuse.
React will handle style pretty much like a JSON object, and this will be the source behind the multiple CSS-in-JS frameworks that emerged. In any case, inline CSS is good, simple, and efficient. They will lack performance improvements (like being able to cache a separate CSS file), and maybe software engineering best practices. But they will work just fine.
Import styled-components as an object
Using JavaScript to style your components has the unbeatable advantage of being able to control the style dynamically. Suppose you are building a component that changes its color based on a given input. JavaScript and React will make your life a breeze because you can just conditionally use the input to render the desired color.
Of course, people wouldn’t stop there: if you can use JavaScript in such cases, you can also use them to control other aspects of your styling. For example, font size, brand colors, default line height, etc. Using technology like styled-components allows you to write CSS-in-JS while creating a hierarchy of values that programmers so often like.
And as I mentioned before, the style property is nothing more than a JSON object. So you can leverage JavaScript constants to build your style schema, inherit from it, and write your component’s style as you would write code. Neat.
Abstract style components
Styles are good and a simple solution, but as a project grows it won’t scale. Think about how many primary buttons you have in your application and you will know what I mean.
As I mentioned in the previous section, styled-components help you to create a basic structure and build from there, with basic properties being set as constants. You can then combine them and create more complex structures, like buttons, paragraphs, headers, heroes, inputs, and so on.
In fact, React creates the perfect ecosystem for you to design your own component library. Forget about Bootstrap, Semantic UI, and their peers – with React, you please any designer you work with consistency and accuracy.
Use CSS and SASS stylesheets
Everything I said thus far is great but some freaks insist on using plain old CSS files. Why? Well, I can tell (I’m one of those freaks). CSS is a technique on its own, and I prefer the separation of concerns of having a file dedicated to styling. It’s easier to review, change and adapt. Also, it can be cached when performance is at stake.
With the surge of frameworks that allow CSS Modules, the typical problem of classes clashing with each other is over. Plus, it generates a fingerprint that helps developers debug by targeting the precise component with that particular style.
And if you want the option to add parameters to your classes, like a brand color or semantic font size, SASS can help you. Pair both and you will get the best of two worlds, while still looking cool by using a time-tested fool-proof technology like cascading stylesheets.
Project structure
React is definitely not an opinionated library. As Uncle Ben would recall, “with great powers come great responsibilities”. I would also say, in a poetic tone, that you are free to do whatever you want, but a prisoner of the consequences.
With that in mind, let us take a look at how you can structure your React project.
Split components into separate .tsx files
We already mentioned the power of componentization and treating components as you would treat functions in a functional paradigm. But in React this is even better if you enforce that only one component should exist per file.
First, files become smaller, and easier to tweak and review. Secondly, the separation of concerns buzzword rings again. By having a single file handling a single part, it’s very easy to spot and tackle eventual changes. Finally, if working on a larger team, will reduce conflicts and make collaboration a breeze.
Give each component single responsibilities
I’m a big fan of small, self-contained files, functions, and components. Think of them as small building blocks that are combined and used to create something bigger, like the pyramids, or a computer processor.
By assigning single responsibilities to each component, you are essentially functioning as a factory, with ultra-specialized employees that handle one thing and that thing only. Therefore, if X goes south, you know immediately that you need to talk with the one responsible for X. If each component has a single responsibility, it follows that it must exist only one component for it.
Combine multiple components with single responsibilities and you get your own production line.
Group files by structures or routes
We have already learned that React won’t enforce any file structure. Some people will circumvent this freedom but enforcing their own strict rules. I disagree. I believe React is better experienced if you leverage this freedom.
I explain: a successful folder structure is one you don’t need to think about. Frankly, the less you need to think about those petty things, the better. On that note, I like to group files semantically, or when they belong to a specific chunk of the app.
Have a route that renders a specific view? Add all your components there. Created a new page? Create also a new folder and add the corresponding components there as well. Simples structure. Not much thought is needed.
I oftentimes see people trying to be smart and breaking down components in multiple folders, with technical characteristics attached to them, like calling them atomic. I don’t like it. This means creating a new file or folder is no longer a no-brainer, and should be actively reviewed at the expense of having inconsistent file structures across the board. If you don’t care about the inconsistencies, then why create a rule in the first place? Moreover, you really don’t gain much (if anything) by adding a custom file structure to your projects. Developers aren’t supposed to be navigating in folders anyway. Components are small and self-contained, which means you potentially have a lot of files, so what’s the use of manually looking for them? Hence my proposal: group files by routes or pages, and forget about it.
Avoid too much nesting
React is a big tree of components as discussed, and this is fine. However, if you are nesting too much, you probably are doing it wrong.
There are so many levels that you can descend. You have a list, a CRUD, a modal… you can’t nest forever. If possible, favor the horizontal spread of components, in its own routes, shallow enough that you are not lost in the abstraction and easy enough for a developer to find his or her way through the code.
Using React with a headless CMS
React is a library for building user interfaces, as we have seen. But it has some interesting integrations. For example, you might want to have your app’s content set somewhere else but render it in your shiny, fast, and efficient React app.
For such cases, content management systems like ButterCMS are a breeze. I spoke already about the separation of concerns, but this isn’t only related to code. In fact, the more you can separate areas of expertise, leaving people to their bread and butter, the better.
If you have a team of content creators, you can give them the freedom to put their creativity to work, generating content that will later be consumed and rendered in your application. Your developers shouldn’t need to have additional work if content changes – the own project structure should be able to do it.
Connecting a CMS provider with a React framework like GatsbyJS will empower you to do exactly that – connect to the content being generated, listen to changes, and rebuild your site with fresh content.
Server-side rendering applications like NextJS will also be valuable by fetching data from the CMS and serving to the user as requested.
In short, the JavaScript ecosystem for content management has evolved to the point where having blazing-fast, SEO-friendly web apps can be achieved with both great content and developer experiences.