React being a highly popular UI library, over the years have matured beautifully. React’s flexibility is its greatest asset but if you do not know how to harness that flexibility, it can be troublesome.
React is by far the most popular front-end framework/library (and continues to grow faster). In addition, React is more loved and “wanted” than most front-end frameworks (although it is more used: satisfaction tends to decrease with popularity).
– Front-end frameworks popularity (React, Vue, Angular and Svelte)
Yes, React is unopinionated, but provides a number of ways to solve the same problem. Things become scary when you go down the route of searching the “perfect way” to execute something. Experienced React developers can attest to the fact that there is no “prefect way” — it is the right way for the situation in hand.
Over the years a lot more newer and robust (so they say) have come to light but React — is still the king!
If you want to be a web developer in 2022, React is the go-to UI library to learn because a lot of other things are also being cooked over React — like NextJS. In this blog post, I’ll try to cover the things I avoid when I am working with React.
Stop Over-Engineering Things
In order to make the “perfect” code that exist, you tend to add good (but not required) libraries to your code. As you proceed further, you tend to over-optimise your code, which was not required in the first place.
Whenever you are starting a project or a component, try not to get stuck in organising your code. Try to write code that works and then try to find “patterns” to break your code into utility functions/classes and re-usable components. This helps when there are no set patterns in your codebase or you are creating it for the very first time.
This also results in an overly large component which is difficult to understand. Once you feel that the component is getting out of hands and it is taking more time to understand the code than to write the code, start breaking your code into one or more components.
One of the VS Code extensions that I am a fan of is “glean”. It (among other things)
- Allows extracting JSX into new component
- Allows converting Class Components to Functional Components and vice-verse
- Allows wrapping JSX with conditional
- Allows renaming state variables and their setters simultaneously.
- Allows wrapping code with
useMemo
,useCallback
oruseEffect
Unnecessary Re-Rendring Of Child Component
Consider this Parent-Child relationship between components:
// Parent
export default function Parent() {
const handleClick = () => {
console.log("clicked");
};
// child
const Child = () => {
return (
<div>
{/* not required to pass props to <Child> component */}
<button onClick={handleClick}>Child</button>
</div>
);
};
return (
<div className="App">
{/* Rendering Child Component */}
<Child />
</div>
);
}
Here, we are using the handleClick
directly in the Child
component. We do not have to explicitly pass the function. But this is an anti-pattern because now everytime the Parent
component renders, the Child
component also gets re-defined (in memory).
A better way to do would be to define the Child
component separately and pass the function.
export default function Parent() {
const handleClick = () => {
console.log("clicked");
};
return (
<div className="App">
{/* Rendering Child Component */}
<Child onClick={handleClick}/>
</div>
);
}
// the child component is separate receiving the function as prop
const Child = ({onClick}) => {
return (
<div>
{/* not required to pass props to <Child> component */}
<button onClick={onClick}>Child</button>
</div>
);
};
When the Child
component is small, it hardly deteriorates the performance, but when the component is a complex component, it can have severe performance issues. This is because of the memory allocation and re-allocation to the Child
component every-time the Parent
is re-defined.
If the Child
is defined outside, memory allocation of that component happens only once and on re-render of the Parent
the Child
component is only “executed” and not re-defined at a different address in memory.
Re-Running The Expensive Operations In React
Once you understand the core of React and its functioning, you start looking at things (a.k.a your code) in a completely different light.
One such thing is understanding the state changes and what happens inside a component when state change happens.
Consider this functional component below with two states. One of the states also goes through a “expensive” operation/computations.
export default function App() {
const [value, setValue] = useState(true);
const [secondValue, setSecondValue] = useState(false);
// someExpensiveOperation will run everytime "any" state changes
// someExpensiveOperation will execute even if "secondValue" changes
const computedValue = someExpensiveOperation(value);
function someExpensiveOperation(data) {
// some expensive operation with data
console.log("someExpensiveOperation");
return data;
}
function buttonClickHandler() {
setSecondValue(true);
}
return (
<div className="App">
<button onClick={buttonClickHandler}>Click</button>
</div>
);
}
The first time the app loads, someExpensiveOperation
is “consoled” as the function is defined and called. The next time it is again consoled when buttonClickHandler
is clicked which updates the secondValue
. Surprise, Surprise!
The click of a button does not even touch value
‘s state, but still the “expensive” operation is exectued — hence huge performance hit!
Memoization to the rescue!
A great introduction to Memoization is given by What is Memoization? How and When to Memoize in JavaScript and React
Using the useMemo
hook provided by the React universe will help remember the execution of someExpensiveOperation
and the expensive operation will ONLY run again when value
changes.
export default function App() {
const [value, setValue] = useState(true);
const [secondValue, setSecondValue] = useState(false);
// someExpensiveOperation will execute ONLY IF "value" changes
const computedValue = useMemo(() => someExpensiveOperation(value), [value]);
function someExpensiveOperation(data) {
// some expensive operation with data
console.log("someExpensiveOperation");
return data;
}
function buttonClickHandler() {
setSecondValue(true);
}
return (
<div className="App">
<button onClick={buttonClickHandler}>Click</button>
</div>
);
}
Avoiding the DIV soup
Adding a div
everywhere makes no sense. Noob devs (just like me) add <div>
tag to wrap the JSX.
return (
// This div is not required
<div>
<button onClick={buttonClickHandler}>Click</button>
<p>Lorem Ipsum....</p>
</div>
);
Unnecessary div
can cause styling and accessibility issues. To avoid this, we can use Fragments in React. You can read more about it here — How to Write Cleaner React code.
return (
// This div is not required
<React.Fragment>
<button onClick={buttonClickHandler}>Click</button>
<p>Lorem Ipsum....</p>
</React.Fragment>
);
or use the shorthand for React.Fragments
return (
// This div is not required
<>
<button onClick={buttonClickHandler}>Click</button>
<p>Lorem Ipsum....</p>
</>
);
Rules For Exporting
Try not to land in this position.
export function Main1() {
}
export function Main2() {
}
Following a thumb rule of having one export per file. Having a clean folder structure is a bullet-proof thing which makes these rules a breeze to follow.
Another thing that I would like to expose you is the concept of “Barrel Files”.
Size Matters!
If you are blatantly adding libraries and modules as your app grows, you’ll end up with a pretty heavy app. The bundle size increases not only with the types of modules and libraries you are using but also the way you are using them.
As a result, the app can negatively impact users by decreasing the page load speed as the browser has to download a large JS bundle.
What you need is code-splitting. If you are using Create-React-App (CRA) or NextJS, it already supports code-splitting and the build script for production is also fine-tuned to ensure tree-shaking.
Code-splitting your app can help you “lazy-load” just the things that are currently needed by the user, which can dramatically improve the performance of your app. While you haven’t reduced the overall amount of code in your app, you’ve avoided loading code that the user may never need, and reduced the amount of code needed during the initial load.
– React Docs
For React, you can make use of lazy
(a new and experimental feature) to load your modules only when they are required by the user.
Two major ways to code-split:
- Dynamic
import
- React.lazy
Example for React.lazy
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
);
}
Prop-Drilling And Correct Usage Of Context In React
Mastering the data flow within parent-child relationship in React is one of the trickiest thing to do. It seems easy on the surface but is far from easy. Consider the example of prop-drilling:
Prop drilling is a situation where data is passed from one component through multiple interdependent components until you get to the component where the data is needed.
– How to Avoid Prop Drilling with the React Context API
Example of prop-drilling
export function Parent() {
const count = 10;
return <Child count={count}/>
}
export function Child({count}) {
return <GrandChild count={count} />
}
// For GrandChild to use the "count" prop, it has to be passed via Child
export function GrandChild({count}) {
return (<p>{count}</p>)
}
The above example shows very simple prop-drilling across 3 components. Imagine this same thing with a large number of components.
I am in no ways saying that “prop-drilling” is bad and should be avoided at all costs — NO. I am trying to state here that every fight needs a suitable weapon!
If your app is tiny and has no plans of scaling, go ahead with prop-drilling and that’s it. But if you are trying to build an enterprise-level application, then avoid prop-drilling.
There are two other ways to share data between parent and child, and the selection of way depends on the size of the app and the functionality you are trying to achive:
- For medium sized modules/apps — Very minimal state management libraries like Zustand, Jotai, and Recoil are easy, approachable, and lightweight ways to do this the ‘correct’ way.
- For large sized modules/apps — The tried and tested (and super-maintained) libraries like Redux
- Context in React
One of the most prominent ways to pass data between parent-child components is via Context. Using Context API is relatively easy but there is a small caveat that one needs to take care of!
import React, { createContext } from "react";
// EXAMPLE OF A CONTEXT
const AuthContext = createContext();
// PROVIDER CAN BE USED TO EXPOSE THE CONTEXT
const AuthProvider = ({ children }) => {
return (
<AuthContext.Provider
value={{
isUserLoggedIn: true
}}
>
{children}
</AuthContext.Provider>
);
};
// HOOK WHICH EXPOSES THE CONTEXT
const withAuthState = (Child) => (props) => {
return (
<AuthContext.Consumer>
{(context) => <Child {...props} {...context} />}
</AuthContext.Consumer>
);
};
export { AuthProvider, withAuthState, AuthContext };
Please go through example here to understand context deeply!
Any item that consumes the context will always “re-render” when the context value changes, regardless of if that component actually uses that value or not.
A very common pitfall is to use Context as a sort of ‘big store’, where you’ve got a ton of values in the context that are consumed by components that only need one or two of said values. This means that every time the Context component updates, every child component that relies on that Context will also re-render. This is a huge performance hit in a lot of large applications.
To rectify this, keep Context to items that will change extremely rarely — themes, authentication states, etc — and never use them for data that changes somewhat frequently.
Curry It!
Stop polluting your JSX with arrow functions — use “Curry” instead. You must have had functions in your JSX that takes an “event” as the first argument and one or more other values as secondary arguments. Therefore, you’ll end up something like:
function eventHandler(event, data) {
console.log(data);
}
return (
<div>
{/* aim is to remove the arrow function */}
<button onClick={(event) => eventHandler(event, 'First Name')}>Click</button>
</div>
);
To avoid having a callback with an event
passed just to take care of the first argument, we can create a curried function. A curried function is nothing but a function that returns another function.
The outer function takes care of our custom arguments while the inner function takes the event
that is passed by default. This eliminates the need to define a arrow function.
// curried function
function skipEvent(data) {
return (event) => {
return console.log(event, data);
};
}
return (
// This div is not required
<div>
{/* no more arrow function */}
<button onClick={skipEvent("First Name")}>Click</button>
</div>
);
Thanks for reading ❤
If this blog was able to bring value, please follow me on Medium! Your support keeps me driven!
If you enjoy reading stories like these and want to support me as a writer, consider signing up to become a Medium member. It’s $5 a month, giving you unlimited access to stories on Medium. If you sign up using my link, I’ll earn a small commission.
Want to connect?
Follow me on Twitter and LinkedIn or reach out in the comments below!
My name is Aditya. I am a Senior Software Engineer. I blog about web development.