I have been working with React for the past couple of years, so naturally, I am not really proud of the code that I wrote when I was just beginning with React, because now I know the mistakes I made which I wasn't aware of back then.
But fast-forwarding to today, I have learned quite a bit along the way through contributing to open source, watching/reading some interesting blogs and conference talks and viewing how other people write code.
Here are some Javascript tips that would've helped my past self and maybe you, in writing more efficient and maintainable React code -
1. Use conditional rendering effectively
As a React developer, you must've been in a situation where you only want to display a component when a certain condition from a prop or state is satisfied or render different components depending on the different values of the state.
For instance, if you have a component where you want to show a loading indicator when the request is being made and render the component with data when the request is successful, this is the way I like to do it -
const SomeComponent = ({ isLoading, data }) => {
if(isLoading) {
return <Loader/>
}
return (
<DataHandler>
.
.
</DataHandler>
);
}
But what if you want to render something inside JSX when a particular condition is satisfied in that case you can use the Logical AND operator (&&
) to render it -
const Button = ({ showHomeIcon, children, onClick }) => (
<button type="button" onClick={onClick}>
{showHomeIcon && <HomeIcon />}
{children}
</button>
);
Although a more useful scenario would be doing something like this, where you have an optional prop called icon which is a string and contains the name of the icon that can be used to render the icon component accordingly -
const Button = ({ icon, children, onClick }) => (
<button type="button" onClick={onClick}>
{/* Icon won't be rendered if the value of
icon prop is anything other than a string */}
{typeof icon === "string" && <Icon name={icon} />}
{children}
</button>
);
// Renders a button with a home icon
<Button icon="home" onClick={handleClick}>Home</Button>
// Renders a button without an icon
<Button onClick={handleClick}>About</Button>
So this solves the problem when you only have one component but what about when you have two or more than two components that you want to render based on some prop or state variable?
For two components ternary operator is my goto method, because of its simiplicity -
const App = props => {
const canViewWelcomeText = isUserAuthenticated(props);
return canViewWelcomeText ? (
<div>Hey, there! Welcome back. Its been a while.</div>
) : (
<div>You need to login to view this page</div>
);
};
And if you have quite a few components that need to be rendered from a condition, then switch case is probably the best one to go with -
const getCurrentComponent = currentTab => {
switch (currentTab) {
case 'profile':
return <Profile />;
case 'settings':
return <Settings />;
default:
return <Home />;
}
};
const Dashboard = props => {
const [currentTab, setTab] = React.useState('profile');
return (
<div className="dashboard">
<PrimaryTab currentTab={currentTab} setTab={setTab} />
{getCurrentComponent(currentTab)}
</div>
);
};
2. Avoid using truthy tests
If you are familiar with JavaScript then you might be aware of truthy and falsy values. So a truthy test is nothing but using this coercion ability of JavaScript in control flow statements like this
// ❌ Avoid adding checks like these
// for non boolean variables
if (somVar) {
doSomething();
}
This might look good at first if you want to avoid something like null
since it is a falsy value so the statement will work as expected. But the catch here is that this is prone to bugs that can be very difficult to track down. This is because the above statement would block the flow for not null
but also for all these falsy values of someVar
which we might want to avoid -
someVar = 0
someVar = ""
someVar = false
someVar = undefined
So what is the correct way for these checks?
The valid way is being as straightforward as possible for these checks to avoid any bugs from creeping in. For the above case it would be -
// ✅ Explictly check for the conditions you want
if (someVar !== null) {
doSomething();
}
This also applies when doing conditional rendering with the Logical and operator that we saw in the previous tip.
If the first operator is falsy then JavaScript returns that object. So in case of an expression like 0 && "javascript"
will return 0
and false && "javascript"
will return false
. This can bite you if you were doing something like this -
// ❌ This will end up rendering 0 as the text if
// the array is empty
{cats.length && <AllCats cats={cats} />}
// ✅ Use this instead because the result of the
// condition would be a boolean
{cats.length > 0 && <AllCats cats={cats} />}
3. Use optional chaining and nullish coalescing
When dealing with data in our apps we often need to deal with parts of data that call be null
or undefined
and provide default values.
Let's suppose we have an API that returns the details of a Pet in the following format -
// Endpoint - /api/pets/{id}
{
id: 42,
name: 'Ghost',
type: 'Mammal',
diet: 'Carnivore'
owner: {
first_name: 'Jon',
last_name: 'Snow',
family: {
name: 'Stark',
location: 'Winterfell'
}
}
}
So you could do something like this if you wanted the first name of the pet owner
const ownerName = pet.owner.first_name;
But like all things in the universe can't be perfect, our API doesn't guarantee that all the details would be available for any given pet and can be null
or undefined
.
In that case, the above line of code can result and the following error "Reference error cannot read property first_name
of null
" and crash your app if the owner is null
.
This is where optional chaining saves you. The optional chaining operator (?.
) allows you to read the property deep in the chain without having to validate whether the chain is valid, and instead of a reference error, it would return the same old undefined
.
So we could easily check for the owner name or even the owner family name without worrying about any errors, like this -
const ownerName = pet?.owner?.first_name;
const ownerFamily = pet?.owner?.family?.name;
Now, this would avoid errors but you still wouldn't want your users to show undefined
in case it is not available. This is where Nullish Coalescing comes in -
const ownerName = pet?.owner?.first_name ?? 'Unknown';
The Nullish Coalescing operator (??
) returns the right hand side operand when the left hand side is null
or undefined
and otherwise it returns the left hand side operand.
You might think here that the Logical Or operator (||
) would also have done the same thing. Well in that case I hope you haven't forgotten the truthy and falsy hell of JavaScript that we just covered. Since this operator would return the right hand side operand for all falsy values and can cause hard to debug errors as mentioned in the previous section.
4. Avoid premature optimization
Be really careful when you want to memoize something in React, because if not done properly it might lead to even worse performance.
I have often seen people prematurely optimizing everything they come across without considering the cost it comes with. For instance, using useCallback
in situations like this -
const MyForm = () => {
const [firstName, setFirstName] = React.useState('');
const handleSubmit = event => {
/**
* Ommitted for brevity
*/
};
// ❌ useCallback is unnecessary and
// can actually be worse for performance
const handleChange = React.useCallback(event => {
setFirstName(event.target.value);
}, []);
return (
<form onSubmit={handleSubmit}>
<input type="text" name="firstName" onChange={handleChange} />
<button type="submit" />
</form>
);
};
Now you might've heard that useCallback
is known to improve performance by memoizing the function and only updating it when the dependencies change. That is true but you need to understand that every optimization comes with a cost associated with it.
In the above case, you are doing more work by creating a useCallback
which in itself is running some logical expression checks, hence you're better off with just defining the inline function directly like this -
const handleChange = event => {
setFirstName(event.target.value);
};
The same things apply with React.memo
. If you have a component like this that accepts children props, then memoizing the component is basically useless if the children are not memoized -
const UselessMemoizedHeader = React.memo(({ children }) => <div>{children}</div>);
const SomeComponent = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<UselessMemoizedHeader>
<span>Header</span>
</UselessMemoizedHeader>
Count: {count}
<button
type="button"
onClick={() => setCount(currentCount => currentCount + 1)}
>
Increment count
</button>
</div>
);
};
In the above case, the UselessMemoizedHeader
component would re-render every time you increment the count even though you might think it is memoized.
But why? Since memo just does a shallow comparison of the current props and previous props, and because the children prop won't be refrentially equal you end up re-rendering the UselessMemoizedHeader
component every time the count changes.
Your code ends up being even worse off because of that unnecessary children prop comparison on every render.
So when do you actually need to memoize? Well I wrote another article that covers all the above things with when you should memoize in great detail.
5. Be vigilant with dependency arrays
The React hooks related to memoization(useCallback
and useMemo
) and the useEffect
hook take a second argument as an array usually known as dependency array.
In case of useEffect
the effect is re-run only when a shallow equality check of the dependency array is not equal to the previous values.
React.useEffect(() => {
/**
* Fetch data with new query
* and update the state
*/
}, [query]); // < The effect reruns only when the query changes
Similarly, the memoization hooks, are recomputed only when the values in their dependency array change
const someValue = React.useMemo(() =>
computationallyExpensiveCalculation(count),
[count]); // < someValue is recomputed only when count changes
So now that's clear. Can you find out why does the effect runs everytime the CatSearch component re-renders, even when the query, height and color props are essentially the same -
const CatSearch = ({ height, color, query, currentCat }) => {
const filters = {
height,
color,
};
React.useEffect(() => {
fetchCats(query, filters);
}, [query, filters]); // ❌ This effect will run on every render
return (
/**
* Ommited for brevity
*/
);
};
Well as we discussed in the last section, React just does a shallow comparison of the items in the dependency array and since the filter object gets created in every render it can never be referentially equal to the filter object in the previous render.
So the correct way to do this would be -
React.useEffect(() => {
fetchCats(query, { height, color });
}, [query, height, color]); // ✅ The effect will
// now run only when one of these props changes
The same applies for spreading the dependencies like this -
React.useEffect(() => {
/**
* Ommited for brevity
*/
}, [...filterArray, query]); // ❌ This effect
// would also run on every render
Also if you're more interested in how useEffect
works and how the dependency array affects the effect, then you should definitely check out A Complete Guide to useEffect by Dan Abramov.