Learning path · Level 4 · Tutorial 6

Make Non-Mutating Updates with Spread Syntax

Extract values with destructuring and produce updated copies with spread syntax without accidentally changing the original data.

  • Level: Beginner
  • Duration: 18 min
  • 8 steps
Start tutorial Back to the path
Make Non-Mutating Updates with Spread Syntax

What you will go through

  1. Extract Named Values with Object Destructuring Extract named values from an object with destructuring while keeping the original task unchanged. 2 min
  2. Extract Positional Values with Array Destructuring Extract the first two task tags with array destructuring while leaving the original tag array unchanged. 2 min
  3. Create an Updated Object with Spread Create a separate updated task object with object spread and verify that the source task remains unchanged. 3 min
  4. Build a New Array with an Additional Item Use array spread to create a new tag array with an additional item while preserving the source array. 2 min
  5. Return Updated Copies from Functions Use small updater functions to return new objects or arrays while keeping the caller's input unchanged. 2 min
  6. Recognize What a Shallow Copy Still Shares Predict which references remain shared after spread and observe why a nested write can affect the original data. 3 min
  7. Copy Each Level Along the Changed Branch Update nested task data by copying each changed branch with spread syntax while preserving original values and unchanged sibling data. 3 min
  8. Apply and Verify a Complete Non-Mutating Update Complete, run, and verify a nested non-mutating task update with destructuring and spread syntax. 3 min

What you will learn

  • Extract selected object properties and array entries with destructuring.
  • Create an updated object while preserving its other properties.
  • Create a new array containing existing values and an additional item.
  • Demonstrate why a shallow copy does not isolate nested objects and copy the changed branch when needed.

Before you start

  • Write Functions with Parameters and Lexical Scope
  • Represent Records and Lists with Objects and Arrays

Step 1 of 8

Extract Named Values with Object Destructuring

Extract named values from an object with destructuring while keeping the original task unchanged.

Extract values without changing the task

Read selected properties

Object destructuring lets you declare bindings by selecting properties from an object. The names inside the braces match the property names you want to read.

This reads title and done from task. It does not remove properties or write anything to the original object.

Run the extraction

javascript
const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };
const { title, done } = task;

console.log(title);
console.log(done);
console.log(task.title);
console.log(task.details.minutes);
Expected output
Practice JavaScript
false
Practice JavaScript
20

These scalar logs show the extracted values and confirm that the original task still provides its properties.

Follow the property names

Names select properties

In an object destructuring pattern, property names choose the values. Their position does not choose an object property.

The title binding receives the value of task.title, and the done binding receives the value of task.done. Properties not named in the pattern, such as details and tags, simply remain on task.

Property-to-binding relationship

A diagram shows title and done properties flowing from the original task object into bindings with the same names, while details and tags remain in the original object.

Object destructuring matches each requested binding name to a property with that name.

Practice the declaration

Complete the pattern

Complete the destructuring pattern using the two binding names introduced in this step.

javascript
const { ,  } = task;

Apply and verify

Explain the extraction

Using the original task record from this lesson, write the lesson’s two-binding object destructuring declaration. Then state the two extracted primitive values and name two properties that are still present on task.

Write at least 80 characters (0/80).

Step 2 of 8

Extract Positional Values with Array Destructuring

Extract the first two task tags with array destructuring while leaving the original tag array unchanged.

Read the first two positions

Position chooses the value

Array destructuring uses position. In a pattern with two bindings, the first binding receives the entry at index 0 and the second receives the entry at index 1.

The binding names are your choice; their order in the brackets determines which entry each one receives. This extracts values only—it does not remove anything from the source array.

How array destructuring maps entries

A two-entry array maps its index 0 entry to firstTag and its index 1 entry to secondTag, while the source array remains unchanged.

Array destructuring follows entry order: index 0 goes to the first binding, and index 1 goes to the second.

Observe the extracted values

Extract without changing the array

Run this in your browser console. The first two logs come from the new bindings. The final logs show that the original array still contains both entries.

Array destructuring in the console

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };

  const [firstTag, secondTag] = task.tags;

  console.log(firstTag);
  console.log(secondTag);
  console.log(task.tags[0]);
  console.log(task.tags[1]);
}
Expected output
study
javascript
study
javascript

Tip

Remember

firstTag and secondTag are not property names being searched for. They are bindings that receive the first and second array entries because of their positions in the pattern.

Practice the destructuring pattern

Complete the pattern

Complete the destructuring pattern using the established binding names.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };

  const [, ] = task.tags;
}

Match positions and selection rules

Match positions and selection rules

Match each item to its correct meaning.

Tap an item and then its matching pair.

Step 3 of 8

Create an Updated Object with Spread

Create a separate updated task object with object spread and verify that the source task remains unchanged.

Compose an updated task

Build a new outer object

Spread syntax composes an object

Object destructuring extracts values into bindings. Object spread syntax contributes an object's properties to a new object literal.

In { ...task, done: true }, spread contributes the existing task properties, then the explicit done: true entry supplies an updated value. The source task is not written to.

Source task

Use this task record for the examples in this tutorial. The surrounding block keeps this console snippet separate from examples on other screens.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };
}

What the object literal creates

A diagram compares an original task object with done false to a separate updated task object with done true while its other displayed properties are preserved.

Spread contributes existing properties; the later explicit property provides the new value.

Verify the original and the copy

Override order matters

Put the override after the spread

Property entries are processed from left to right. If a property appears more than once, the last supplied value wins.

So { ...task, done: true } produces a task whose done value is true. By contrast, { done: true, ...task } finishes by supplying task.done, which is still false.

Writing task.done = true would change the original object. Constructing updatedTask creates a non-mutating update instead.

Create and verify an updated copy

Run this complete snippet in your browser console. The block makes it safe to run even after other lesson examples. Scalar logs show values and outer-object identity directly.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };
  const updatedTask = { ...task, done: true };

  console.log(task.done);
  console.log(updatedTask.done);
  console.log(task.title);
  console.log(updatedTask.title);
  console.log(task.details.minutes);
  console.log(updatedTask.details.minutes);
  console.log(updatedTask !== task);
}
Expected output
false
true
Practice JavaScript
Practice JavaScript
20
20
true

The final true means updatedTask and task are different outer objects.

Tip

Keep both values

A non-mutating update keeps the original binding available. Use task when you need the unchanged record and updatedTask when you need the changed result.

Apply the override rule

Check object-spread behavior

Given the task record from the previous screen, which complete statement is correct?

Step 4 of 8

Build a New Array with an Additional Item

Use array spread to create a new tag array with an additional item while preserving the source array.

Copy the existing tags

Copy an array’s entries

tags refers to the task’s tag array. Inside a new array literal, spread syntax contributes every existing entry in its current order.

[...tags] creates a new outer array with the same entries. It does not remove entries from tags or write to the source array.

Create a separate outer array

Run this complete snippet in your browser console. The surrounding block keeps its bindings separate from other lesson examples.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };
  const tags = task.tags;
  const copiedTags = [...tags];

  console.log(copiedTags !== tags);
  console.log(tags.length);
  console.log(copiedTags.length);
}
Expected output
true
2
2

Tip

Read the checks

The true reference check shows that copiedTags and tags are different outer arrays. Both lengths are 2 because spread contributed the existing entries without changing the source array.

Construct the updated array

Append without mutation

To build an updated array, put an additional entry in the new array literal. An entry after the spread is appended; an entry before the spread is prepended.

For this task, place 'spread' after ...tags. The original tags array keeps its two entries, while updatedTags contains three.

Original versus updated tag arrays

Diagram showing an original tags array with exactly two entries, study and javascript, and an updated array with those entries followed by spread.

Array spread retains the source order, and an entry after the spread is appended to the new array.

Append a tag in a new array

Run this complete snippet in your browser console.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };
  const tags = task.tags;
  const updatedTags = [...tags, 'spread'];

  console.log(tags.length);
  console.log(updatedTags.length);
  console.log(tags[1]);
  console.log(updatedTags[2]);
}
Expected output
2
3
javascript
spread

Practice the resulting order

Arrange the updated entries

Arrange the entries of updatedTags from first to last.

  1. javascript
  2. study
  3. spread

Check the complete non-mutating update

Compose and verify the update

Complete the array-spread expression so it creates the updated list in order. Then complete the two length predictions.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };
  const tags = task.tags;
  const updatedTags = [];

  console.log(tags.length === );
  console.log(updatedTags.length === );
}

Step 5 of 8

Return Updated Copies from Functions

Use small updater functions to return new objects or arrays while keeping the caller's input unchanged.

Return an updated object

Return a new task

A non-mutating updater receives an input, constructs a replacement, and returns that replacement instead of writing to its parameter.

markDone(task) uses its parameter as the source for object spread. The caller keeps the original task and stores the returned object separately in updatedTask. The returned object has a changed top-level status; the input is not assigned to.

Input and returned value

Diagram showing an original task entering a function, remaining unchanged, and a separate updated task being returned.

The caller retains the original task and captures a distinct outer object in updatedTask.

Object updater

Run this complete block in your browser console. The logs verify the returned status, the unchanged input, and the distinct outer references.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };

  function markDone(task) {
    return { ...task, done: true };
  }

  const updatedTask = markDone(task);

  console.log(updatedTask.done);
  console.log(task.done);
  console.log(updatedTask !== task);
}
Expected output
true
false
true

Return an updated array

Return a new tag array

The same pattern works for an array. addTag(tags) uses the parameter as the source of array spread, then returns a newly constructed array with one additional entry.

The caller still has tags, while updatedTags receives the returned array. A new array can contain the old entries without changing the source array’s length or entries.

Array updater

Run this complete block in your browser console. It checks the returned array, the source array, and their outer references.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };
  const tags = task.tags;

  function addTag(tags) {
    return [...tags, 'spread'];
  }

  const updatedTags = addTag(tags);

  console.log(updatedTags !== tags);
  console.log(tags.length);
  console.log(updatedTags.length);
  console.log(updatedTags[2]);
}
Expected output
true
2
3
spread

Practice: returns versus inputs

Complete the updaters and predict the results

Complete both return expressions, then complete the four value predictions. When complete, every console check evaluates to true. The final two checks distinguish each input reference from its returned outer reference.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };
  const tags = task.tags;

  function markDone(task) {
    return ;
  }

  function addTag(tags) {
    return ;
  }

  const updatedTask = markDone(task);
  const updatedTags = addTag(tags);

  console.log(updatedTask.done === );
  console.log(task.done === );
  console.log(updatedTags.length === );
  console.log(tags.length === );
  console.log(updatedTask !== task);
  console.log(updatedTags !== tags);
}

Step 6 of 8

Recognize What a Shallow Copy Still Shares

Predict which references remain shared after spread and observe why a nested write can affect the original data.

See the shallow-copy boundary

A copy has layers

Spread syntax makes a shallow copy. It creates a new outer object or array, but it does not recursively create new nested objects and arrays.

For task, the outer task object can be new while its details object and tags array remain shared references. Likewise, a new outer tasks array can still contain the same task object.

Outer copies versus nested references

A reference diagram showing separate original and updated outer containers that point to the same nested details object, tags array, and task object.

Spread separates the outer container only. Nested values can still be shared.

Tip

Key distinction

A new container is not automatically independent at every depth. Use reference comparisons to check which values are separate and which values are still shared.

Verify what spread shares

Compare outer and nested references

Run these reference checks in your browser console. The surrounding block makes this example safe to run alongside other lesson snippets.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };
  const { details, tags } = task;
  const tasks = [task];

  const copiedTask = { ...task };
  const copiedTasks = [...tasks];

  console.log(copiedTask === task);
  console.log(copiedTask.details === details);
  console.log(copiedTask.tags === tags);
  console.log(copiedTasks === tasks);
  console.log(copiedTasks[0] === tasks[0]);
}
Expected output
false
true
true
false
true

Each line is a scalar boolean result from one comparison.

Read the results

The first and fourth checks are false: spread made new outer containers. The nested details and tags references are still the same, and both arrays contain the same task object.

This is why spread is called a shallow copy. Destructuring details and tags selected their existing references; it did not clone either nested value.

Observe a shared nested write

Warning

A nested write crosses the boundary

A write through a shared nested reference is visible through every container that reaches that reference. The next snippet mutates deliberately so you can observe the risk; it is not a non-mutating update pattern.

Observe a shared nested write

Run this separate, scoped console snippet. Notice the original duration after the write.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };
  const { details, tags } = task;
  const copiedTask = { ...task };

  copiedTask.details.minutes = 30;

  console.log(task.details.minutes);
  console.log(details === task.details);
  console.log(tags === task.tags);
}
Expected output
30
true
true

The first line shows the original task's nested duration after the deliberate write. The next two lines confirm that destructuring selected existing references.

Why the original changed

copiedTask is a new outer object, but copiedTask.details reaches the same nested object as task.details. Therefore, changing minutes through the copy also changes the value visible through the original.

Destructuring a nested object or array extracts the existing reference; it does not clone that object or array.

Check a shallow copy

Trace the shared references

Complete the reference checks and the final duration prediction.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };
  const tasks = [task];

  const copiedTask = { ...task };
  const copiedTasks = [...tasks];

  console.log(copiedTask  task);
  console.log(copiedTask.details  task.details);
  console.log(copiedTasks  tasks);
  console.log(copiedTasks[0]  tasks[0]);

  copiedTask.details.minutes = 30;
  console.log(task.details.minutes === );
}

Step 7 of 8

Copy Each Level Along the Changed Branch

Update nested task data by copying each changed branch with spread syntax while preserving original values and unchanged sibling data.

Trace the changed branch

Follow the changed branch

A nested non-mutating update creates a new container at every level on the path to the changed value. To change task.details.minutes, create a new outer task object and a new details object.

Branches outside that path do not need to be cloned. In a minutes-only update, tags is untouched, so it may remain a shared reference. This is deliberate: non-mutating does not mean copying every nested value.

Copy only the path that changes

A minutes update copies the outer task and its details branch; tags is unchanged.
PartMinutes-only update
Outer taskNew object
detailsNew object
minutesChanged to 30
priorityPreserved
tagsMay remain shared

A copied changed branch

Diagram showing new outer task and details objects for a minutes update, while the unchanged tags array remains shared.

For a minutes update, copy the task and details containers; the unchanged tags branch may stay shared.

Copy the details branch

Copy task and details

Put the outer spread first, then replace details with a new object. Inside the new details object, spread the existing details before setting minutes: 30.

The inner spread preserves the sibling priority property. The two changed containers are distinct, while the untouched tags branch can still be shared.

Update minutes safely

Run this complete, scoped snippet in your browser console. The block prevents its declarations from conflicting with examples on other screens.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };

  const updatedTask = {
    ...task,
    details: {
      ...task.details,
      minutes: 30
    }
  };

  console.log(task.details.minutes);
  console.log(updatedTask.details.minutes);
  console.log(updatedTask.details.priority);
  console.log(updatedTask !== task);
  console.log(updatedTask.details !== task.details);
  console.log(updatedTask.tags === task.tags);
}
Expected output
20
30
normal
true
true
true

The final check is true because tags was not changed in this update, so that nested array remains shared.

Choose the other changed branch

Copy the tags branch

When the tag list changes instead, the changed path is the outer task and its tags array. Create a new outer object, then use array spread to build a new array with the existing tags followed by 'spread'.

This time, details is untouched and may remain a shared reference. Copy the branch you change—not the entire structure.

Append a tag inside a copied task

Run this complete, scoped snippet in your browser console.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };

  const updatedTask = {
    ...task,
    tags: [...task.tags, 'spread']
  };

  console.log(task.tags.length);
  console.log(updatedTask.tags.length);
  console.log(task.tags[1]);
  console.log(updatedTask.tags[2]);
  console.log(updatedTask !== task);
  console.log(updatedTask.tags !== task.tags);
  console.log(updatedTask.details === task.details);
}
Expected output
2
3
javascript
spread
true
true
true

The final check is true because details was not changed, so that branch may remain shared.

Apply the changed-branch rule

Identify what must be copied

Match each update situation to the required result.

Tap an item and then its matching pair.

Practice: copy each changed branch

Complete both nested non-mutating updates. Each scoped example also checks the untouched branch and the preserved sibling value.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };

  const updatedTask = {
    ...task,
    details: {
      ,
      minutes: 30
    }
  };

  console.log(updatedTask !== task);
  console.log(updatedTask.details !== task.details);
  console.log(updatedTask.tags  task.tags);
  console.log(updatedTask.details.priority === task.details.priority);
}

{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };

  const updatedTask = {
    ...task,
    tags: [, 'spread']
  };

  console.log(updatedTask !== task);
  console.log(updatedTask.tags !== task.tags);
  console.log(updatedTask.details  task.details);
}

Step 8 of 8

Apply and Verify a Complete Non-Mutating Update

Complete, run, and verify a nested non-mutating task update with destructuring and spread syntax.

Plan the complete update

Combine the update rules

A complete non-mutating update combines the rules you have practiced:

  • prepareTask returns a new outer task with a new done value.
  • Because details.minutes changes, it also creates a new details object.
  • Because the tag list changes, it creates a new tags array.
  • The existing priority value is preserved when the new details object spreads the old details first.

After the function returns, destructuring only extracts selected values from the returned task. It does not perform the update.

Think in paths: copy every container on a changed path, then override the final value. Unchanged branches may still use a shared reference.

Follow the changed branches

Diagram comparing an original task with an updated task. The outer task, details object, and tags array are all new on the updated side, while priority is preserved.

Copy the outer task and each changed nested branch; preserve sibling data with spread syntax.

Tip

Verification tip

A reference check answers a different question from a value check. A value check confirms what is stored. A comparison such as updatedTask.details !== task.details confirms that the changed nested container is distinct.

Practice the complete function

Complete the updater

Complete prepareTask so it returns the intended updated task while preserving the input task and the sibling priority value.

javascript
function prepareTask(task) {
  const updatedTags = [...task.tags, 'spread'];

  return {
    ,
    done: ,
    details: {
      ,
      minutes: 
    },
    tags: updatedTags
  };
}

Run a full verification

Run the checks

Paste this complete example into your browser console and run it. The surrounding block keeps these declarations separate from every earlier console example. Each log is a scalar value or a reference comparison, so you can verify the update without relying on an expandable object preview.

javascript
{
  const task = { title: 'Practice JavaScript', done: false, details: { minutes: 20, priority: 'normal' }, tags: ['study', 'javascript'] };

  function prepareTask(task) {
    const updatedTags = [...task.tags, 'spread'];

    return {
      ...task,
      done: true,
      details: {
        ...task.details,
        minutes: 30
      },
      tags: updatedTags
    };
  }

  const updatedTask = prepareTask(task);
  const { title, done } = updatedTask;
  const [firstTag, secondTag] = updatedTask.tags;

  console.log('Updated title:', title);
  console.log('Updated done:', done);
  console.log('First tag:', firstTag);
  console.log('Second tag:', secondTag);
  console.log('Updated minutes:', updatedTask.details.minutes);
  console.log('Last updated tag:', updatedTask.tags[2]);
  console.log('Original done:', task.done);
  console.log('Original minutes:', task.details.minutes);
  console.log('Original tag count:', task.tags.length);
  console.log('Preserved priority:', updatedTask.details.priority);
  console.log('Different task objects:', updatedTask !== task);
  console.log('Different details objects:', updatedTask.details !== task.details);
  console.log('Different tag arrays:', updatedTask.tags !== task.tags);
}
Expected output
Updated title: Practice JavaScript
Updated done: true
First tag: study
Second tag: javascript
Updated minutes: 30
Last updated tag: spread
Original done: false
Original minutes: 20
Original tag count: 2
Preserved priority: normal
Different task objects: true
Different details objects: true
Different tag arrays: true

The labels and values below are the expected console output.

Verify and retain the rules

Report what the checks prove

After running the example, write a short verification report. Include the original completion status, duration, and tag count; the returned completion status, duration, and added tag; the preserved priority; and the three reference comparisons that establish distinct outer, details, and tags containers.

Write at least 180 characters (0/180).

Summary

Three rules to retain

  • Destructuring extracts selected values; it does not change the source or compose an update.
  • Spread syntax composes a new object or array, and later explicit properties supply updated values.
  • For a nested non-mutating update, copy every container on the changed branch while preserving untouched sibling data.
  • Verify both values and references: unchanged source values plus distinct copied containers demonstrate the intended update.

Tutorial complete

Congratulations! You completed: Make Non-Mutating Updates with Spread Syntax

You can now extract values and build verified non-mutating updates with spread syntax, including nested changes.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate