There are plenty of articles and blogs that contain useful information on how to do things the right way. Therefore I’ve decided to take things the opposite way and look for how not to do things!
This article will state the bad practices and combine them into an enjoyable read.
While a ReactJS state created with useState or useReducer is useful, not everything should be placed within it. A lot of new developers struggle with this very concept. They do not know when to put something in the state and when not to.
An example would be storing data in the state which should have been derived from the state. Let’s say you have a state that represents a filled shopping cart on a webshop. The bad practice would be setting the total price inside the state too. One can simply compute the value from the state already.
Simple computational tasks or variables exist for that particular reason. The usual idea is to store as little data as possible in your state. Before you place data in the state, ask yourself if you can get the needed data from the other stored variables or state.
I just had to put this eternal React developer’s debate in here. Developers ask and say stuff like: “Should I use Redux or should I use Context?”, “Just use Context instead of Redux” or “Is Context a good replacement for Redux?”
There are many tools and mechanisms that partially do what Redux does. This in short explains the questions and statements mentioned above.
Let’s try to settle this debate once and for all.
Redux & Context
Many developers tend to think that Context by itself is a state management system. It is not! Context is a dependency injection mechanism.
Inside it you can put anything your heart desires, it can become a state management system if you implement it that way. One has to use useState and/or useReducer hook to manage the state inside it. That way, you are deciding where the state lives and you are handling how to update it and where exactly you wish to use it.
Context was made exactly to avoid passing data through many layers of components. If you only need to tackle this issue, then just use Context.
Redux & caching
Most applications need a cache of some sort for server state these days.
If we stick to the REST API’s there are a couple of libraries that do the caching for you. For example React Query or Vercel’s swr both do a good job for REST API.
If we use a more modern approach with GraphQL, caching is really easy with Apollo Client.
If caching is the only necessity in your app, you do not need Redux in your app at that point.
What is Redux used for then?
Redux is a generic state management tool. It has a lot of use-cases simultaneously. The most noticeable ones are: Caching state, UI state, complex data management on client, middlewares, etc.
In the end, it all depends on what specific problem is the app you are building trying to solve. Usually, you will only need the partial Redux features (global state management, caching).
This is severely bad because of a multitude of reasons:
- The code becomes very coupled. The inner components become dependant on the scope of the parent’s component.
- Inner components are almost non-reusable. You cannot export the inner components, you can only pass them as props further down the scope, which is not ideal.
- Performance. On each parent’s component’s render, the declaration function for the inner component will be re-created. To explain this further, inner component’s lifecycle methods will be called each render cycle. Along with the performance issues, the previous state will be lost as well.
Keep the components in their respective files to avoid this issue.
(in some cases)
Keep in mind that using the initial state for generic components such as counter component from the official React docs is perfectly fine. In a more detailed manner, this means setting props to state in order to initialize the state of a component with a non-reactive prop.
Outside the provided example, the initial react state should not be set to a value of a reactive prop. Why? Well because that state will not get changed unless you call the state setter, a setState function. If the props from the upper level get changed, the component will get the changed props, however, the state will stay the same as the initial prop value.
This issue destroys the single source of truth concept used in components. It is a bad practice and it should be avoided.
You render multiple items in React with array.map method. Keys must be unique so that React can handle proper tracking of that element or component. If you were to use the index as a key, that key can be a duplicate in some cases, which should be avoided.
Imagine having an array of items that you are going to render via .map and use the index as keys. Furthermore, imagine adding to the middle or removing an item from the middle of the array. Key will end up being the same as before, and React will assume it is the same identical element/component as before.
This could lead to undesired effects and should be avoided.
Use-cases of spread operator are great. It helps us reduce the code and manage it in a more clear way if used properly. Spread operators are nice when declaring reusable components or creating new data objects that reuse data and even when passing arguments into a function.
However, a lot of times, developers make a mistake of using a spread operator on props and setting wrong or undesired props on a component. This can result in the following error showing up in the console:
Not using useEffect, useMemo & useCallback dependencies
The stated React hooks introduce a concept of dependencies. This is just an array of items which, when changed, will cause the hook to update. Managing the dependencies can be a bit tricky if you haven’t done such a thing a couple of times. Dependencies array should consist of items that reflect the hooks and should not be crowded with a great number of those items.
ESLint static analysis has a rule that can help us use dependencies in those hooks.
The dependencies array can only be empty if you intend to use useEffect once when the component mounts.
Doing optimizations is usually a good thing, but it should not be done for every little tiny thing. To see the benefits from memoization, it is required to use hooks like useMemo or useCallback and even PureComponents. Developers need to be very focused and implement memoization with proper care because otherwise, it can break memoization one by one.
The following image says a thousand words:
Badly declaring TypeScript types
Most of us have grown to love TypeScript and cannot develop in JS without it anymore. Furthermore, most of us know about keywords known as any, never and unknown.
Unknown represents a set of all possible values, any value can be assigned to a variable of such type. It is a type-safe counterpart of any
Never represents an empty set, which means no value can be assigned to a such typed variable.
Those should be avoided most of the time. This can’t be stressed enough. Developers tend to be frustrated at TypeScript and then just write one of these keywords to get it off their backs. This is a bad practice and should be avoided.
There is a place for using these keywords, but it should be done scarcely:
- Use never in positions where there will not or should not be a value.
- Use unknown where there will be a value, but it might have any type.
- Use any if you really need an unsafe escape hatch.
There are many bad patterns we came across today and also how to avoid them and use proper patterns instead. If you learn to avoid these bad patterns, your life as a coder will be much easier and you will avoid many bugs and potential refactors.
Thank you so much for reading!
We’re available for partnerships and open for new projects.
If you have an idea you’d like to discuss, share it with our team!