Module 1: Getting Started
The JavaScript You Need Before React
Arrow functions, destructuring, spread and rest, modules and a few array habits that React code uses on every single line.
Most people who struggle with React are not struggling with React. They're struggling with JavaScript they thought they knew. A component is just a function, props are just an object, and state updates are just copying things correctly. If those pieces feel shaky, every later lesson gets harder than it should be.
So this lesson is a targeted refresher. I'm only covering the parts you will type hundreds of times in React code.
Arrow functions#
You already know the syntax. What matters in React is two details.
const double = (n) => n * 2; // implicit return
const makeUser = (name) => ({ name }); // returning an object literal needs parentheses
const greet = (name) => {
const msg = `Hi ${name}`;
return msg; // block body needs an explicit return
};
That object-literal case bites everyone once. Without the parentheses, JavaScript reads the braces as a function body, not an object, and you get undefined back.
The other detail is that arrow functions don't have their own this. They borrow it from the surrounding scope. That's why they're the default choice for event handlers and callbacks in React. You'll see why in the class component lesson.
Destructuring#
Destructuring pulls values out of objects and arrays into variables. In React you use it on props, on state, and on the return value of hooks.
const user = { id: 7, name: "Asha", address: { city: "Pune" } };
const { name, id } = user; // by property name
const { name: userName } = user; // rename while extracting
const { role = "viewer" } = user; // default if undefined
const { address: { city } } = user; // nested
const colors = ["red", "green", "blue"];
const [first, , third] = colors; // by position, skipping one
Object destructuring goes by name. Array destructuring goes by position. That difference is the whole reason useState returns an array: you get to name the pair anything you like, because position is what carries the meaning.
const [count, setCount] = useState(0);
const [name, setName] = useState("");
You can also destructure right in the parameter list, which is how most components receive props:
function Avatar({ src, size = 40 }) {
return <img src={src} width={size} height={size} />;
}
The default only applies when the value is undefined. If someone passes null or 0, the default is ignored. I've watched that cause a "why is my size zero" bug more than once.
Spread and rest#
The ... syntax does two opposite jobs depending on where it sits.
Spread expands something. Rest collects the leftovers.
// Spread: copy and override
const base = { theme: "dark", fontSize: 14 };
const next = { ...base, fontSize: 16 }; // { theme: "dark", fontSize: 16 }
const nums = [1, 2, 3];
const more = [...nums, 4]; // [1, 2, 3, 4]
// Rest: collect what's left
const { id, ...others } = { id: 1, a: 2, b: 3 }; // others = { a: 2, b: 3 }
const [head, ...tail] = [10, 20, 30]; // tail = [20, 30]
Spread is how you update state without mutating it, which is the single most repeated move in React. Hold onto that thought for module 3.
One thing to be careful about: spread makes a shallow copy. Only the top level is new. Anything nested is still the same object in memory.
const a = { name: "Asha", address: { city: "Pune" } };
const b = { ...a };
a ──► { name, address ─┐ }
├──► { city: "Pune" } (one shared object)
b ──► { name, address ─┘ }
b.address.city = "Delhi" // changes what a sees too
If you ever update a nested value by mutating it after a spread, you've fooled yourself into thinking you made a copy. You didn't.
Modules#
Every file you write in a React project is a module with its own scope. Nothing is global unless you export it and someone imports it.
// math.js
export const PI = 3.14159; // named export
export function square(n) { return n * n; }
export default function cube(n) { return n ** 3; } // one default per file
// app.js
import cube, { PI, square } from "./math.js";
import * as math from "./math.js";
A rule of thumb that has served me well: components get a default export, everything else gets a named export. Named exports are easier to rename and autocomplete safely. Default exports let whoever imports them pick any name, which is how you end up with the same component called Header in one file and TopBar in another.
Also: relative paths need ./ or ../. A bare name like "react" means "look in node_modules".
Array methods you'll use constantly#
React renders lists by transforming data into elements, so these three show up everywhere.
const products = [
{ id: 1, name: "Pen", price: 20, inStock: true },
{ id: 2, name: "Book", price: 250, inStock: false },
{ id: 3, name: "Bag", price: 900, inStock: true },
];
products.map((p) => p.name); // ["Pen", "Book", "Bag"] (same length)
products.filter((p) => p.inStock); // only the matching items
products.reduce((sum, p) => sum + p.price, 0); // 1170 (collapse to one value)
products.find((p) => p.id === 2); // first match, or undefined
None of these change the original array. That's not an accident, it's the style React expects. Be wary of sort, reverse and splice, which do mutate. Copy first with [...arr].sort(), or use toSorted(), which is available in current browsers and Node.
Optional chaining, nullish coalescing and truthiness#
const city = user?.address?.city; // undefined instead of a crash
const label = input ?? "Untitled"; // fallback only for null or undefined
const label2 = input || "Untitled"; // fallback for any falsy value, including "" and 0
?? and || look similar and behave differently. If 0 or an empty string is a legitimate value, || will throw it away. This matters a lot in JSX, because {count && <Badge />} renders a literal 0 on screen when count is zero. We'll hit that again in the conditional rendering lesson.
Closures#
A function remembers the variables that were around when it was created. That's a closure, and it's the reason React has the concept of a "stale value".
function makeCounter() {
let n = 0;
return () => ++n;
}
const next = makeCounter();
next(); // 1
next(); // 2
Every time React renders a component, it creates new functions that close over that render's values. When you later wonder why a click handler sees an old number, this is the cause. Keep it in the back of your mind.
Where people get burned#
- Mutating arrays inside
mapor after spreading, then wondering why nothing updates. - Using
==instead of===. Don't use==. - Forgetting that
asyncfunctions always return a promise, so you can't use their result directly. - Importing a default export with curly braces, or the other way around. The error message is confusing the first few times.
Practice#
- Given
const todo = { id: 1, text: "Buy milk", done: false, tags: ["home"] }, write one line that creates a new todo withdone: truewithout touching the original. - Write a function that takes the
productsarray above and returns the names of in-stock items sorted alphabetically, without changingproducts. - Make a shallow copy of an object with a nested array, push to the nested array on the copy, and print the original. Then write the version that really leaves the original untouched.
- Create two files, one exporting a named function and one default export, and import both into a third.