Now Reading:

Mastering Data Types in JavaScript For Optimised React Re-rendering

One of the sneakiest culprits behind unexpected re-renders in React is how JavaScript handles data types. Specifically, how primitive and reference types behave when used as props, state, or dependencies in hooks like useEffect and useMemo.

In this article, we’ll break down JavaScript’s data types and explore how they influence React’s rendering behaviour.

However; they’re still relevant. It’s still really important to understand how these concepts work and if you’re using React 18 or earlier, these concepts will really help you out!

What are Data Types?

#

JavaScript categorises data into primitive and reference types, affecting how values are stored, compared, and passed around in your code.

Essentially, the difference between the two is determined by where they are stored in the computers RAM which fall into in two distinct areas.

Stack → The stack manages function calls and local variables automatically in sequential order this is where primitives are stored.

Heap → The heap allows for dynamic memory allocation for data with extended lifespans but requires manual management. This is used for storing reference types.

FeatureStack (Primitives)Heap (Reference Types)
Speed🔥 Fast (direct access)🐢 Slower (indirect lookup)
Memory Size✅ Small, fixed size⚠️ Can grow dynamically
Data Storage📌 Stored by value🔗 Stored by reference
Copy Behavior📝 Creates a new copy🔄 Shares reference

Primitive Data Types

#

Primitive values are simplest forms of data in JavaScript. They are immutable and compared by value, these types include:

  • String
  • Number
  • Boolean
  • Null
  • Undefined
  • Symbol
  • BigInt

Immutability means that once a primitive value is created, it cannot be changed.

Any operation that modifies a primitive value will actually create a brand new value.

This means that when you assign a primitive to a variable, JavaScript stores the value directly in memory.

Playground
let a: number = 10; 


let b: number = a; 


a = 20;


console.log("a:", a); 


console.log("b:", b);

In the example above, b is assigned the value of a, not a reference to it. Since primitive types are stored by value, b gets a separate copy of a's value. When a is updated to 20, it does not affect b, which remains 10.

This behaviour ensures that primitive values are independent of each other, making them predictable when managing data in JavaScript.

Reference Data Types

#

Reference types are more complex datatypes and their values are stored by reference. These include:

  • Object
  • Array
  • Function

When you assign an object to a variable, you’re actually storing a reference to memory address rather than the value itself.

Playground
let petRabbit = { name: "Fluffy" };


let petCat = { name: "Fluffy" };


console.log("petRabbit === petCat:", petCat === petRabbit);


petCat = petRabbit;


petCat.name = "Fluffykins";


console.log("Cat name:", petCat.name);


console.log("Rabbit name:", petCat.name)


console.log("petRabbit === petCat:", petCat === petRabbit);

Even though petCat and petRabbit have identical contents, JavaScript treats them as separate entities because they occupy different locations in memory.

When checking equality, JavaScript handles reference types differently from primitives. Instead of comparing their actual values, it compares their memory references. If two objects share the same memory address, they are considered equal or true. However, if they occupy different memory locations, they are treated as distinct or false, even if their contents appear identical.

If we assign petRabbit to petCat and change the name of petCat this also changes the name of petRabbit. Now when we do a equality check, this time the result is true as both variables have been assigned to the same memory address.

How JavaScript Data Types Affect React Re-rendering

#

React re-renders a component whenever it’s state or props change. But how React determines whether something has changed depends on how JavaScript compares values.

Primitive values are compared by value, so React detects changes easily.

Reference values are compare by reference, even if the content remains the same, React will consider them different if their reference changes.

React determines if the props have changed simply by comparing the two versions current and incoming using an equality check, if they aren’t equal, then React will assume that they have changed.

How Reference Types Trigger Re-renders

In React, the way in which reference types are handled directly impacts rendering behaviour.

Mutating a reference type (e.g., modifying an object or array in place) won't trigger a re-render because the reference remains unchanged. This can make the UI appear "stuck" when an update is expected.

Creating a new reference without actual changes causes unnecessary re-renders, leading to performance issues, especially in complex UIs.

Let's look at a simple React component that renders a child component with an object prop:

Playground
import React, { useState } from "react";


import "./styles.css";


import Container from "./Container";


export default function App() {


const [count, setCount] = useState(0);


const style = { borderColor: count > 2 ? "red" : "blue" }; // New object every render


return (


<Container style={style}>


<div className="card">


<h2>Responsive Card</h2>


<p>This card adjusts based on its container size.</p>


</div>


<button onClick={() => setCount(count + 1)}>Click {count}</button>


</Container>


);


}

Mutating Reference Values vs. Replacing Them Immutably

#

Consider a scenario where you have an array in state and need to update it.

In this example React will NOT re-render because the reference remains the same.


_10
const [items, setItems] = useState([1, 2, 3]);
_10
_10
items.push(4);
_10
setItems(items);

By spreading the original array React detects the new reference and re-renders the component.


_10
const [items, setItems] = useState([1, 2, 3]);
_10
setems([...items, 4];

Objects behave in a similar way. Take the following example. Because we are Mutating the object directly React won't detect a change, so no re-render occurs.


_10
const [user, setUser] = useState({ name: "Danny", age: 69 });
_10
_10
user.age = 64;
_10
setUser(user);

Below React is able to detect the new reference and re-renders the component


_10
const [user, setUser] = useState({ name: "Danny", age: 69 });
_10
_10
setUser({ ...user, age: 64 });

This is illustrated in the following example:

Playground
import React, { useState } from "react";


import "./styles.css";


export default function App() {


const [items, setItems] = useState([1, 2, 3]);


const addItemIncorrectly = () => {


items.push(4);


setItems(items); 


};


const addItemCorrectly = () => {


setItems([...items, 4]); 


};


return (


<div className="app">


<h2>Array Mutation vs. Immutable Update</h2>


<p>{JSON.stringify(items)}</p>


<button onClick={addItemIncorrectly}>Mutate Array (No Re-render)</button>


<button onClick={addItemCorrectly}>Immutable Update (Re-renders)</button>


</div>


);


}

Stay ahead of the pack 🐶

Join the newsletter to get the latest articles and expert advice directly to your inbox.
Totally free, no spam and you can unsubscribe anytime you want!

  • Expert Tips
  • No Spam
  • Latest Updates

I'll never share any of your information with a third party. That's a promise.

Anonymous Functions vs. Memoised Functions with useCallback

#

Passing anonymous functions as props can cause unnecessary re-renders in child components.


_14
import { useEffect, useState } from "react";
_14
_14
export default function App() {
_14
const [count, setCount] = useState(0);
_14
_14
const fetchData = () => {
_14
console.log("Fetching data...");
_14
};
_14
_14
useEffect(() => {
_14
fetchData();
_14
}, [fetchData]); // ⚠️ Effect runs on every render!
_14
_14
return <button onClick={() => setCount(count + 1)}>Click {count}</button>;

In the above example fetchData is recreated on each render. Since fetchData is a new function reference each time, React sees it as a dependency change and re-runs the effect unnecessarily.


_15
import { useEffect, useState, useCallback } from "react";
_15
_15
export default function App() {
_15
const [count, setCount] = useState(0);
_15
_15
const fetchData = useCallback(() => {
_15
console.log("Fetching data...");
_15
}, []); // Function reference remains the same
_15
_15
useEffect(() => {
_15
fetchData();
_15
}, [fetchData]); // Effect only runs once
_15
_15
return <button onClick={() => setCount(count + 1)}>Click {count}</button>;
_15
}

Using React.memo to Control Unnecessary Re-renders

#

Sometimes, a parent component re-renders, but a child component doesn’t need to. Here the child in React.memo can prevent unnecessary updates.


_16
const ExpensiveComponent = React.memo(({ data }) => {
_16
console.log("ExpensiveComponent rendered");
_16
return <div>{data}</div>;
_16
});
_16
_16
const ParentComponent = () => {
_16
const [count, setCount] = useState(0);
_16
const [data] = useState("Static Data");
_16
_16
return (
_16
<div>
_16
<button onClick={() => setCount(count + 1)}>Increment</button>
_16
<ExpensiveComponent data={data} />
_16
</div>
_16
);
_16
};

In the above example, ExpensiveComponent only re-renders when its data prop changes. Clicking the button won’t trigger unnecessary renders, improving performance.

Conclusion

#

Understanding how reference types behave in dependency arrays is crucial for writing efficient React applications. By leveraging useMemo, useCallback, and React.memo improve overall performance and keep your user experience silky smooth.

Halpy coding,

DANNY

Quiz Time!

#

Reinforce Your Learning

One of the best ways to reinforce what your learning is to test yourself to solidify the knowlege in your memory.
Complete this 10 question quiz to see how much you remember.

1) Why does `useEffect` re-run when an object is used as a dependency, even if its values have not changed?
file under:
Javascript

Thanks alot for your feedback!

The insights you share really help me with improving the quality of the content here.

If there's anything you would like to add, please send a message to:

[email protected]

Was this article this helpful?

D is for danny

About the author

Danny Engineering

A software engineer with a strong belief in human-centric design and driven by a deep empathy for users. Combining the latest technology with human values to build a better, more connected world.

Gobacktothetop

Made with 🥰 in 🏴󠁧󠁢󠁥󠁮󠁧󠁿

©2025 All rights reserved.