JavaScript & React Gotchas Cheatsheet
A scannable reference of the JavaScript quirks and React gotchas that trip up real codebases. Each entry pairs a minimal code example with a one-line takeaway so you can refresh your mental model in seconds.
JavaScript & React Gotchas Cheatsheet
This is the cheatsheet I wish I had during code reviews and 1 AM debugging sessions. Every entry is built the same way: a tiny code example, then a one-line takeaway. Scan it top to bottom when you need a refresher, or jump to whichever section is biting you today.
JavaScript Concepts
Scope & Variables
var vs let vs const
function f() {
if (true) { var a = 1; let b = 2; }
console.log(a); // 1 (var leaks)
console.log(b); // ReferenceError (let is block-scoped)
}
Takeaway: var is function-scoped; let/const are block-scoped.
Hoisting
console.log(x); // undefined (declaration hoisted, not assignment)
var x = 5;
console.log(y); // ReferenceError (temporal dead zone)
let y = 5;
Takeaway: var hoists as undefined; let/const exist but are unreachable until declared (TDZ).
Function vs Function Expression Hoisting
foo(); // works
function foo() {}
bar(); // TypeError: bar is not a function
var bar = function() {};
Takeaway: Function declarations hoist fully; function expressions only hoist the variable.
Variable Shadowing
const x = 1;
function f() {
const x = 2; // shadows outer x
console.log(x); // 2
}
Takeaway: Inner scope hides outer name. Useful for parameter renaming, dangerous when accidental.
Temporal Dead Zone (TDZ)
{
console.log(a); // ReferenceError
let a = 1;
}
Takeaway: Between block entry and let/const declaration, the variable exists but throws on access.
Closures
Basic Closure
function counter() {
let n = 0;
return () => ++n;
}
const c = counter();
c(); c(); // 2
Takeaway: Inner function "remembers" outer variables even after outer function returns.
Loop Closure Classic
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
// 3 3 3
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
// 0 1 2
Takeaway: var shares one binding; let creates a new binding per iteration.
Data Privacy
function makeAcct(bal) {
return { deposit: n => bal += n, get: () => bal };
}
Takeaway: Closures give private state without classes.
this Binding
Implicit vs Explicit
const obj = { x: 1, f() { return this.x; } };
obj.f(); // 1 (implicit)
const g = obj.f;
g(); // undefined (lost context)
g.call(obj); // 1 (explicit)
Takeaway: this is determined by how a function is called, not where defined.
Arrow Functions
const obj = {
x: 1,
f: () => this.x, // wrong: `this` is outer
g() { return () => this.x; } // right: arrow inherits g's this
};
Takeaway: Arrow functions don't have their own this; they inherit from enclosing scope.
Class Method Loss
class C { constructor() { this.x = 1; } f() { return this.x; } }
const c = new C();
const f = c.f;
f(); // TypeError
Takeaway: Detaching a method loses this. Use .bind, class fields, or arrow methods.
Equality & Coercion
== vs ===
0 == ''; // true
0 == false; // true
null == undefined; // true
[] == false; // true
0 === ''; // false
Takeaway: Always use === unless you specifically want coercion.
NaN
NaN === NaN; // false
Number.isNaN(NaN); // true
Takeaway: NaN is never equal to itself. Use Number.isNaN.
Object Equality
{} === {}; // false
[1] === [1]; // false
Takeaway: Objects compared by reference, not structure.
Truthy / Falsy
// Falsy: false, 0, -0, 0n, '', null, undefined, NaN
// Everything else is truthy, including [] and {}
if ([]) console.log('truthy'); // logs
Reference vs Value
Primitives vs Objects
let a = 1, b = a; b = 2; console.log(a); // 1
let x = {n:1}, y = x; y.n = 2; console.log(x.n); // 2
Takeaway: Primitives copied by value; objects copied by reference.
Function Argument Passing
function f(o) { o.n = 99; }
const obj = {n: 1};
f(obj); // obj.n is now 99
Takeaway: "Pass by value of the reference". Mutating contents affects the caller; reassigning doesn't.
Shallow vs Deep Copy
const a = { nested: { x: 1 } };
const b = { ...a }; // shallow
b.nested.x = 99; // also changes a.nested.x
const c = structuredClone(a); // deep
Array & Object Quirks
Mutating vs Non-Mutating Array Methods
// Mutate: push, pop, shift, unshift, splice, sort, reverse
// Return new: map, filter, slice, concat, toSorted, toReversed
[3,1,2].sort(); // mutates
[3,1,2].toSorted(); // returns new
Spread Pitfall
const a = [{x:1}], b = [...a];
b[0].x = 99; // a[0].x also 99 (shallow)
Destructuring Defaults
const { a = 1 } = { a: undefined }; // 1
const { a = 1 } = { a: null }; // null (default only on undefined)
Object Key Order
Object.keys({2:'a', 1:'b', x:'c'}); // ['1','2','x']
Takeaway: Integer-like keys sort numerically first, then insertion order.
Async & Event Loop
Microtasks vs Macrotasks
setTimeout(() => console.log('macro'), 0);
Promise.resolve().then(() => console.log('micro'));
console.log('sync');
// sync → micro → macro
Takeaway: Microtasks (promises) drain fully between each macrotask.
Promise Chaining
Promise.resolve(1)
.then(x => x + 1)
.then(x => Promise.resolve(x + 1))
.then(console.log); // 3
Async/Await Error Handling
async function f() {
try { await mayFail(); }
catch (e) { /* handles both sync throws and rejections */ }
}
Sequential vs Parallel
// Sequential: slow
const a = await fetchA();
const b = await fetchB();
// Parallel: fast
const [a, b] = await Promise.all([fetchA(), fetchB()]);
forEach + async ≠ awaited
[1,2,3].forEach(async x => await save(x)); // does NOT wait
for (const x of [1,2,3]) await save(x); // does
Unhandled Rejections
Promise.reject('x'); // unhandled if no .catch
Takeaway: Every promise chain needs terminal error handling.
Prototypes & Classes
Prototype Chain
const a = {};
a.toString; // inherited from Object.prototype
Object.getPrototypeOf(a) === Object.prototype; // true
Class Field vs Method
class C {
arrow = () => this; // bound per instance
method() { return this; } // not bound
}
Other Sharp Edges
typeof Quirks
typeof null; // 'object' (historical bug)
typeof []; // 'object'
typeof NaN; // 'number'
typeof undefined; // 'undefined'
typeof (() => {}); // 'function'
Array.length Mutation
const a = [1,2,3]; a.length = 1; // a is now [1]
Number Precision
0.1 + 0.2 === 0.3; // false (0.30000000000000004)
Default Parameters Evaluated Each Call
function f(x = []) { x.push(1); return x; }
f(); f(); // each call gets new []
IIFE
(function() { /* private scope */ })();
React Gotchas
Rendering Model
Renders Are Snapshots
function App() {
const [n, setN] = useState(0);
const handle = () => {
setN(n + 1);
setN(n + 1); // both see n=0; final n=1, not 2
};
}
Takeaway: Within one render, state values are frozen. Use updater form for sequential updates.
Updater Form
setN(prev => prev + 1);
setN(prev => prev + 1); // final n=2
Takeaway: Functional updater always sees latest pending state.
State Updates Are Batched
setA(1); setB(2); // one re-render, not two
Takeaway: React batches updates within event handlers (and in React 18+, everywhere).
Stale Closures
Stale State in Timeout
function App() {
const [n, setN] = useState(0);
const delayed = () => setTimeout(() => console.log(n), 3000);
// logs n from the render when delayed was created
}
Takeaway: Closures capture render-time values. Use refs or updater form for "latest."
Stale Effect
useEffect(() => {
const id = setInterval(() => setN(n + 1), 1000); // always n=0+1
return () => clearInterval(id);
}, []); // missing dep
Fix: setN(prev => prev + 1) or include n in deps.
useState
Initializer Runs Every Render
useState(expensiveCalc()); // runs every render
useState(() => expensiveCalc()); // runs once
Direct Mutation Doesn't Trigger Re-render
const [arr, setArr] = useState([1,2]);
arr.push(3); setArr(arr); // same reference → no re-render
setArr([...arr, 3]); // new reference → re-render
Object State Replace, Not Merge
const [s, setS] = useState({a:1, b:2});
setS({a: 99}); // b is gone (unlike class setState)
setS(prev => ({...prev, a: 99})); // correct
useEffect
Missing Dependencies
useEffect(() => { fetch(`/api/${id}`); }, []); // stale id
useEffect(() => { fetch(`/api/${id}`); }, [id]); // correct
Infinite Loop
useEffect(() => { setN(n + 1); }); // no deps → re-renders forever
Object/Function in Deps
useEffect(() => {}, [{x:1}]); // new ref every render → fires every render
Fix: Memoize with useMemo/useCallback or use primitive deps.
Cleanup Order
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // runs before next effect AND on unmount
}, [tick]);
Async in Effect
useEffect(() => {
let cancelled = false;
(async () => {
const data = await fetchData();
if (!cancelled) setData(data);
})();
return () => { cancelled = true; };
}, []);
Takeaway: Can't make the effect callback itself async; wrap an inner function. Handle race conditions with a cancel flag.
Strict Mode Double Invocation
// In dev with StrictMode, effects run → cleanup → run again
// to surface bugs. Make effects idempotent.
Rules of Hooks
Conditional Hooks
if (x) useState(0); // ERROR. Hooks must run in same order every render
Hooks in Loops
items.forEach(() => useState(0)); // ERROR
Takeaway: Always call hooks at the top level, in the same order.
Refs
useRef vs useState
const r = useRef(0); r.current = 5; // no re-render
const [n, setN] = useState(0); setN(5); // re-render
Takeaway: Use refs for values that shouldn't trigger render (DOM nodes, mutable instance vars).
Reading Ref During Render
function App() {
const r = useRef(0);
return <div>{r.current}</div>; // generally don't. Value isn't a render signal
}
Memoization
useCallback Without Stable Deps
const fn = useCallback(() => {}, [obj]); // obj is new each render → useless
useMemo Doesn't Guarantee Caching
// React may discard memo cache; don't rely on it for correctness, only perf.
React.memo + New Props Every Render
<Child style={{color:'red'}} /> // new object → memo bypassed
Lists & Keys
Index as Key
items.map((x, i) => <Item key={i} />); // breaks on reorder/insert
items.map(x => <Item key={x.id} />); // stable identity
Missing Key
items.map(x => <li>{x}</li>); // warning + reconciliation bugs
Events & Handlers
Call vs Reference
<button onClick={handle}> // correct: pass function
<button onClick={handle()}> // wrong: invokes on render
<button onClick={() => handle(id)}> // wrap when passing args
Synthetic Event Pooling (pre-React 17)
// Old React reused event objects; don't access async. Fixed in React 17+.
Forms
Controlled vs Uncontrolled
<input value={v} onChange={e => setV(e.target.value)} /> // controlled
<input defaultValue="x" ref={r} /> // uncontrolled
Takeaway: Don't mix; set value once or always.
Controlled with Undefined
<input value={undefined} /> // becomes uncontrolled (warning)
<input value={v ?? ''} /> // safe
Context
Re-renders on Provider Value Change
<Ctx.Provider value={{x:1}}> // new object each render → all consumers re-render
<Ctx.Provider value={memoVal}> // memoize
Splitting Contexts
Takeaway: Separate frequently-changing values from stable ones into different contexts.
Common Patterns
Derived State Anti-pattern
// Don't store derived data in state
const [items, setItems] = useState([]);
const [count, setCount] = useState(0); // bad. Derive instead: items.length
Lifting State Up
// When two siblings need the same data, move it to their common parent.
Key to Reset Component
<Form key={userId} /> // changing key remounts; resets internal state
Don't Set State in Render
function App() {
setN(1); // infinite loop
return <div />;
}
Performance Pitfalls
Anonymous Functions in Props
<Child onClick={() => doX()} /> // new ref each render
Takeaway: Only matters if Child is memo'd or it's in deps somewhere.
Wrapping Everything in useMemo
Takeaway: Memoization has cost. Profile before optimizing.
Master Mental Models
The Three Big Ideas
1. Renders Are Snapshots
Every render produces a fresh set of variables and a fresh JSX tree. Your event handler belongs to one specific snapshot.
2. Functions Carry Backpacks (Closures)
A function "remembers" the variables in scope at the moment it was created, not when it's called.
3. State Updates Are Scheduled
setState does not change state immediately; it queues a new render with the new value.
Diagnostic Questions
For Any Weird React Bug
- Which render created this function?
- Do I need the latest state or the snapshot value?
- Are my dep array values referentially stable?
For Any Weird JS Bug
- What scope is this variable in?
- Is this primitive (copy) or reference (shared)?
- When does this code actually run (sync vs micro vs macro)?
Final Thoughts
Most "weird" bugs in JavaScript and React are not weird. They are predictable consequences of the language's scoping rules, the event loop, and React's rendering model. Once those three feel intuitive, the gotchas above stop being traps and start being signals that point straight at the cause.
Bookmark this page, scan it before a tricky review, and keep building.