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(); // 2Takeaway: 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 2Takeaway: 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(); // TypeErrorTakeaway: 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 === ''; // falseTakeaway: Always use === unless you specifically want coercion.
NaN
NaN === NaN; // false
Number.isNaN(NaN); // trueTakeaway: NaN is never equal to itself. Use Number.isNaN.
Object Equality
{} === {}; // false
[1] === [1]; // falseTakeaway: 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'); // logsReference 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); // 2Takeaway: 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 99Takeaway: "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); // deepArray & 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 newSpread 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 → macroTakeaway: 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); // 3Async/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); // doesUnhandled Rejections
Promise.reject('x'); // unhandled if no .catchTakeaway: Every promise chain needs terminal error handling.
Prototypes & Classes
Prototype Chain
const a = {};
a.toString; // inherited from Object.prototype
Object.getPrototypeOf(a) === Object.prototype; // trueClass 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=2Takeaway: Functional updater always sees latest pending state.
State Updates Are Batched
setA(1); setB(2); // one re-render, not twoTakeaway: 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 depFix: setN(prev => prev + 1) or include n in deps.
useState
Initializer Runs Every Render
useState(expensiveCalc()); // runs every render
useState(() => expensiveCalc()); // runs onceDirect 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-renderObject 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})); // correctuseEffect
Missing Dependencies
useEffect(() => { fetch(`/api/${id}`); }, []); // stale id
useEffect(() => { fetch(`/api/${id}`); }, [id]); // correctInfinite Loop
useEffect(() => { setN(n + 1); }); // no deps → re-renders foreverObject/Function in Deps
useEffect(() => {}, [{x:1}]); // new ref every render → fires every renderFix: 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 renderHooks in Loops
items.forEach(() => useState(0)); // ERRORTakeaway: 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-renderTakeaway: 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 → uselessuseMemo 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 bypassedLists & Keys
Index as Key
items.map((x, i) => <Item key={i} />); // breaks on reorder/insert
items.map(x => <Item key={x.id} />); // stable identityMissing Key
items.map(x => <li>{x}</li>); // warning + reconciliation bugsEvents & Handlers
Call vs Reference
<button onClick={handle}> // correct: pass function
<button onClick={handle()}> // wrong: invokes on render
<button onClick={() => handle(id)}> // wrap when passing argsSynthetic 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} /> // uncontrolledTakeaway: Don't mix; set value once or always.
Controlled with Undefined
<input value={undefined} /> // becomes uncontrolled (warning)
<input value={v ?? ''} /> // safeContext
Re-renders on Provider Value Change
<Ctx.Provider value={{x:1}}> // new object each render → all consumers re-render
<Ctx.Provider value={memoVal}> // memoizeSplitting 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.lengthLifting 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 stateDon'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 renderTakeaway: 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.