React Reference Notes (Parts 1–8)
Part 1 — Fundamentals of React
Mental model:
UI = f(state). State changes → re-render → diff virtual DOM → minimal real DOM updates.
What React Is (and Isn't)
React is a view library, not a framework. It handles rendering and state. Everything else (routing, data fetching, forms) you bring yourself.
| Concept | Meaning |
|---|---|
| Declarative UI | Describe what to render, not how to do it step-by-step |
| Component-based | UI = tree of isolated, reusable pieces |
| Unidirectional data flow | Data moves parent → child via props only |
| Virtual DOM | In-memory representation; React diffs it and patches only what changed |
Setup (Vite — use this, not CRA)
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev # dev server — NOT production-ready
npm run build # optimized production build
npm run preview # serve the production build locallyJSX
JSX = JavaScript + HTML syntax sugar. Transpiled by Babel to React.createElement() calls.
const name = 'Nishanth';
const element = <h1>{name}</h1>; // {} embeds any JS expression
// Must have one root — use Fragment to avoid extra DOM nodes
return (
<>
<Header />
<Main />
</>
);HTML → JSX attribute differences:
| HTML | JSX |
|---|---|
class |
className |
for |
htmlFor |
onclick |
onClick |
tabindex |
tabIndex |
Rendering Lists
// ✅ Good — stable unique key
items.map(item => <li key={item.id}>{item.name}</li>)
// ❌ Bad — index as key breaks reordering
items.map((item, index) => <li key={index}>{item}</li>)Keys must be unique among siblings. They help React identify which items changed, were added, or were removed. Array index keys break when the list reorders.
Entry Point
// main.jsx
ReactDOM.createRoot(document.getElementById('root')).render(<App />);Key Pitfalls
- JSX is not HTML —
class→className,for→htmlFor. - Fragment (
<>) avoids unnecessary wrapperdivs. - Never use array index as key if list can reorder or filter.
- JSX must return one root element.
Part 2 — Components and Props
Props are immutable inputs. If a child needs to change something in the parent, pass a callback down.
Function Components
// Named function (prefer for debugging)
function Welcome({ name }) {
return <h2>Hello {name}</h2>;
}
// Arrow function
const Welcome = ({ name }) => <h2>Hello {name}</h2>;Props
// Parent passes data
<UserCard name="Nishanth" role="Engineer" />
// Child receives via destructuring
function UserCard({ name, role }) {
return <p>{name} — {role}</p>;
}Props are read-only. Never mutate them:
// ❌ Never do this
function Profile(props) {
props.name = 'Changed'; // mutation — will cause bugs
}Child → Parent Communication
Pass a callback function as a prop:
function Child({ onAction }) {
return <button onClick={onAction}>Click Me</button>;
}
function Parent() {
const handleAction = () => console.log('child triggered this');
return <Child onAction={handleAction} />;
}Spread and Rest Props
// Spread — pass all props of an object
const user = { name: 'Nishanth', role: 'Engineer' };
<UserCard {...user} />
// Rest — capture remaining props
function Card({ title, ...rest }) {
return <div {...rest}>{title}</div>; // rest forwarded to div
}Default Props
function Button({ label = 'Submit', size = 'md' }) {
return <button className={`btn-${size}`}>{label}</button>;
}Prop Types (runtime validation — use TypeScript in real projects)
import PropTypes from 'prop-types';
Button.propTypes = {
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
};Key Pitfalls
- Props vs state: props are external inputs, state is internal mutable data.
- Child → parent always through callbacks, never by mutating props.
childrenis a special prop — what you put between component tags.
Part 3 — State Management in React
State is data that changes over time and triggers re-renders.
useState
const [count, setCount] = useState(0);
// Functional update — use when new state depends on old state
setCount(prev => prev + 1); // safe in async context
setCount(count + 1); // can be stale in batched updatesRules of Hooks
- Call hooks only at the top level — never inside loops, conditions, or nested functions.
- Call hooks only in React function components or custom hooks.
Lifting State Up
When siblings need to share state, move it to their closest common ancestor:
function App() {
const [query, setQuery] = useState('');
return (
<>
<Search onSearch={setQuery} /> {/* sets state */}
<List query={query} /> {/* reads state */}
</>
);
}Controlled vs Uncontrolled Components
// Controlled — React owns the value
function ControlledInput() {
const [value, setValue] = useState('');
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
// Uncontrolled — DOM owns the value, ref to read it
function UncontrolledForm() {
const inputRef = useRef();
const handleSubmit = e => {
e.preventDefault();
console.log(inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
<button type="submit">Submit</button>
</form>
);
}Prefer controlled components — React has full control of the data.
useReducer — When State Logic Gets Complex
Use over useState when:
- Multiple sub-values are interdependent
- Next state depends on a specific action type
- Logic is complex enough to extract and test separately
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT': return { count: state.count + 1 };
case 'DECREMENT': return { count: state.count - 1 };
case 'RESET': return { count: 0 };
default: throw new Error(`Unknown action: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</>
);
}Key Pitfalls
- Never mutate state directly:
state.items.push(x)— React won't re-render. - Always use spread to update objects/arrays:
setItems([...items, newItem]). - State updates are asynchronous and batched — don't read state immediately after setting.
useState(fn)— pass a function for expensive initial state computation (runs once).
Part 4 — React Hooks in Depth
useEffect
Runs after the render is committed to the screen.
// Dependency array controls when it runs
useEffect(() => { /* ... */ }); // every render
useEffect(() => { /* ... */ }, []); // once on mount
useEffect(() => { /* ... */ }, [value]); // when value changesAlways return a cleanup function for subscriptions, timers, and event listeners:
useEffect(() => {
const timer = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(timer); // cleanup on unmount or before next run
}, []);⚠️ Don't make the
useEffectcallbackasyncdirectly. Create an inner async function and call it:
useEffect(() => {
const load = async () => {
const res = await fetch(url);
setData(await res.json());
};
load();
}, [url]);useEffect vs useLayoutEffect
| Hook | Fires When | Use For |
|---|---|---|
useEffect |
After browser paint | Data fetching, subscriptions, logging |
useLayoutEffect |
Before browser paint (sync) | DOM measurements, preventing flicker |
Default to useEffect. Only use useLayoutEffect for visual DOM measurements.
useRef
Persists a value across renders without triggering a re-render.
// 1. DOM access
const inputRef = useRef();
useEffect(() => { inputRef.current.focus(); }, []);
return <input ref={inputRef} />;
// 2. Mutable value (timer ID, previous value, etc.)
const timerRef = useRef(null);
timerRef.current = setTimeout(...);
clearTimeout(timerRef.current);useRef vs useState:
useState |
useRef |
|
|---|---|---|
| Triggers re-render | Yes | No |
| Persists across renders | Yes | Yes |
| Use for | UI data | DOM refs, mutable values |
useCallback and useMemo
// useCallback — memoize a function (stable reference across renders)
const handleSearch = useCallback((query) => {
performSearch(query);
}, [/* deps */]);
// useMemo — memoize a computed value
const sortedList = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);When to use:
useCallback— when passing callbacks to memoized children (React.memo)useMemo— when computation is expensive and deps change infrequently
Don't memoize everything — it has overhead. Measure first.
Custom Hooks
Extract and reuse stateful logic. Must start with use.
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const load = async () => {
try {
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to fetch');
const json = await res.json();
if (!cancelled) setData(json);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
};
load();
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}Key Pitfalls
- Missing deps in
useEffect→ stale closures. - Async
useEffectcallback → will return a Promise, not a cleanup function. useLayoutEffectin SSR → runs synchronously, can cause issues.- Refs don't trigger re-renders — don't use them to display data in JSX.
Part 5 — Asynchronous Operations and Data Fetching
Fetch Patterns
// Basic
const res = await fetch('/api/posts');
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
const data = await res.json();
// Parallel — faster than sequential awaits
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
]);Standard Async State Pattern
function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const load = async () => {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('Network error');
const data = await res.json();
if (!cancelled) setPosts(data);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
};
load();
return () => { cancelled = true; }; // prevent state update after unmount
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}useReducer for Async State (Cleaner Pattern)
function dataReducer(state, action) {
switch (action.type) {
case 'FETCH_INIT': return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS': return { loading: false, error: null, data: action.payload };
case 'FETCH_FAILURE': return { ...state, loading: false, error: action.error };
default: throw new Error(`Unknown action: ${action.type}`);
}
}
// Dispatch actions instead of juggling 3 setX() calls
dispatch({ type: 'FETCH_INIT' });
dispatch({ type: 'FETCH_SUCCESS', payload: data });
dispatch({ type: 'FETCH_FAILURE', error: err.message });Debounced Search (Common Interview Pattern)
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debounced;
}
function Search() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500); // only fires 500ms after typing stops
useEffect(() => {
if (debouncedQuery) fetch(`/api/search?q=${debouncedQuery}`);
}, [debouncedQuery]);
return <input onChange={e => setQuery(e.target.value)} />;
}Key Pitfalls
- Not checking
res.ok— fetch only rejects on network failure, not HTTP errors (404, 500). - Setting state after unmount — use the cancel flag pattern.
- Sequential
awaitcalls when they could be parallel — usePromise.all. - Not handling loading and error states — users see nothing or a broken UI.
Part 6 — Styling in React
Approach Comparison
| Approach | Scope | Dynamic Styles | Pseudo-selectors | Best For |
|---|---|---|---|---|
| Plain CSS | Global | No | Yes | Simple apps |
| CSS Modules | Scoped | No | Yes | Most projects |
| Inline styles | Scoped | Yes | ❌ No | Quick one-offs |
| Styled Components | Scoped | Yes | Yes | Design systems |
| Tailwind CSS | Utility | Yes | Yes | Rapid UI |
CSS Modules (Recommended Default)
// Button.module.css
.primary { background: blue; color: white; }
.active { border: 2px solid green; }
// Button.jsx
import styles from './Button.module.css';
function Button({ isActive }) {
return (
<button className={`${styles.primary} ${isActive ? styles.active : ''}`}>
Click
</button>
);
}Generates unique class names at build time — no global collision.
Conditional Classes with clsx
import clsx from 'clsx';
<button className={clsx(
styles.button,
isActive && styles.active,
isDisabled && styles.disabled
)}>
Submit
</button>Styled Components (CSS-in-JS)
import styled from 'styled-components';
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
color: white;
padding: 10px 20px;
&:hover { opacity: 0.9; }
@media (max-width: 600px) { padding: 6px 12px; }
`;
<Button primary>Submit</Button>Inline Styles — Use Sparingly
<div style={{ backgroundColor: 'blue', fontSize: '16px' }}>
Inline styled
</div>No pseudo-selectors (:hover, :focus). No media queries. Only for truly dynamic, one-off values.
Key Pitfalls
- Inline styles can't do
:hoveror media queries. - Plain CSS classes leak globally — use Modules or CSS-in-JS for isolation.
- Don't put all styles in one file — colocate component styles with the component.
Part 7 — Testing in React
Test Types
| Type | What It Tests | Tools |
|---|---|---|
| Unit | Single function or component in isolation | Jest, Vitest |
| Integration | Multiple components working together | React Testing Library |
| E2E | Full user flows in real browser | Cypress, Playwright |
React Testing Library (RTL) — Core Philosophy
Test behavior, not implementation. Query the DOM as a user would.
npm install --save-dev @testing-library/react @testing-library/jest-domimport { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('increments count on button click', () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
fireEvent.click(button);
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});Query Priority (Use in This Order)
| Query | Use When |
|---|---|
getByRole |
Accessible role (button, heading, input) — prefer |
getByLabelText |
Form labels |
getByPlaceholderText |
Input placeholders |
getByText |
Visible text content |
getByTestId |
Last resort — brittle |
Async Queries
// findBy — returns a Promise, use with await
const element = await screen.findByText(/loaded data/i);
// waitFor — waits until assertion passes
await waitFor(() => expect(screen.getByText(/done/i)).toBeInTheDocument());Mocking Fetch
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve([{ id: 1, title: 'Mock Post' }]),
})
);
});
afterEach(() => jest.resetAllMocks());Snapshot Testing
test('matches snapshot', () => {
const { asFragment } = render(<App />);
expect(asFragment()).toMatchSnapshot();
});Use sparingly. Snapshots fail on any UI change — they're noisy and developers tend to blindly update them.
Jest Basics
describe('sum function', () => {
test('adds two numbers', () => {
expect(sum(2, 3)).toBe(5);
});
test('returns 0 for empty input', () => {
expect(sum(0, 0)).toBe(0);
});
});Common matchers: toBe, toEqual, toBeInTheDocument, toHaveBeenCalled, toThrow.
Key Pitfalls
- Testing internal state or implementation details — tests break on refactor.
- Using
getByTestIdeverywhere — ties tests to markup, not behavior. - Not cleaning up mocks between tests — test pollution.
- Shallow rendering — doesn't test component integration.
Part 8 — Performance, Patterns, and Interview Scenarios
Virtual DOM and Reconciliation
React builds a virtual DOM tree. On state change, it diffs the new tree against the old one and applies only the changed nodes to the real DOM. Keys help React match list items across renders.
Immutability Is Required
React uses referential equality to detect changes. Mutating an object/array directly won't trigger a re-render.
// ❌ Mutation — React won't re-render
state.items.push(newItem);
// ✅ New reference — React detects change
setItems(prev => [...prev, newItem]);
setUser(prev => ({ ...prev, name: 'Nishanth' }));Performance Optimization
// React.memo — skip re-render if props didn't change
const MemoizedList = React.memo(function List({ items }) {
return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
});
// useCallback — stable function reference for memoized children
const handleClick = useCallback(() => doSomething(id), [id]);
// useMemo — expensive computation
const total = useMemo(() => items.reduce((sum, i) => sum + i.price, 0), [items]);
// Code splitting — lazy load heavy components
const Chart = React.lazy(() => import('./Chart'));
// Wrap in Suspense:
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>Props vs State vs Context
| Concept | Owned By | Mutable | Purpose |
|---|---|---|---|
| Props | Parent | No | Pass data down |
| State | Component | Yes | Internal changing data |
| Context | Provider | Yes | Avoid prop drilling for global data |
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Header />
</ThemeContext.Provider>
);
}
function Header() {
const theme = useContext(ThemeContext); // no prop threading
return <h1 className={theme}>Title</h1>;
}Context is not a state management solution — it's a dependency injection mechanism. Every consumer re-renders when context value changes.
Error Boundaries
Catch rendering errors in child trees. Must be class components — no hook equivalent yet.
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToService(error, info);
}
render() {
if (this.state.hasError) return <h2>Something went wrong.</h2>;
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<RiskyComponent />
</ErrorBoundary>React.StrictMode
<React.StrictMode>
<App />
</React.StrictMode>In development only: double-invokes render, useState initializer, and useReducer to surface side effects and unsafe patterns. No effect in production.
Common Interview Questions
What is the Virtual DOM? An in-memory JS object representation of the real DOM. React diffs old vs new virtual DOM on each render and applies minimal real DOM updates.
What triggers a re-render?
State change, prop change, or parent re-render (even if props didn't change — unless wrapped in React.memo).
useEffect cleanup — when does it run?
Before the next effect execution, and on component unmount.
Why can't you call hooks conditionally? React relies on hook call order to associate state with the right hook. Conditional calls break this ordering.
useState vs useReducer — when to switch?
useReducer when state transitions are complex, interdependent, or numerous enough that multiple useState setters become hard to reason about.
What is prop drilling? Passing props through intermediate components that don't use them. Fix with Context or state management (Zustand, Redux).
What is a controlled component? An input whose value is controlled by React state — not the DOM.
How does React.memo differ from useMemo?
React.memo memoizes a component (skips re-render if props unchanged). useMemo memoizes a computed value inside a component.
Quick Reference
Hook Cheatsheet
| Hook | Purpose | Triggers Re-render |
|---|---|---|
useState |
Reactive state | Yes |
useReducer |
Complex state with actions | Yes |
useEffect |
Side effects after render | No |
useRef |
DOM access, mutable values | No |
useCallback |
Memoize function reference | No |
useMemo |
Memoize computed value | No |
useContext |
Read from nearest Provider | Yes (on context change) |
When to Use What
| Situation | Solution |
|---|---|
| Share state across siblings | Lift state up |
| Avoid passing props 3+ levels deep | Context or state manager |
| Expensive list filter/sort | useMemo |
Callback passed to React.memo child |
useCallback |
| Async data fetching | useEffect + cancel flag |
| Complex async state | useReducer |
| DOM manipulation or timer ID | useRef |
| Debounce user input | Custom useDebounce hook |
| Lazy load heavy component | React.lazy + Suspense |
| Catch render errors | ErrorBoundary |