DEV Community

Cover image for React: It's Not Rocket Science.
Prakhar Tandon
Prakhar Tandon

Posted on

React: It's Not Rocket Science.

React is essentially just JSX and Hooks, with hundreds of tiny concepts that you must know. I’ll recommend to check out the previous post as well. In the last one, Batman was learning, now he's helping Stark as well, that's React Community, my friends lol.

So here's Part 2 for you all. Let’s jump back in.

The useState Hook

Just for the sake of it, the useState hook is one of the most commonly used hooks in React. It allows you to add state to your functional components. The useState hook takes one argument, the initial state, and returns an array with two elements: the current state value and a function to update it.
(Why an array? Because we can name the constituents whatever we want using Array destructuring)

Okay, we all know about all this, how this hook works, triggers re-renders, and persists value across renders. But there is something to remember.

  • If the state value is unchanged or the new value is “same” as the previous one, it’ll not update the state and will not trigger re-render.

Now you might be wondering why “same” is in quotes. Here’s why

Object Comparison in JavaScript

In JavaScript, comparing two objects can sometimes lead to unexpected results. This is because when you compare objects, JavaScript compares their references, not their contents. So even if two objects have the same properties and values, they are still considered different if they reference different memory locations.

The Object.is method is used to compare the previous and new state value.

It compares objects by their reference, not their contents. So even with Object.is, two objects with identical contents are considered different if they are not the exact same object.

let obj1 = { org: "Dualite" };
let obj2 = { org: "Dualite" };

console.log(Object.is(obj1, obj2)); // This will output false
Enter fullscreen mode Exit fullscreen mode

meme

Below is similar example with useState.

import React, { useState } from 'react';

function MyComponent() {
  const [state, setState] = useState({ name: 'Prakhar' });

  const handleClick = () => {
    // This will trigger a re-render even if the new state is
    // structurally the same as the old state, but it's a new object reference.
    setState({ name: 'Prakhar' });
  }
// checkout the sandbox at the end of post

  return (
    <div>
      <p>{state.name}</p>
      <button onClick={handleClick}>Change Name</button>
    </div>
  );
};

export default MyComponent;

Enter fullscreen mode Exit fullscreen mode

But all this is fairly simple. The challenge comes in the design part, where you decide what component should have what states, local states or sending state as a prop, the nesting architecture, other props, etc. Because if not planned well, you’ll end up making dozens of states in your components.

Mostly what happens is we focus on the main business-logic problems and forget such small optimizations that can improve performance, that’s what happened with me.

I’ll share an example from work. At Dualite, we generate code for Figma designs and prototypes (with just a click, I’m so proud of this.) Okay so after the code is generated, we provide a few buttons to copy the code, as in the screenshot below.

Screenshot from Dualite figma plugin

When you click a button, the button text will just toggle to “Copied” for a few seconds then back to the original one.

So the problem was design, we had this massive “buttonTexts” state with all texts and we changed this state for any button click. Since this state was in the main “Output” component, the entire component re-renders twice when you just copy the code! (idk who came up with that sh*t design, definitely not me… lol)

// Output.tsx -> itself a large component
const [buttonTexts, setButtonTexts] = useState({
    button1: "Copy HTML",
    button2: "Copy CSS",
    button3: "Copy React",
    button4: "Copy Script"
})

// inside button's onClick
setButtonTexts({
    ...buttonTexts,
    button1: "Copied"
})

//then we revert it back to original one, using a setTimeOut
Enter fullscreen mode Exit fullscreen mode

You guys realize this! it’s something most of you will never notice, thanks to the notion “If it works don’t play with it…” but this was definitely a problem. So I came up with a solution, to transfer the “text” as a state local to each button, hence only that button re-renders on click. Like this→

// Button.tsx
const Button = ({initalText, finalText}) => {
        const [text, setText] = useState( initalText );
        // setTimeout and copy logic
        return <button>{text}</button>
}

// Output.tsx

<Button initialText="Copy React" finalText="Copied!" />
Enter fullscreen mode Exit fullscreen mode

Now, check out your latest React/Next projects and see how many times components are rendering. You can use console logs or "Profiler" from React Dev tools to check that out.

We'll continue the discussion over re-renders in the next part with stuff like memoizaton.

The useEffect hook

The object comparison problem from useState will also persist here since useEffect involves comparing dependency array values to trigger passed callback. Solution? restructure your code so that you have only primitives for JS to compare.

The useEffect hook is mainly used for performing side effects. Any operation that’ll technically make our component “impure”, like accessing browser APIs, DOM, etc.

Hence you might have called APIs using useEffect hook. Although that’s not a good practice anymore, instead use libraries like React Query or RTK Query for handling your APIs. They provide fantastic error handling and caching abilities out of the box.

As a bonus, let’s talk about our very own “console.log”. This cute little function helps us to log anything to the browser console. But this has some problems of its own.

First and foremost, it slows down your app a bit [check here]. And since it calls a browser API, this is a “Side Effect” and having a console.log inside your component makes it “impure” [refer to previous article], hence you shouldn’t have any console logs “directly” inside any React component, especially in production. Hence ideally these should reside inside useEffect blocks.

What we did at Dualite was we made a wrapper class around the browser console object, and when in production, the wrapper functions become empty. Here’s an example →

class Logger {
  static log(...params) {
    if (process.env.CURR_ENV !== 'production') {
      console.log(...params);
    }
  }

  static error(...params) {
    if (process.env.CURR_ENV !== 'production') {
      console.error(...params);
    }
  }

  //Add any other console method you need
}

export default Logger;

Enter fullscreen mode Exit fullscreen mode

Then you can create objects of this class and use them inside your components. This approach allows you to even add some custom functionality with your logs based on your use case.

The useReducer Hook

The useReducer hook is another built-in hook that allows you to manage complex state logic in your React components. It's essentially a built-in Redux at the component level, allowing you to manage the state using actions and reducers. This hook is particularly useful when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

Here’s a sandbox where you can play with state, re-renders, and useReducer. Do check it out.

https://stackblitz.com/edit/stackblitz-starters-lx34or?file=src%2FApp.js

Thanks, everyone! Stay in touch on Twitter @PrakharTandon29

Top comments (2)

Collapse
 
devgancode profile image
Ganesh Patil

Not readable due the font.

Collapse
 
prakhart111 profile image
Prakhar Tandon

Hey, can you elaborate the issue you're facing? Coz the font is coming fine for me?