Create your own
Lesson illustration

Immutable Data Transformation with Destructuring, Spread, and Array Methods

Welcome to the first module of the course. We will begin by revising the JavaScript patterns that make browser applications predictable and maintainable—especially when handling lists of data returned from an API. These patterns will reappear constantly in React, where state updates must be treated as immutable.

In this lesson, you will learn to unpack data with destructuring, create updated copies with spread syntax, and use array methods such as map, filter, and reduce without changing the original data. We will use a small video-platform dataset, which will later become part of the larger project.


Why “without mutation” matters

Suppose an application has fetched a list of videos:

const videos = [
  { id: 1, title: "Docker Basics", views: 120, status: "ready" },
  { id: 2, title: "PostgreSQL Joins", views: 85, status: "processing" },
  { id: 3, title: "React State", views: 240, status: "ready" },
];

A mutation changes an existing array or object:

videos.push({ id: 4, title: "HLS Streaming", views: 0, status: "queued" });

After this line, videos itself has changed. Sometimes mutation is acceptable, but it becomes risky when several pieces of code share the same data. One function may silently alter a value that another function assumed was unchanged.

A non-mutating transformation instead creates a new value:

const updatedVideos = [
  ...videos,
  { id: 4, title: "HLS Streaming", views: 0, status: "queued" },
];

Here:

  • videos remains unchanged.
  • updatedVideos is a new array.
  • Existing video objects are reused unless you explicitly copy them too.

This is not merely a style preference. In React, state is normally updated by creating a replacement value. If you mutate existing state, React can fail to recognize what changed, and debugging becomes harder.

An array is shown being transformed into separate output elements, illustrating the idea of deriving new data rather than altering the original collection.

One important distinction:

const original = [1, 2, 3];
const copy = [...original];

console.log(original === copy); // false

The arrays are different objects in memory. But if an array contains objects, spreading makes only a shallow copy:

const originalVideos = [
  { id: 1, title: "Docker Basics" },
];

const copiedVideos = [...originalVideos];

console.log(originalVideos[0] === copiedVideos[0]); // true

The outer array is new, but its first object is still shared. We will address this when updating an individual video.


Destructuring: unpack only the data you need

Destructuring lets you take values out of arrays or properties out of objects and assign them directly to variables.

Why Is Array/Object Destructuring So Useful And How To Use It

Watch “Why Is Array/Object Destructuring So Useful And How To Use It” from Web Dev Simplified for a compact visual explanation of unpacking arrays and objects, including skipped values and defaults.

First watch array destructuring. Focus on how positions in the pattern correspond to positions in an array, and how ...rest gathers remaining values. Then watch defaults for destructuring a returned array safely. Finally, watch object destructuring, paying attention to property names, renaming, and fallback values.

Array destructuring

Without destructuring, extracting the first two values means indexing:

const playback = ["playing", 42, 3600];

const status = playback[0];
const currentTime = playback[1];

With destructuring:

const [status, currentTime] = playback;

The left side is a pattern. Array destructuring is based on position.

const [status, , duration] = playback;

console.log(status);   // "playing"
console.log(duration); // 3600

The empty position skips the second item. You can also gather all remaining items:

const renditions = ["1080p", "720p", "480p", "360p"];

const [primaryRendition, ...fallbackRenditions] = renditions;

console.log(primaryRendition);    // "1080p"
console.log(fallbackRenditions);  // ["720p", "480p", "360p"]

In this context, ...fallbackRenditions is called a rest element: it gathers the remaining values into a new array.

You can provide defaults for data that might not be present:

const [title, duration = 0] = ["Django REST Framework"];

console.log(duration); // 0

Object destructuring

API responses are usually arrays of objects, so object destructuring is especially useful:

const video = {
  id: 3,
  title: "React State",
  views: 240,
  status: "ready",
};

const { title, views } = video;

console.log(title); // "React State"
console.log(views); // 240

Unlike arrays, object destructuring is based on property names, not order:

const { status, id } = video;

This works regardless of where those properties appear in the object.

You can rename a property while destructuring:

const { title: videoTitle } = video;

console.log(videoTitle); // "React State"

And you can set a default value:

const { thumbnailUrl = "/images/default-thumbnail.png" } = video;

You may also destructure directly in a function parameter. This keeps the function explicit about the fields it needs:

function formatVideoCard({ title, views, status }) {
  return `${title} — ${views} views (${status})`;
}

console.log(formatVideoCard(video));

At this stage, prefer this form when a function uses only a few properties. It avoids repetitive expressions such as video.title, video.views, and video.status.


Spread syntax: make copies and build updated values

The same three dots, ..., have different meanings depending on where they appear:

  • In a destructuring pattern, ...rest collects remaining values.
  • In an array, object, or function call, spread syntax expands values.

For this lesson, spread syntax is primarily a tool for building copies and updates.

Arrays: copy, append, prepend, and merge

const readyVideos = videos.filter(video => video.status === "ready");

const allVideos = [...readyVideos];

allVideos is a new array containing the same items as readyVideos.

Appending a new item:

const newVideo = {
  id: 4,
  title: "HLS Streaming",
  views: 0,
  status: "queued",
};

const withNewVideo = [...videos, newVideo];

Prepending an item:

const featuredVideo = {
  id: 0,
  title: "Course Introduction",
  views: 500,
  status: "ready",
};

const withFeaturedFirst = [featuredVideo, ...videos];

Merging two arrays:

const uploadedVideos = [
  { id: 4, title: "FFmpeg Fundamentals", views: 0, status: "processing" },
];

const library = [...videos, ...uploadedVideos];

Spread syntax can also expand an array into function arguments:

const viewCounts = videos.map(video => video.views);

const highestViewCount = Math.max(...viewCounts);

console.log(highestViewCount); // 240

Math.max expects separate number arguments, while viewCounts is an array. ...viewCounts expands the array into the required argument list.

Objects: copy properties and override selected ones

To update an object without modifying it, copy its existing properties and then specify replacements:

const originalVideo = videos[1];

const processingVideo = {
  ...originalVideo,
  status: "ready",
};

console.log(originalVideo.status);   // "processing"
console.log(processingVideo.status); // "ready"

Property order matters when spreading objects:

const videoWithIncorrectOrder = {
  status: "ready",
  ...originalVideo,
};

console.log(videoWithIncorrectOrder.status); // "processing"

Because originalVideo is spread last, its status overwrites "ready".

Use this reliable pattern:

const updatedObject = {
  ...originalObject,
  propertyToChange: newValue,
};

For example, merging defaults with a supplied configuration:

const defaultPlayerOptions = {
  autoplay: false,
  muted: false,
  playbackRate: 1,
};

const userOptions = {
  muted: true,
  playbackRate: 1.5,
};

const playerOptions = {
  ...defaultPlayerOptions,
  ...userOptions,
};

console.log(playerOptions);
// { autoplay: false, muted: true, playbackRate: 1.5 }

Later objects override matching properties from earlier ones.


The essential non-mutating array methods

An array transformation begins with a useful question: What should the result be?

  • A transformed version of every item? Use map.
  • Only items that meet a condition? Use filter.
  • One accumulated result, such as a count or total? Use reduce.
  • A copy with extra items? Use spread syntax or concat.
  • A sorted copy? Copy first, then sort the copy.

How to use map() filter() reduce() | JavaScript Array Methods Tutorial

Watch “How to use map() filter() reduce() | JavaScript Array Methods Tutorial” from Coding2GO. It connects each method to the shape of the value it returns and demonstrates transformations of objects with spread syntax.

Watch map to see why returning new objects with spread is necessary when changing one field. Continue with filter to distinguish retaining items from transforming them. Then watch reduce, focusing on the accumulator and the value of supplying an initial value. Finish with method chaining for the common filter–map–reduce pipeline.

map: transform every item

map returns a new array with exactly the same number of items as the original. Its callback decides the new form of each item.

const titles = videos.map(video => video.title);

console.log(titles);
// ["Docker Basics", "PostgreSQL Joins", "React State"]

This transforms an array of video objects into an array of strings.

To transform objects, return a new object for each item:

const labeledVideos = videos.map(video => ({
  ...video,
  label: `${video.title} (${video.views} views)`,
}));

The parentheses around the object are important:

video => ({
  ...video,
  label: video.title,
})

Without the parentheses, JavaScript treats {} as the body of the arrow function rather than an object being returned.

A practical immutable update is “increment the views for only one video”:

const viewedVideoId = 2;

const videosAfterView = videos.map(video =>
  video.id === viewedVideoId
    ? { ...video, views: video.views + 1 }
    : video
);

This has an important property:

  • The array is new.
  • The changed video is a new object.
  • Unchanged videos are reused safely because they were not altered.
  • The original videos array and its objects remain unchanged.

Avoid this:

const incorrectUpdate = videos.map(video => {
  if (video.id === viewedVideoId) {
    video.views += 1;
  }

  return video;
});

Although map creates a new outer array, the callback mutates the original video object. A non-mutating array method cannot protect you from mutations inside its callback.

filter: retain matching items

filter creates a new array containing only items whose callback returns a truthy value.

const readyVideos = videos.filter(video => video.status === "ready");

console.log(readyVideos);
// [
//   { id: 1, title: "Docker Basics", views: 120, status: "ready" },
//   { id: 3, title: "React State", views: 240, status: "ready" }
// ]

Filtering is the natural way to remove an item immutably:

const deletedVideoId = 2;

const videosWithoutDeleted = videos.filter(
  video => video.id !== deletedVideoId
);

Unlike map, filter may return fewer items. It never changes the original array.

reduce: combine many values into one result

reduce walks through an array and produces one final result: a number, string, object, array, or another value.

To calculate total views:

const totalViews = videos.reduce(
  (total, video) => total + video.views,
  0
);

console.log(totalViews); // 445

The 0 is the initial value. The sequence is:

Current videoAccumulated total
Start0
Docker Basics: 120120
PostgreSQL Joins: 85205
React State: 240445

Always provide an initial value when practical. It establishes the type of the result and makes the code safe for an empty array:

const noVideos = [];

const total = noVideos.reduce(
  (sum, video) => sum + video.views,
  0
);

console.log(total); // 0

Without an initial value, reduce throws an error for an empty array.

Combine transformations deliberately

A typical UI needs data that is filtered, transformed for display, then summarized:

const readyVideoSummary = videos
  .filter(video => video.status === "ready")
  .map(video => ({
    id: video.id,
    title: video.title,
    views: video.views,
  }))
  .reduce((total, video) => total + video.views, 0);

console.log(readyVideoSummary); // 360

Read the chain from top to bottom:

  1. Keep only ready videos.
  2. Project each video into the fields the UI needs.
  3. Add the remaining view counts.

Do not chain methods simply to make code shorter. A chain is useful when each step has a distinct data purpose. For more complex logic, assigning meaningful intermediate variables is often clearer.


Methods that can surprise you

Some familiar array methods mutate their array:

MethodMutates the original?Safer non-mutating pattern
push, pop, shift, unshiftYesUse spread syntax
spliceYesUse filter, map, or array slices
sortYesCopy first, then sort
reverseYesCopy first, then reverse
map, filter, reduceNo, by themselvesAvoid mutating objects inside callbacks
slice, concatNoAlready return new arrays

The most common accidental mutation in UI code is sort:

const sortedByViews = videos.sort((a, b) => b.views - a.views);

This sorts videos itself. The variable sortedByViews points to the same mutated array.

Instead, copy first:

const sortedByViews = [...videos].sort(
  (a, b) => b.views - a.views
);

Now sortedByViews is sorted while videos stays in its original order.

The same approach works with reverse:

const newestFirst = [...videos].reverse();

For a smaller section of an array, slice creates a new array:

const firstTwoVideos = videos.slice(0, 2);

And concat returns a new combined array:

const moreVideos = videos.concat(uploadedVideos);

In new application code, [...videos, ...uploadedVideos] is often easier to scan, but both approaches are non-mutating.


A practical transformation toolkit

For the video data used in this course, these are the core patterns worth becoming fluent with:

// Read fields
const { id, title, status } = video;

// Copy an array
const copiedVideos = [...videos];

// Add an item
const afterUpload = [...videos, newVideo];

// Remove an item
const afterDelete = videos.filter(video => video.id !== deletedVideoId);

// Update one object in an array
const afterStatusUpdate = videos.map(video =>
  video.id === targetId
    ? { ...video, status: "ready" }
    : video
);

// Create display data
const cards = videos.map(({ id, title, views }) => ({
  id,
  text: `${title} — ${views} views`,
}));

// Calculate a summary
const totalViews = videos.reduce(
  (sum, video) => sum + video.views,
  0
);

// Sort without altering source data
const mostViewed = [...videos].sort((a, b) => b.views - a.views);

A useful habit while revising is to verify both the result and the source:

const source = [3, 1, 2];
const result = [...source].sort((a, b) => a - b);

console.log(result); // [1, 2, 3]
console.log(source); // [3, 1, 2]

Use console.log freely while working. It is the quickest way to confirm whether you have created a fresh value or accidentally changed existing data.


Key takeaways

Destructuring lets you express exactly which values or properties you need. Spread syntax lets you construct new arrays and objects from existing ones. Together with map, filter, and reduce, these features form a practical immutable-data toolkit:

  • Use array destructuring for positional values and object destructuring for named properties.
  • Use spread syntax to copy, merge, append, and override values.
  • Use map to transform every item, filter to retain matching items, and reduce to compute one accumulated result.
  • Copy before using mutating methods such as sort and reverse.
  • When changing an object inside an array, create both a new array and a new version of the changed object.

Next, you will organize browser code with ES modules and manage project dependencies with npm—the foundation needed to begin a React application with Vite.

Can't find a good explanation? Sign up and we'll make it for you

Sign up