← Back to all posts

React Like a Ninja

·Anmol Thukral

Have you ever seen the code of your peers and thought, "that's one way of doing it" or "I wish I could have thought of that"? And then tried to do the same but ended up overusing things and creating complicated structures that you didn't even understand but knew worked.

If that's what you went for, then my dear friend, what you have done is not ninja code but an example of bad code.

A good React code always follows the following principles to make your applications clean, performant and scalable.

1. Components in the Purest Capacity

A pure function is one that accepts some parameters and its result will always remain the same.

Similarly, per React's mental model, "All components should be pure functions of props and state," i.e., renders the same thing for the same set of props and state.

But why should we do that?

We already have an option to do things differently and it sounds easy so why shouldn't we do that? Because React works best with help of predictability. And predictability leads to easier debugging and reasoning.

So rule of thumb should be: No forced mutations, no random values and no direct DOM manipulation.

Here is an example:


let counter = 0;

function InternComponent() {
  counter += 1; // modifies a variable outside the component
  document.title = \`Count: \${counter}\`; // directly manipulates the DOM
  
  return 
Render count: {counter}
; }

Ninja move:


function NinjaComponent({ count }) {
  return 
Render count: {counter}
; }

2. Side Effects are for Sides and Effects

Anything that is not rendering is a side effect, be it updating a state or having logic to create or pull data from an API or executing some business logic.

React provides provisions to run these logics via useEffect or calculations via useMemo. And we should stick to using them.

Rule of thumb: calculate what needs to be rendered and cumulate what should be or might get rendered.


// Preventing side effects 
useEffect(() => {
  myAPIcall(data)
}, [data]); // dependencies

3. Prefer Composition over Configuration

React frontend is a composition of smaller components and let's stick to this logic entirely. We have the ability to split the code and use them as per our choice, we should capitalize on that the most. Stick to smaller components and larger component trees for easy debugging, better performance and faster recalculation of renders.

Instead of:


<SuperTable configurable={true} fancyMode={false} ... />

Do:


<BasicTable>
  <SortableHeader />
  <PaginatedBody />
</BasicTable>

Take components as smaller lego blocks which can be joined to build one big application. This makes your application scalable and easily testable.

4. Keep your States Close and Renders Closer

Creating unnecessary dependencies leads to extra renders. Only keep the states that are actually required at the level to be lifted up, no higher.

Your dependency and composition chart should be around the components that use the state and the ones that update it and nothing more.

Also remember App level states or global states are a scalability nightmare.

As a ninja way, states that keep on changing should remain in component and component only. Whatever needs to be shared should only be shared. That's the rule of need to know basis.

5. Avoid Premature Memoization (Measure First)

Memoization patterns like useMemo, useCallback, React.memo are not cheap operations. They take away memory and add an extra layer of complexities over the normal logic. Thus, using them right can only help in optimization not the other way around.

Again as rule of thumb, write your code first (cleaner and sleeker if possible). Profile all the calculations with React profiler, check for renders then see what can be memorized and what shouldn't.

6. Components Should be Adaptable Not Reusable

Ironically, this will be a controversial statement; but as far as experience goes, the best and most components are the ones who get built over the iteration not in the first time. With more and more possibilities from React ecosystem, there are more than one ways of making a component reusable. You can use parent components, configure your components via props, use HOCs etc.

The config and usage comes based upon the requirement. But it should not disobey the idea number one of component purity. Thus to grow into that mindset the minimal and optimal components with right usability and minimality should be built.

For example let's just take a button component like:


function Button({ variant,buttonText, clickHandler}){
  if(variant==='primary') return <button className='primary' onClick={clickHandler}>{buttonText}</button>
  if(variant==='secondary') return <button className='secondary' onClick={clickHandler}>{buttonText}</button>
  return <button className='tertitary' onClick={clickHandler}>{buttonText}</button>
}

This can be an optimal component as


function Button ({buttonText, clickHandler,className}) {
  return <button className={className} onClick={clickHandler}>{buttonText}</button>
}

And this can be furthermore used as


function PrimaryButton (props){
  return <Button className='primary' {...props} />
}

7. UI State ≠ Server State ≠ Global State

Understand what they are and treat them differently:

  • UI state: local and transient, keeps on changing, usually held at client level in order to keep track of your components e.g. isModalOpen, isDropdownOpen etc.
  • Server state: cached and async fetched from the API calls or server operations, usually the data powering your components. Example should be data you receive in your server components from async task. Need to understand what needs to be cached and what should be refetched or reevaluated based upon the need. This preserves and differentiates your component from SSR, SSG, ISR etc.
  • Global app state: Things your applications needs all the time and probably everywhere. Like your auth or theme values, design system tokens etc.

Mixing them leads to drastic downgrades in performance followed by debugging deadends.

8. Applications are Prone to Errors but Users Aren't

Make your errors friends; don't let your user experience break even if your code breaks. React offers error boundaries, wrap your components around the same to keep user aware of the breakage and what should be the next step for user in activity cycle so that it doesn't break.


<ErrorBoundary fallback={<SadCatError />}>
  <MyComponent />
</ErrorBoundary>