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.
Info
With the introduction of the compiler in React 19, the optimisation strategies described here may well no longer be necessary as the compiler is designed to make these optimisations on your behalf.
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!
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.
Feature | Stack (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.
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.
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:
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.
_10const [items, setItems] = useState([1, 2, 3]);_10_10items.push(4);_10setItems(items);
By spreading the original array React detects the new reference and re-renders the component.
_10const [items, setItems] = useState([1, 2, 3]);_10setems([...items, 4];
Tip
React shallowly compares state. When we mutate an array or object directly, its reference doesn’t change, and React doesn’t detect the update. Instead, always return a new reference to ensure React recognises the change.
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.
_10const [user, setUser] = useState({ name: "Danny", age: 69 });_10_10user.age = 64;_10setUser(user);
Below React is able to detect the new reference and re-renders the component
_10const [user, setUser] = useState({ name: "Danny", age: 69 });_10_10setUser({ ...user, age: 64 });
This is illustrated in the following example:
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> ); }
Anonymous Functions vs. Memoised Functions with useCallback
#Passing anonymous functions as props can cause unnecessary re-renders in child components.
_14import { useEffect, useState } from "react";_14_14export 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.
_15import { useEffect, useState, useCallback } from "react";_15_15export 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.
Warning
Avoid overusing memoization as using it itself has a cost in memory and CPU.
_16const ExpensiveComponent = React.memo(({ data }) => {_16 console.log("ExpensiveComponent rendered");_16 return <div>{data}</div>;_16});_16_16const 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,
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.
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?

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.