Skip to content

JavaScript for Interactive Interfaces

Step 6 of 45

Functions and Data Transformations

Introduction

Almost everything a React interface does is a transformation of data: take a list of registrations, keep the paid ones, group them, sort them, and render the result. This lesson, part of the JavaScript domain of the Meta Front-End Developer pathway (course 2), gives you the tools for that work: functions, arrays, objects, the map/filter/reduce family, destructuring, spread and rest, and the habit of updating data without mutating it. That last habit is not a style preference. React decides what to re-render by comparing old and new values, so code that changes data in place produces screens that do not update.

The practice scenario is original: Harbor Workshops has a list of registrations and needs a sorted attendance summary for the organizer.

Learning objectives

  1. Write functions as declarations, expressions, and arrow functions, with default and rest parameters.
  2. Explain return values, undefined results, and the difference between pure and impure functions.
  3. Use map, filter, reduce, find, some, every, includes, and sort correctly.
  4. Read and write destructuring, spread, and rest syntax.
  5. Update arrays and objects immutably.
  6. Distinguish shallow copies from deep copies and know when each matters.
  7. Describe closures and the behavior of this at a basic level.

1. Functions

A function is a reusable block of code that receives parameters (names in the definition), is called with arguments (values at the call), and returns a value.

function seatsLeft(capacity, booked) {   // declaration
  return capacity - booked;
}

const seatsLeft2 = function (capacity, booked) { // expression
  return capacity - booked;
};

const seatsLeft3 = (capacity, booked) => capacity - booked; // arrow, implicit return

Key differences:

Form Hoisted? Own this? Typical use
Declaration Yes, fully Yes Named utilities
Function expression Only the variable name Yes Callbacks, assigned values
Arrow function Only the variable name No (uses the surrounding this) Short callbacks, React code

An arrow function with a block body needs an explicit return. Without it the function returns undefined:

const double = (n) => { n * 2 };     // returns undefined!
const double2 = (n) => n * 2;        // returns the product
const makeObj = () => ({ ok: true });// parentheses needed to return an object literal

A function without a return statement returns undefined. Forgetting return inside a callback is a very common reason map produces [undefined, undefined].

Default and rest parameters

const add = (a, b = 1) => a + b;
console.log(add(2));            // 3  (b defaults to 1)
console.log(add(2, undefined)); // 3  (undefined triggers the default)
console.log(add(2, null));      // 2  (null does not: 2 + null = 2)

function sum(...xs) {            // rest: collects remaining arguments into an array
  return xs.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum());        // 0

Functions are values

Functions can be stored in variables, passed to other functions, and returned from them. A function that takes or returns a function is a higher-order function. Array methods like map are higher-order: you pass them a callback.

Closures

A closure is a function that remembers the variables of the place where it was created.

const makeCounter = () => {
  let n = 0;
  return () => ++n;
};
const counter = makeCounter();
counter();
console.log(counter()); // 2

n is private to the closure; nothing else can change it. React hooks and event handlers rely on this behavior, which is why a handler "sees" the values of the render in which it was created.

Pure functions

A pure function returns the same output for the same input and has no side effects (no changing outer variables, no writing to the DOM, no network). Pure functions are easy to test and are exactly what React expects from a component's rendering logic.

let total = 0;
const addImpure = (n) => { total += n; };    // impure: changes outer state
const addPure = (current, n) => current + n; // pure

2. Arrays

An array is an ordered list. Indexes start at 0, and length is one more than the last index.

const nums = [10, 2, 33, 4];
nums[0];        // 10
nums.at(-1);    // 4
nums.length;    // 4

Methods that mutate vs. methods that return new arrays

Mutates the original Returns a new array/value
push, pop, shift, unshift map, filter, slice, concat
splice, sort, reverse [...arr], flat, toSorted, toReversed

Knowing which group a method belongs to is a classic exam topic. sort() and reverse() change the array they are called on.

The transformation trio

map transforms each element and returns an array of the same length:

["1", "2", "3"].map(Number); // [1, 2, 3]

filter keeps elements whose callback returns a truthy value:

[1, 2, 3, 4].filter((n) => n % 2 === 0); // [2, 4]

reduce folds the array into a single value. It takes a callback (accumulator, element) => newAccumulator and an initial value:

[1, 2, 3].reduce((acc, n) => acc + n, 0); // 6

Always provide the initial value. Without it, the first element is used as the accumulator, which fails on empty arrays (TypeError: Reduce of empty array with no initial value).

Other helpers worth knowing:

[1, 2, 3, 4].find((n) => n > 2);      // 3     first match or undefined
[1, 2, 3, 4].findIndex((n) => n > 9); // -1    not found
[1, 2, 3].some((n) => n > 2);         // true  at least one
[1, 2, 3].every((n) => n > 2);        // false all
[1, 2].includes(2);                   // true
[[1], [2, [3]]].flat();               // [1, 2, [3]] one level
Array.from({ length: 3 }, (_, i) => i * 2); // [0, 2, 4]

A well-known trap: ["1", "2", "3"].map(parseInt) gives [1, NaN, NaN], because map passes (element, index, array) and parseInt reads the index as its radix. Write .map((s) => parseInt(s, 10)) or use Number.

Sorting

sort() without a comparator converts elements to strings and sorts alphabetically, so numbers sort incorrectly:

const nums = [10, 2, 33, 4];
console.log([...nums].sort());                 // [10, 2, 33, 4] (string order)
console.log([...nums].sort((a, b) => a - b));  // [2, 4, 10, 33]

The comparator returns a negative number if a goes first, positive if b goes first, and zero for a tie. Note the copy [...nums]: sorting the original would mutate it, which can break React state. toSorted() returns a sorted copy in current browsers and Node.

3. Objects

An object is a collection of key-value pairs.

const registration = {
  name: "Lena",
  workshop: "Pottery",
  seats: 2,
  paid: true,
  describe() { return `${this.name}: ${this.seats}`; }, // method shorthand
};

registration.name;        // dot access
registration["workshop"]; // bracket access, needed for dynamic keys
Object.keys(registration);   // ["name", "workshop", ...]
Object.values({ a: 1, b: 2 }); // [1, 2]
Object.entries({ a: 1 });      // [["a", 1]]
Object.fromEntries([["a", 1]]);// { a: 1 }

Computed property names let you build keys at runtime: { [workshop]: seats }.

this in short

Inside a method called as obj.method(), this is obj. If you extract the method (const f = obj.describe; f()), this is lost and the call fails or returns undefined values. Arrow functions do not have their own this; they use the one from the surrounding code. This is why arrow functions are convenient for callbacks and why you should not use them as object methods that rely on this.

4. Destructuring, spread, and rest

Destructuring unpacks values into variables:

const [first, , third = 9] = [1, 2];
console.log(first, third); // 1 9  (third uses its default)

const { name, ...rest } = registration;
console.log(name);  // "Lena"
console.log(rest);  // { workshop: "Pottery", seats: 2, paid: true, describe: [Function] }

You can destructure function parameters directly, which React components do constantly:

const label = ({ workshop, seats = 1 }) => `${workshop} x${seats}`;

Spread (...) expands an iterable or object in place:

const a = [1, 2, 3];
const copy = [...a, 4];            // [1, 2, 3, 4], a is unchanged
const merged = { ...registration, paid: false }; // later keys win

The same three dots mean rest when collecting (in parameters or the left side of destructuring) and spread when expanding (in calls, array literals, and object literals).

5. Immutable updates

Arrays and objects are assigned by reference. Two names can point at the same array:

const a = [1, 2, 3];
const b = a;
b.push(4);
console.log(a); // [1, 2, 3, 4]  a changed too

To change data without mutation, create a new value:

Goal Mutating Immutable
Add item arr.push(x) [...arr, x]
Remove item arr.splice(i, 1) arr.filter((_, idx) => idx !== i)
Replace item arr[i] = x arr.map((v, idx) => (idx === i ? x : v))
Change property obj.k = v { ...obj, k: v }
Sort arr.sort(fn) [...arr].sort(fn) or arr.toSorted(fn)

Updating one record in a list:

const users = [{ id: 1, n: "a" }, { id: 2, n: "b" }];
const updated = users.map((x) => (x.id === 2 ? { ...x, n: "B" } : x));
console.log(updated); // [{ id: 1, n: "a" }, { id: 2, n: "B" }]
console.log(users[1].n); // "b" (original untouched)

Shallow versus deep copies

Spread and slice make shallow copies: nested objects are still shared.

const o1 = { x: { y: 1 } };
const o2 = { ...o1 };
o2.x.y = 2;
console.log(o1.x.y); // 2  nested object shared!

Use structuredClone(o1) for a deep copy, or better, update only the nested path you change: { ...o1, x: { ...o1.x, y: 2 } }. The second form is the pattern you will use with React state.

6. Worked example: the attendance summary

Task. Given the registrations below, produce one row per workshop with the number of paid seats, sorted by seats descending and then by workshop name.

const regs = [
  { name: "Lena", workshop: "Pottery", seats: 2, paid: true },
  { name: "Omar", workshop: "Weaving", seats: 1, paid: false },
  { name: "Ana",  workshop: "Pottery", seats: 3, paid: true },
  { name: "Ben",  workshop: "Weaving", seats: 4, paid: true },
];

Step 1: keep paid registrations. regs.filter((r) => r.paid) removes Omar.

Step 2: group and total. Use reduce with an object accumulator:

const summary = regs
  .filter((r) => r.paid)
  .reduce(
    (acc, r) => ({ ...acc, [r.workshop]: (acc[r.workshop] ?? 0) + r.seats }),
    {}
  );
console.log(summary); // { Pottery: 5, Weaving: 4 }

Trace: start {}; Lena gives { Pottery: 2 }; Ana gives { Pottery: 5 }; Ben gives { Pottery: 5, Weaving: 4 }. The ?? 0 supplies the starting total for a workshop not yet seen.

Step 3: turn the object into rows and sort.

const rows = Object.entries(summary)
  .map(([workshop, seats]) => ({ workshop, seats }))
  .sort((a, b) => b.seats - a.seats || a.workshop.localeCompare(b.workshop));
console.log(rows);
// [{ workshop: "Pottery", seats: 5 }, { workshop: "Weaving", seats: 4 }]

The comparator uses ||: if the seat difference is 0 (falsy) it falls through to the name comparison.

Why other approaches are weaker. A for loop with summary[w] += seats works, but mutates and fails when the key does not exist yet (undefined + 2 is NaN) unless you guard it. Sorting Object.entries directly without a comparator would sort the pairs as strings. Filtering after grouping would count Omar's unpaid seat in the total.

7. Pitfalls and exam tips

  • Pitfall: sort() with numbers and no comparator. Tip: if a question sorts [10, 2, 33, 4] with no callback, the answer is string order.
  • Pitfall: map callback with a block body and no return.
  • Pitfall: reduce without an initial value on possibly empty arrays.
  • Pitfall: .map(parseInt).
  • Pitfall: mutating state in place (push, sort, direct assignment). In React the screen may not update.
  • Pitfall: believing spread deep-copies.
  • Pitfall: returning an object literal from an arrow without parentheses; () => { ok: true } is a block with a label, returning undefined.
  • Tip: identify whether a method mutates before predicting what the original array contains afterwards.
  • Tip: const arr = []; arr.push(1) is legal; const does not freeze.

Mini case study: the waiting-list bug

Situation. A teammate wrote this to add a person to the waiting list and show the position of each person:

const waiting = ["Lena", "Omar"];

function addToWaiting(list, name) {
  list.push(name);
  return list;
}

const before = waiting;
const after = addToWaiting(waiting, "Ana");
console.log(before === after);      // true
console.log(before.length);         // 3
const labels = after.map((n, i) => { `${i + 1}. ${n}` });
console.log(labels);

The component built on this never re-renders after a person is added, and the labels print as [undefined, undefined, undefined].

Questions. Explain both symptoms and rewrite the code.

Commented solution.

  1. list.push mutates the array that waiting points at, and the function returns the same reference, so before === after is true. React compares references to detect a change; seeing the same array, it concludes nothing changed.
  2. The arrow function in map uses braces without return, so each call returns undefined.
const addToWaiting = (list, name) => [...list, name];

const waiting = ["Lena", "Omar"];
const after = addToWaiting(waiting, "Ana");
console.log(waiting.length);   // 2  original intact
console.log(after.length);     // 3
const labels = after.map((n, i) => `${i + 1}. ${n}`);
console.log(labels); // ["1. Lena", "2. Omar", "3. Ana"]

The new function is pure: same inputs give the same output, and nothing outside is modified. It is also easy to test with two lines of Jest, which you will write in lesson 4.

Key terms

  • Parameter / argument: the name in a definition / the value passed in a call.
  • Callback: a function passed to another function.
  • Higher-order function: takes or returns a function.
  • Closure: a function with access to variables of its creation scope.
  • Pure function: deterministic, with no side effects.
  • Mutation: changing existing data in place.
  • Destructuring: unpacking values into variables.
  • Spread / rest: ... to expand / to collect.
  • Shallow copy: copies the top level only.

Summary checklist

  • I can write a function three ways and know the arrow-function return rules.
  • I use default and rest parameters correctly.
  • I know which array methods mutate.
  • I can write map, filter, and reduce (with an initial value) from memory.
  • I always give sort a comparator for numbers and copy before sorting.
  • I read and write destructuring and spread.
  • I update arrays and objects immutably, including one item inside a list.
  • I can explain shallow copies and when structuredClone is needed.
  • I can explain a closure with a counter example.

Official sources