I never thought that writing efficient React code required so little effort. In essence, it’s basically just a bunch of functions and classes spitting out markup, and occasionally interacting with each other.
Going from a small, pet project to a big-ass application is almost seamless — you build medium things from small things, and big things from medium things. Voilà, you have yourself an app!
To help you reap the benefits of React and its ecosystem, let’s go through some of the most important patterns when building efficient applications!
Smart and dumb component separation
Give yourself a pat on the back if you read
Dan Abramov’s post about Presentational and Container Components. It gives you a perfect introduction to the idea. The next example shows how to build a simple smart component that utilises two dumb components.
You first want to make a root component that will serve as the container. It will cover all the possible side effects, gather all the necessary data and set up all the possible callbacks that will be propagated to your dumb components.
Up in the hierarchy lies our root component:
This example component has everything you will commonly see in smart components:
- Side effect via one of React’s lifecycle hooks
- Reacting to user inputs via handler functions
- Supplies dumb components with the necessary data and callbacks
A good analogy for this dumb and smart separation can be taken from Haskell — pure and impure. Pure components are the dumb ones that simply spit out markup, impure ones kind of lie in the IO monad, they take care of all the side effects and interactions.
Let’s see a dumb component in action:
All it does is display todo’s text and an icon. There are no direct side effects, no lifecycle hooks, only content.
Although it isn’t a functional component because of the event handlers it provides, it’s still considered dumb because it only propagates the event upwards for its parent component to consume and cause effects.
You will often encounter dumb components that are functional. If we didn’t have any callbacks, our dumb component could look a lot dumber:
Basically the only time you want your dumb components to be classes is when you have to partially apply a callback, but more on that later.
Smart components are the brain, dumb components are the muscles! This kind of separation will allow you to build more reusable (pure) components, while keeping the interactions and side effects centralized.
The library approach
The library approach, as I call it, consists of two important rules to remember.
When you design components, always start with the assumption that your component can be functional. Functional components are much more lightweight and will see an improved performance in future React versions.
You only need a class when:
- You need internal state
- You need to use one of React’s lifecycle hooks
- You need to supply callbacks with additional parameters
The first two are only really needed for your smart components. The only exception is if you need to have some kind of small UI state in your component. The third point is also seen more often in dumb components— you often want to propagate some kind of information to the overlord smart component via a callback, thus a class is suitable in these cases.
Writing many stateless components will also greatly ease your debugging sessions. How many times did you have to debug the state of a component for being wrong, something wild coming as a prop, lifecycle hook doing wild stuff, or for not doing anything at all?
On the contrary, only two things can actually go wrong with functional components. You either supplied bad props to the component, or your component rendered the wrong way. Both cases can easily be avoided using a type checker of your choice and, of course, testing.
It should get clearer why I call this the library approach in this second rule. Since most of the time you’ll spend creating dumb components, it is a good idea to keep them only dependant on the properties they receive. This means no
inject and ideally no touching the context at all.
The thing is, no matter what state container you use, your components will always behave the same. Another benefit is that when using something like a storybook, you don’t have to mock your context every time you want to add a new component. The list goes further:
- Better modularity
- Easier documentation
- Global state independence
Let’s have ourselves an example, shall we?
A very simple, yet rigid component! Notice that the header will always be the same, as will the todo’s data source. One would have to mock two different contexts to test this component.
What can we do? Simply let smart components propagate the necessary data via props!
Now we don’t care if we use i18n or where the data comes from. We simply render the component as it is — no huge wrappers, no context mocking. Want to put it into a storybook for designers? Test it painlessly with Enzyme? Adjust header dynamically via props? Go ahead!
The library approach is literally writing your components as if they were meant to be used as a library — contextless, reusable, small, performant. You will eventually develop a set of nice and simple components for your team to use.
Mind your re-renders
In a previous section we discussed supplying callback functions with additional data. Many people tend to either use some kind of partial application (bind, Ramda, lodash…), or arrow functions, but this is a big antipattern! The same applies to creating new objects or arrays. They create a new reference every time, often causing unnecessary re-rendering.
Let’s have a look at a bad example first.
Notice that we create a new
onClick function and a new
geo object every render. If you used some kind of performance library it would eat you alive and ensure your government wiped away any details of your existence.
To fix the
onClick function, simply transform the component to a class and bind in the construction phase by one of the common ways.
geo object is a bit trickier to fix. If the component eating the prop is yours, just spread it flat to avoid any issues whatsoever — since shallow compare will be sufficient. However, if you must have it as an object, you will have to implement
shouldComponentUpdate yourself if it’s worth it.
Note the big highlight on the “if it’s worth it” part of the sentence. When an optimization like this is done in a very difficult or awkward way, simply ignore this specific case and move on, unless you absolutely must make something turbo fast. Measure first!
A cleansed version of the previous component:
Now, a re-render of
CountryIcon won’t be triggered unless some properties actually change.
If you really had to keep the
geo prop, you could implement an update on the
CountryIcon component yourself:
If you couldn’t adjust the
CountryIcon component (say it was an unoptimized library), there’s one more approach that’s worth mentioning:
This way, the
geo object is created at mount time, then re-created only when really necessary. This, in my opinion, is the best way of handling such situation in case you simply cannot change the component receiving non-primitive props (
CountryIcon in this case).
When it comes to performance, remember these rules:
- Make functions and static objects only during the construction phase
- Keep props flat, ideally comparable via
- Measure first, then optimize if it’s worth it
- Construct dynamic objects only when necessary,
componentWillReceivePropshelps a lot here
shouldComponentUpdateif you must optimize, and you cannot change your prop signature
Remove logic from components
I often encounter this when doing CRs. When you have a component that receives some properties, you often need to perform some operations in order to get the data you actually care about.
It may seem like an overhead, but trust me, it’s totally worth it in the long run. Keep your components like this:
Notice I explicitly wrote what kind of functions to place in a class component. This is where many people tend to put a lot of ballast (including me in the past, don’t worry).
Anyway, my point is, no computations. No if-else chains, all you care about in a component is the markup (and the side effects in smart components). Where to locate stuff like data selection, computations and such, then?
Task: create a component that displays todos based on a filter in the url.
We now know not to put the filtering directly on the component. While there’s no cookie-cutter answer on where it should take place, my big recommendation is to simply follow the conventions. Meet reselect:
Alright, we have ourselves a selector! A selector in this context is basically a memoized function that returns some data derived from its input. Check the documentation in case you’re not yet comfortable with the API.
Now, how do we actually use such selector? Since one of the most common use cases is to derive data from Redux’s state, it may be a good idea to place it into the
Pretty straightforward. Not only is our code neater, we can test it a lot easier, too. But selectors don’t stop there! We can also create ourselves a selector for
connect-less components, selecting only from props:
And this is how we use it:
That’s all there is to it! Keep in mind you don’t have to use reselect, you can simply make your own functions that take props and spit out filtered todos. Or use some other library — it’s up to you.
It’s not only about selecting derived data. There are many more use cases of separating logic from components:
- API calls
- API response mappers
- Validation functions
- Different browser-based side effects (setting cookies, local storage, … )
Let your components do what they are designed to do — spit out markup and handle user interactions. Place any logic in a separate file, where it can be tested a lot more easily, and allow for certain optimization techniques, such as memoization.
Static analysis is your friend
In a world of constantly growing single page applications written in a language that lets you do all kinds of crazy stuff, the need to statically check different kinds of code quality is higher than ever. Also, how satisfying is this?!
ESlint is the hot one now. With Airbnb having one of the most widely accepted set of rules, and more and more editors directly supporting error highlighting, we are getting to a spot where writing high quality code is totally painless.
There’s often a problem, though. With so many rules available, it’s hard to actually agree on what style should be accepted as the correct one. That’s why, in my humble opinion, the best approach is just follow the most widely accepted standard, which is the aforementioned Airbnb styleguide.
They have rules for React, import, ES6+ syntax, JSX … basically all you would ever want to use in this stack. It even checks for potential HTML issues!
Whether your team consists solely of you, or you work in a company of 100 front-end developers, having a unified, widely accepted code style goes a long way.
I don’t want to be biased here, so I mentioned both options. Types are a godsend. If you’ve ever programmed in a language with a good type system, you know how much they can help make sure your program works before you actually run the code.
There’s a common saying that if it compiles, it probably runs. Although Flow itself doesn’t involve any compilation, you can check for type errors ahead of time. This means a lot less runtime errors, debugging sessions and headaches!
One huge, although less frequently mentioned, advantage of static typing is that the code you write will actually have some structure. This especially involves getting rid of weird data mappings so typical for JS functions, however, totally unpredictable and impossible to type.
All kinds of common errors that are very hard to track will be caught in advance:
- Mistyped object property
- Mistyped string constant
- Accessing a non-existent nested property
- Functions returning unexpected results
When considering whether types are worth it for you, the answer is almost always a big “Yes”. Starting with types is a lot less painful than trying to incorporate them into an already huge codebase (that’s what I am currently doing, lol). The better the coverage, the better the results!
Now, when it comes to
propTypes vs. Flow or TypeScript, personally I’d just go with the static version.
propTypes are awesome if you currently don’t use any kind of static typing, however, in a well covered code, they become obsolete.
Static analysis is a very good way of keeping the codebase neat and bug-free. It also allows noobs to be productive a lot more quickly because the code style is standardized, and types make it safe and self-documenting.
I would not be able to extensively write about these, however, they are still very important: