React in 2026 is written with modern ES6+ JavaScript everywhere — arrow functions for callbacks, destructuring for props, spread for state updates, template literals for class names, async/await for data fetching. If you only know older JavaScript (or only Python, Java, etc.), the React code you’ll meet will look like a different language. This reference fixes that.
Every section has a hands-on example you can paste straight into the browser DevTools Console (Cmd + Option + J on macOS — you’ll meet it in detail in Browser DevTools Tour). Don’t just read — type the examples, change values, see what breaks. That’s how the syntax becomes reflexive.
1. let, const, and Why We Never Use var
Older JavaScript declared variables with var. Modern JavaScript uses let and const.
let count = 0; // can be reassigned later
const name = "Alex"; // cannot be reassigned
count = 1; // ✅
name = "Sam"; // ❌ TypeError: Assignment to constant variable.
Two rules to internalize:
- Use
constby default. Only reach forletwhen you know the variable will be reassigned. This makes code easier to read — you seeconstand know the binding is stable. - Never use
var. It has confusing scope rules (function-scoped instead of block-scoped) that lead to bugs.
const doesn’t mean “deeply immutable.” You can still mutate the contents of a const object or array:
const user = { name: "Alex" };
user.name = "Sam"; // ✅ allowed — we're mutating the object, not rebinding `user`
const numbers = [1, 2, 3];
numbers.push(4); // ✅ allowed for the same reason
What const prevents is rebinding — making user point at a different object entirely.
Why this matters in React: Every React component uses const for everything by default — props are const, derived values are const, even state hooks return const arrays. Reassigning a const is a clear signal that something is wrong.
2. Arrow Functions
Arrow functions are a concise syntax for writing functions using =>. They’re used everywhere in React — every callback, every event handler, and most function components.
// Traditional function expression:
const greetOld = function(name) {
return "Hello, " + name + "!";
};
// Equivalent arrow function:
const greetNew = (name) => "Hello, " + name + "!";
console.log(greetOld("Alice")); // "Hello, Alice!"
console.log(greetNew("Bob")); // "Hello, Bob!"
Four things to know:
- Concise syntax. No
functionkeyword. - Implicit return for single-expression bodies.
n => n * 2is the same as(n) => { return n * 2; }. - Parentheses around parameters are optional with exactly one parameter (
n => n * 2). Required for zero or multiple (() => 42,(a, b) => a + b). - No
thisof their own. Arrow functions inheritthisfrom the surrounding scope. This matters less in modern React (function components don’t usethis), but it’s why arrow functions are safer in callbacks.
Multi-line arrow function — needs explicit braces and return:
const greet = (name) => {
const greeting = "Hello, " + name;
return greeting + "!";
};
Why this matters in React: Every inline event handler is an arrow function: <button onClick={() => setCount(count + 1)}>. Every .map callback that renders a list of components. Every async data-fetch inside useEffect. You’ll write hundreds.
3. Template Literals
Template literals are strings wrapped in backticks (`) instead of quotes. They support interpolation (variables inside ${}) and multi-line strings without escape characters.
const user = "Dana";
const points = 120;
console.log(`${user} has scored ${points} points this week.`);
// Dana has scored 120 points this week.
// Multi-line:
const msg = `Hello, ${user}!
Your total: ${points}.
Keep going.`;
console.log(msg);
Inside ${} you can write any expression — variables, function calls, arithmetic, conditionals:
console.log(`Doubled: ${points * 2}`);
console.log(`Status: ${points >= 100 ? "champion" : "rising"}`);
Why this matters in React: Class names built from props (`btn btn--${variant}`), API URLs (`${API_BASE}/users/${userId}`), aria-labels, error messages, console.log for debugging. The old "..." + variable + "..." pattern is dead.
4. Destructuring — Objects and Arrays
Destructuring lets you unpack values from objects or arrays into named variables in one line. It’s used constantly in React, especially for component props and Hook return values.
Object destructuring
const player = { name: "Sam", score: 5, level: 10 };
const { name, score } = player;
console.log(name); // "Sam"
console.log(score); // 5
The curly-brace pattern on the left mirrors the shape of the object on the right.
Renaming when you want a different variable name:
const { name: playerName } = player;
console.log(playerName); // "Sam"
Default values when a property might be missing:
const { score, rank = "Rookie" } = player;
console.log(rank); // "Rookie" — player has no `rank`
In function parameters — extremely common in React:
function Profile({ name, age }) {
// `name` and `age` are pulled out of the props object automatically
return `${name}, ${age}`;
}
Array destructuring
Same idea but with brackets [], destructuring by position:
const colors = ["red", "green", "blue"];
const [primary, secondary] = colors;
console.log(primary); // "red"
console.log(secondary); // "green"
// Skip elements with empty slots:
const [, , third] = colors;
console.log(third); // "blue"
Why this matters in React: Every React Hook returns a value (or an array) you destructure. The most common one:
const [count, setCount] = useState(0);
// ^-- the value ^-- the updater function
And every functional component destructures props in its parameter list:
function Button({ label, onClick, disabled }) { /* ... */ }
Without destructuring, that’s function Button(props) { /* props.label, props.onClick everywhere */ }. Painful.
5. Spread (...) and Rest (...)
The ... syntax does two opposite things depending on where it appears.
Spread — expand an array or object into individual pieces
Combining arrays:
const nums1 = [10, 20, 30];
const nums2 = [40, 50];
const combined = [...nums1, ...nums2];
console.log(combined); // [10, 20, 30, 40, 50]
Copying an array (shallow):
const copy = [...combined];
Merging objects:
const base = { color: "red", size: "M" };
const update = { size: "L", price: 25 };
const merged = { ...base, ...update };
console.log(merged); // { color: "red", size: "L", price: 25 }
When keys conflict, later properties win — size: "L" from update overrode size: "M" from base.
Spreading into function arguments:
const numbers = [1, 5, 3, 8, 2];
console.log(Math.max(...numbers)); // 8
Rest — collect remaining values into an array or object
In function parameters:
function announce(greeting, ...names) {
return `${greeting} ${names.join(", ")}!`;
}
console.log(announce("Hello", "Alice", "Bob", "Charlie"));
// "Hello Alice, Bob, Charlie!"
In array destructuring:
const [first, ...rest] = [10, 20, 30, 40];
console.log(first); // 10
console.log(rest); // [20, 30, 40]
In object destructuring:
const { name, ...other } = { name: "Alex", age: 25, city: "Austin" };
console.log(name); // "Alex"
console.log(other); // { age: 25, city: "Austin" }
Why this matters in React: Spread is the immutable-update pattern. React state must not be mutated, so to “update” an object or array, you spread the old one and overlay the new bits:
setUser({ ...user, name: "Sam" }); // update one field
setItems([...items, newItem]); // append to a list
setItems(items.filter(i => i.id !== removed)); // remove from a list
Forgetting this pattern is the #1 source of “my component isn’t re-rendering” bugs in React.
6. Array Methods — .map, .filter, .reduce, and friends
JavaScript arrays come with a handful of methods that take a callback and return a new array (immutably). The ones you’ll use weekly:
const numbers = [1, 2, 3, 4, 5];
numbers.map(n => n * 2); // [2, 4, 6, 8, 10]
numbers.filter(n => n % 2 === 0); // [2, 4]
numbers.find(n => n > 3); // 4 (first match, not an array)
numbers.some(n => n > 10); // false
numbers.every(n => n > 0); // true
numbers.includes(3); // true
numbers.reduce((sum, n) => sum + n, 0); // 15
These methods do not mutate the original array — they return a new one. The original numbers is unchanged.
Chaining:
const evenSquaresOver10 = numbers
.filter(n => n % 2 === 0)
.map(n => n * n)
.filter(sq => sq > 10);
console.log(evenSquaresOver10); // [16]
.forEach vs .map: .forEach returns nothing — use it only for side effects (logging, mutating an external counter). .map returns a new array — use it for transformation. In React you almost always want .map.
Why this matters in React: .map is how you turn a list of data into a list of JSX components:
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
.filter is how you implement search and visibility toggles. .find is how you look up the currently selected item. .reduce is how you compute totals.
7. Optional Chaining (?.) and Nullish Coalescing (??)
Two small operators that prevent a whole class of “Cannot read property ‘X’ of undefined” bugs.
Optional chaining ?.
Read a nested property without crashing if any intermediate value is null or undefined:
const user = { profile: { name: "Alex" } };
console.log(user.profile.name); // "Alex"
console.log(user.account?.email); // undefined (no crash — user.account is undefined)
Without ?., the second line would throw TypeError: Cannot read properties of undefined (reading 'email').
Works with function calls and array indexing too:
user.greet?.(); // calls greet() if it exists, otherwise undefined
items?.[0]?.name; // first item's name, or undefined if no items
Nullish coalescing ??
Provide a default only when the left side is null or undefined (not for 0, "", or false):
null ?? "default"; // "default"
0 ?? "default"; // 0 (0 is not null/undefined)
"" ?? "default"; // "" (empty string is not null/undefined)
undefined ?? "default"; // "default"
This is different from || (logical OR), which falls back on any falsy value — and bites you when 0 or "" is a valid value:
const score = 0 || 100; // 100 — WRONG if 0 is a valid score
const score2 = 0 ?? 100; // 0 — RIGHT
Why this matters in React: Component props are often optional, API responses often have missing fields. user?.name ?? "Guest" is the safe-and-clean pattern:
<h1>Welcome, {user?.name ?? "Guest"}</h1>
8. Async / Await and Promises
Most React data fetching uses Promises (values that may be available later) and the async/await syntax that makes Promises read like normal sequential code.
The basics
A function declared async automatically returns a Promise. Inside an async function, await pauses execution until a Promise resolves:
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
getUser(123).then(user => console.log(user));
You can only use await inside an async function. Otherwise the keyword is a syntax error.
Error handling with try/catch
async function getUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
console.error("Failed to load user:", error);
return null;
}
}
Running things in parallel — Promise.all
const [user, posts, comments] = await Promise.all([
fetch("/api/user").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/comments").then(r => r.json()),
]);
All three requests fire at the same time; await waits for the slowest one. Compare to sequential awaits, which fire one at a time and add up to roughly the sum of latencies.
Why this matters in React: Every API call your capstone makes — to your own backend, to Anthropic, to Tavily, to ElevenLabs — is an async function. You’ll write dozens. The Week 1 pattern is const data = await fetch(...).then(r => r.json()) inside a useEffect or a TanStack Query function.
9. Modules — import and export
Modern JavaScript splits code across files. Each file is a module; you export things from one file and import them in another.
Named exports
// utils.js
export function formatDate(d) { return d.toISOString(); }
export const MAX_RETRIES = 3;
// app.js
import { formatDate, MAX_RETRIES } from "./utils";
Default exports
// Button.jsx
export default function Button({ label }) {
return <button>{label}</button>;
}
// app.jsx
import Button from "./Button"; // no braces — default import
A file can have one default export and any number of named exports. React components are usually default-exported; utility functions and constants are usually named-exported.
Renaming on import
import { formatDate as fmt } from "./utils";
Why this matters in React: Every React component lives in its own file. Every project has dozens of imports per file — components, hooks, utilities, third-party libraries. Knowing the difference between default and named imports prevents an hour of “why is Button is not a constructor” debugging.
Practice exercises
Open the browser DevTools Console (Cmd + Option + J on Chrome). Paste each block. Predict what prints before pressing Enter.
// 1. Arrow function with implicit return
const square = n => n * n;
console.log(square(7)); // ?
// 2. Destructuring with rename + default
const settings = { theme: "dark" };
const { theme, fontSize = 14 } = settings;
console.log(theme, fontSize); // ?
// 3. Spread for immutable update
const cart = ["apple", "bread"];
const newCart = [...cart, "milk"];
console.log(cart); // ?
console.log(newCart); // ?
// 4. Chained array methods
const transactions = [
{ type: "credit", amount: 100 },
{ type: "debit", amount: 30 },
{ type: "credit", amount: 50 },
{ type: "debit", amount: 20 },
];
const totalCredits = transactions
.filter(t => t.type === "credit")
.reduce((sum, t) => sum + t.amount, 0);
console.log(totalCredits); // ?
// 5. Optional chaining + nullish coalescing
const user = { name: "Alex" };
const city = user.address?.city ?? "Unknown";
console.log(city); // ?
// 6. Async/await
async function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
(async () => {
console.log("A");
await delay(500);
console.log("B");
})();
Run #6 and notice the half-second pause before "B" appears. That’s await in action.
Cheat sheet — keep this open while you code
| Concept | Syntax |
|---|---|
| Block-scoped const | const x = 5; |
| Block-scoped let | let x = 5; x = 10; |
| Arrow function | const f = (x) => x + 1; |
| Implicit return | n => n * 2 |
| Template literal | `${name} scored ${pts}` |
| Object destructure | const { name, age } = user; |
| Array destructure | const [first, second] = arr; |
| Rename + default | const { a: alias = 0 } = obj; |
| Spread array | [...arr1, ...arr2] |
| Spread object | { ...obj1, ...obj2 } |
| Rest in params | function f(a, ...rest) { } |
| Map | arr.map(x => x * 2) |
| Filter | arr.filter(x => x > 0) |
| Reduce | arr.reduce((a, b) => a + b, 0) |
| Optional chain | user?.address?.city |
| Nullish coalesce | value ?? "default" |
| Async function | async function f() { ... } |
| Await | const data = await fetch(url); |
| Promise.all | await Promise.all([p1, p2]) |
| Named import | import { x } from "./y"; |
| Default import | import X from "./y"; |
What’s next
You’ve now seen the JavaScript surface that 90% of React code sits on. The next module — TypeScript Quick Reference — layers a type system on top of everything you just learned. Same syntax, plus types after the colon: const name: string = "Alex".
When you’re ready, post in #wins on Discord: “Module 0.6 complete.” See you in the next module.