Author Watermark

Chapter 5: Spread & Rest Operators: Why is it foundational requirement for React Native Developers

Prerequisites & Mental Check

πŸ“Ί Video Explanation

Watch the full course with certification on : Udemy


In JavaScript and React Native, few syntax features are used as frequentlyβ€”or misunderstood as fundamentallyβ€”as the three dots (...).

Depending strictly on syntactic context, the three dots perform two diametrically opposed operations: packing individual elements into a single collection (Rest), or unpacking structured collections into individual elements (Spread).

Mastering both is not merely a matter of clean code; it is a foundational requirement for mastering predictable state architecture, writing flexible component wrappers, and preventing silent rendering bugs in mobile runtimes.


1. Syntax Duality: Same Symbol, Two Opposing Operations

The JavaScript engine determines how to evaluate ... based entirely on where it appears in the Abstract Syntax Tree (AST):

TEXT
Target Position: Receiving end of data(Parameters, Destructuring)  ──►  REST(Packs values in)
Source Position: Supplying end of data(Literals, Function calls)   ──►  SPREAD(Expands values out)
Context Operator Name Operation Structural Transition
Destructuring Pattern Rest Collects remaining items a, b, c ──► [...rest]
Function Signatures Rest Collects unassigned arguments (x, y, ...args) ──► args = []
Object / Array Literals Spread Unpacks items into a new container [...arr] ──► el1, el2, el3
Function Invocations Spread Passes elements as positional arguments fn(...arr) ──► fn(a, b, c)

2. Deep-Dive: How React Reconciliation & Reference Equality Work

Before mastering how to update state, you must understand why React Native requires brand-new object references to update the UI on mobile devices.

2.1 The Reconciliation Engine & State Diffing

In React Native, your component tree represents a hierarchy of Native Views (e.g., RCTView, RCTTextView on iOS or ReactViewGroup on Android). When you call a state updater function (setState or setWorkout), React must decide: Did the application state actually change, and do I need to re-render this component and update the screen?

To keep mobile apps running at a smooth 60 or 120 FPS, React never performs deep, recursive checks on your nested state objects during state updates. Checking every nested key and sub-array on every user tap would cause massive performance lag ($O(N)$ tree-walking overhead).

Instead, React uses shallow reference equality via the ECMAScript Object.is() algorithm:

$$\text{Trigger UI Re-render?} \iff \text{Object.is}(\text{prevState}, \text{newState}) === \text{false}$$

2.2 Strict Equality (===) vs. Object.is()

React uses Object.is() rather than standard triple-equals (===) because Object.is() correctly handles JavaScript's two notorious mathematical edge cases:

  1. NaN === NaN is false, but Object.is(NaN, NaN) is true. (Setting state from NaN to NaN will not cause an accidental loop).
  2. +0 === -0 is true, but Object.is(+0, -0) is false.

For all objects and arrays, however, Object.is(A, B) behaves identically to strict reference equality (A === B): it checks whether both operands point to the exact same memory address in the Heap.


3. The Spread Operator (...): Expanding Iterables & Objects

The spread operator takes an existing array, iterable, or object literal, iterates through its elements or enumerable properties, and projects them into a brand-new container allocated at a new heap address.

3.1 Spreading Arrays (Cloning & Concatenation)

When applied to an array, the engine consumes the array's built-in iterator ([Symbol.iterator]) to extract elements in sequential order.

JS
const morning = ["stretch", "hydrate"];
const evening = ["walk", "meditate"];

// Cloning & Interspersing elements seamlessly into a new Heap slot
const fullDay = [...morning, "lunch", ...evening];
// Result: ["stretch", "hydrate", "lunch", "walk", "meditate"]

3.2 Spreading Objects (Merging & Overriding)

When applied to object literals, spread copies own enumerable key-value pairs into a new object. If the same key appears multiple times, the last assignment in the evaluation sequence overwrites any earlier assignment.

JS
const defaults = { theme: "dark", fontSize: 16, notifications: true };
const userPrefs = { fontSize: 20, language: "en" };

const finalConfig = { ...defaults, ...userPrefs };
// Result: { theme: "dark", fontSize: 20, notifications: true, language: "en" }

3.3 Spreading Positional Function Arguments

Array spread can project array items directly into positional parameters:

JS
const coordinates = [12.9716, 77.5946];
const setLocation = (lat, lng) => console.log(`Lat: ${lat}, Lng: ${lng}`);

setLocation(...coordinates); // Lat: 12.9716, Lng: 77.5946

4. Deep-Dive: JavaScript Engine Mechanics of Object Spread

To avoid subtle bugs, you must understand how ECMAScript actually evaluates { ...source } at the runtime layer.

TEXT
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Object Spread: Internal Pipeline             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 1. Check if source is null/undefined?
    β”œβ”€β”€ YES ──► Silently Ignore(Do nothing)
    └── NO  ──► Convert to Object: ToObject(source)
 2. Get own enumerable keys: [[OwnPropertyKeys]]()
 3. For each key:
    β”œβ”€β”€ Execute [[Get]](source, key) ──► Invokes Getter!
    └── Execute [[CreateDataProperty]](target, key, value)

4.1 What Spread Copies (and What It Skips)

JS
const proto = { inheritedKey: "I will be ignored" };
const targetSource = Object.create(proto);

targetSource.visible = "I will be copied";
Object.defineProperty(targetSource, "hidden", {
  value: "secret",
  enumerable: false,
});

const cloned = { ...targetSource };
console.log(cloned.visible);      // "I will be copied"
console.log(cloned.inheritedKey); // undefined (Prototype dropped!)
console.log(cloned.hidden);       // undefined (Non-enumerable dropped!)

4.2 Falsy & Primitive Spreading Edge Cases

Object spread and Array spread handle non-object data types with fundamentally different rules:

JS
// 1. Object Spread: Coerces primitives; silently ignores null/undefined
const safeObject = { ...null, ...undefined, ...true, ...42 };
console.log(safeObject); // {} (Empty object, no runtime exceptions thrown)

// String primitives have enumerable index properties:
const stringSpread = { ..."React" };
console.log(stringSpread); // { 0: 'R', 1: 'e', 2: 'a', 3: 'c', 4: 't' }

// 2. Array Spread: Demands a valid [Symbol.iterator]
// const badArray = [...null]; // πŸ’₯ TypeError: null is not iterable

5. The Rest Operator (...): Collecting Values In

Rest collects disconnected values and aggregates them into a clean JavaScript array or object.

5.1 Rest in Function Parameters vs. Legacy arguments

Before ES6, handling variable arguments required using the implicit arguments object. Rest parameters (...args) supersede arguments across modern codebases.

Feature Legacy arguments Object Modern Rest Parameter (...args)
Underlying Type Array-like Object (length and index access only; lacks array methods) True Array Instance (Array.isArray(args) === true; inherits full Array.prototype)
Arrow Functions ❌ Unavailable (inherits parent's arguments binding lexically) βœ… Fully supported
Capture Scope Inflexible; captures all parameters passed Selective; captures only remaining unassigned arguments
Destructuring ❌ Cannot be destructured within the signature βœ… Can be directly destructured in-place
JS
// ❌ Legacy arguments object: Requires manual array conversions
function legacyLogger(prefix) {
  // arguments includes 'prefix' along with all trailing strings
  const messages = Array.prototype.slice.call(arguments, 1);
  return messages.map(msg => `${prefix}: ${msg}`);
}

// βœ… Modern Rest parameter: True Array, selective capture, arrow-ready
const modernLogger = (prefix, ...messages) => {
  return messages.map(msg => `${prefix}: ${msg}`);
};

console.log(modernLogger("WARN", "Network timeout", "Retrying..."));
// ["WARN: Network timeout", "WARN: Retrying..."]

5.2 Rest Destructuring for Property Omission

Rest destructuring offers an immutable pattern for stripping unwanted keys from an object:

JS
const rawUser = {
  id: 101,
  username: "subrata_dev",
  passwordHash: "x89#kL@!",
  role: "admin",
};

// Omit 'passwordHash' without mutating the source object
const { passwordHash, ...sanitizedUser } = rawUser;

console.log(sanitizedUser); // { id: 101, username: "subrata_dev", role: "admin" }

6. Daily Production Patterns in React Native

6.1 Atomic Object State Updates

When calculating the next state from the previous state, pass a callback updater to avoid closure race conditions:

JS
const [profile, setProfile] = useState({
  name: "Subrata",
  city: "Bengaluru",
  steps: 0,
});

setProfile(prev => ({
  ...prev,
  steps: prev.steps + 500,
}));

6.2 Array State: Appending, Prepending, and Modifying Items

Never use in-place array mutators (.push(), .pop(), .splice(), .sort()). Use immutable array expressions:

JS
const [items, setItems] = useState([{ id: 1, text: "Buy Milk", done: false }]);

// Prepend item
const prependItem = (newItem) => setItems(prev => [newItem, ...prev]);

// Append item
const appendItem = (newItem) => setItems(prev => [...prev, newItem]);

// Update item immutably
const toggleDone = (id) => {
  setItems(prev => prev.map(item => 
    item.id === id ? { ...item, done: !item.done } : item
  ));
};

// Remove item immutably
const removeItem = (id) => {
  setItems(prev => prev.filter(item => item.id !== id));
};

6.3 Building Pass-Through Component Wrappers

Design systems use rest destructuring to extract layout-specific properties while forwarding remaining native props (onPress, testID, accessibility labels) via JSX spread:

JS
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';

const PrimaryButton = ({ title, style, textStyle, ...touchableProps }) => (
  <TouchableOpacity
    style={[styles.button, style]}
    activeOpacity={0.7}
    {...touchableProps} // Forwards onPress, disabled, testID, onLongPress, etc.
  >
    <Text style={[styles.text, textStyle]}>{title}</Text>
  </TouchableOpacity>
);

const styles = StyleSheet.create({
  button: { paddingVertical: 12, paddingHorizontal: 20, backgroundColor: "#007AFF", borderRadius: 8 },
  text: { color: "#FFFFFF", fontWeight: "600", textAlign: "center" },
});

7. The Shallow Copy Trap & Memory Overhead

7.1 The Shallow Copy Caveat

Fundamental Memory Law: The spread operator clones only the first level of an object or array. Nested objects, arrays, and functions are copied by reference (their pointer addresses are copied, not their underlying heap data).

JS
const state = {
  user: { name: "Subrata", address: { city: "Bengaluru" } },
  steps: 0,
};

const next = { ...state, steps: 500 };

// ⚠️ WARNING: next.user.address and state.user.address point to the EXACT SAME heap slot!
next.user.address.city = "Mumbai"; 
console.log(state.user.address.city); // "Mumbai" ❌ Original state was mutated!

7.2 Multi-Level Spread Architecture

To preserve pure immutability on deeply nested models, you must spread every level of the ancestor tree being altered:

JS
const next = {
  ...state,
  user: {
    ...state.user,
    address: {
      ...state.user.address,
      city: "Mumbai", // βœ… Receives a unique reference at every modified ancestor node
    },
  },
};

7.3 Performance Overhead & Hermes Engine Garbage Collection (GC)

While spreading is fast, it is not free:

TEXT
Deep State Management Rule:
1-2 Levels of Nesting ──► Use native Spread({ ...prev })
3+ Levels of Nesting  ──► Normalize state or use Immer(Structural Sharing)

8. Hands-On State Update Exercises

Given the following workout state:

JS
const [workout, setWorkout] = useState({
  userId: "u_001",
  date: "2026-06-29",
  exercises: ["pushups", "squats"],
  stats: {
    calories: 0,
    duration: 0,
  },
});

Task 1: Add "lunges" to the exercises array immutably.

JS
setWorkout(prev => ({
  ...prev,
  exercises: [...prev.exercises, "lunges"],
}));

Task 2: Update calories to 320 inside stats without modifying duration.

JS
setWorkout(prev => ({
  ...prev,
  stats: {
    ...prev.stats,
    calories: 320,
  },
}));

Task 3: Change date to "2026-06-30" and append "burpees" in a single batch update.

JS
setWorkout(prev => ({
  ...prev,
  date: "2026-06-30",
  exercises: [...prev.exercises, "burpees"],
}));

9. Common Pitfalls & Antipatterns

Pitfall 1: Mutating Arrays Inside State Setter Callbacks

JS
// ❌ FAILS: Mutates previous array in place and returns the same reference pointer
setLogs(prev => {
  prev.push(newEntry);
  return prev;
});

// βœ… FIX: Instantiates a new array container containing previous elements plus newEntry
setLogs(prev => [...prev, newEntry]);

Pitfall 2: Key Precedence Inversion

JS
const baseTheme = { color: "blue", size: 16 };

// ❌ BUG: baseTheme is spread after custom overrides, overwriting your intended size
const bad = { size: 24, ...baseTheme }; // { size: 16, color: "blue" }

// βœ… FIX: Place fallback/defaults first; place explicit overrides after
const good = { ...baseTheme, size: 24 }; // { color: "blue", size: 24 }

Pitfall 3: Trailing Rest Parameters in Function Signatures

JS
// ❌ SYNTAX ERROR: Rest must be the terminal parameter in the signature
// function process(first, ...rest, last) {}

// βœ… FIX: Rest parameter placed at the end of the argument list
function process(first, ...rest) {}

Technical Interview Questions

Q1: What will be logged to the console by the following code?

JS
const itemA = { id: 1, details: { count: 10 } };
const itemB = { ...itemA };

itemB.details.count = 25;
itemB.id = 2;

console.log(itemA.id, itemA.details.count);

Output: 1 25

Explanation: id is a primitive value stored directly on the top-level object memory record; modifying itemB.id does not affect itemA.id. However, details holds a heap reference pointer; because spread performs only a shallow copy, both itemA.details and itemB.details point to the identical heap address.


Q2: What is the difference in behavior between Array Spread and Object Spread when handling null or undefined?

Answer:


Q3: How does the Rest parameter solve scope and prototype limitations of the legacy arguments object in modern React Native functional components?

Answer:

  1. Arrow Function Lexical Binding: Arrow functions do not bind their own arguments object (they inherit it from the surrounding lexical scope). Rest parameters (...args) provide local argument capture directly in arrow functions.
  2. True Array Prototype: Rest produces an actual Array instance, allowing direct access to .map(), .filter(), and .reduce() without conversion overhead (Array.from(arguments) or [].slice.call(arguments)).
  3. Selective Parameter Partitioning: Rest parameters allow explicitly naming leading parameters (e.g., (prefix, delimiter, ...messages)) and collecting only the trailing items, whereas arguments captures the entire argument list indiscriminately.

Q4: In React Native, why does this component fail to update its UI when the button is pressed?

JS
const TaskList = () => {
  const [tasks, setTasks] = useState(["Design UI", "Write Code"]);

  const handleAdd = () => {
    tasks.push("Test App");
    setTasks(tasks);
  };

  return <Button title="Add" onPress={handleAdd} />;
};

Answer: Array.prototype.push() mutates the existing array in place. When passing tasks into setTasks(tasks), React performs a reference equality check via Object.is(prev, next). Because both variables hold the exact same pointer to the same heap location, React determines that state has not changed and bails out of the reconciliation cycle. To trigger a re-render, pass a new array reference: setTasks(prev => [...prev, "Test App"]).


Q5: How do you conditionally append a property to an object using spread and logical AND (&&) without adding an undefined key?

Answer:

JS
const includeLocation = true;

const userPayload = {
  name: "Subrata",
  ...(includeLocation && { location: "Bengaluru" }),
};

// If includeLocation is true  ──► { name: "Subrata", location: "Bengaluru" }
// If includeLocation is false ──► { name: "Subrata" } (false is ignored by object spread)