Table of contents
- Part 1: Fundamentals of React
- Part 2: Components and Props
- Part 3: State Management in React
- Part 4: React Hooks in Depth
- Part 5: Asynchronous Operations and Data Fetching
- Part 6: Styling in React
- Part 7: Testing in React
- Part 8: Common Interview Questions and Practical Scenarios
The Road to React - Annotated Study Notes
Based on annotations from "The Road to React" by Robin Wieruch.
Part 1 - Fundamentals of React
React is a JavaScript library for building user interfaces. It focuses on the view layer: how data is displayed and updated in response to user interactions.
Key Concepts
- Declarative UI: describe what the UI should look like, not how to build it step by step.
- Component-based: break UIs into reusable, self-contained units.
- Unidirectional data flow: data flows parent -> child through props.
- Virtual DOM: React keeps a virtual representation of the DOM and updates only parts that change.
Setting Up and Running a React App
Use Vite (or create-react-app) for fast setup.
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev # runs the development server
npm run build # creates the optimized production build
npm run preview # tests the production build locallyNote: npm run dev is for local development; it is not optimized for production. Use npm run preview to test the production build.
Hot Reloading vs Fast Refresh
React Fast Refresh bridges React with the development server, instantly updating the browser when files change.
JSX (JavaScript XML)
JSX lets you write HTML inside JavaScript.
const greeting = 'Hello React';
const element = <h1>{greeting}</h1>;- Curly braces
{}embed any JS expression inside JSX. - JSX must have one parent element. Use
<div>or<React.Fragment>to wrap siblings. - JSX is transpiled (e.g., by Babel) into
React.createElement()calls.
JSX Attributes and Conventions
React replaces HTML attributes with camelCase equivalents:
| HTML Attribute | JSX Equivalent |
| --- | --- |
| class | className |
| for | htmlFor |
| onclick | onClick |
Example:
<label htmlFor="username">Username:</label>
<input id="username" className="form-input" onClick={handleClick} />Embedding Expressions
const user = { name: 'Nishanth', role: 'Engineer' };
return <p>{user.name} is a {user.role}</p>;You can also call functions:
<p>{formatDate(new Date())}</p>Rendering Lists in React
Use Array.map().
const items = ['React', 'Redux', 'TypeScript'];
function List() {
return (
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
);
}Why Keys Matter
- Each item needs a unique
keyso React can track changes efficiently. - Use stable identifiers (IDs), not indexes.
Bad:
{items.map((item, index) => <li key={index}>{item}</li>)}Good:
{items.map(item => <li key={item.id}>{item.name}</li>)}React DOM and the Root Element
index.html:
<div id="root"></div>index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(<App />);Components and Hierarchies (Preview)
function App() {
return (
<div>
<Search />
<List />
</div>
);
}Interview Checkpoints
- What is JSX? JSX is a syntax extension that must be transpiled to JavaScript.
- Is JSX mandatory? No;
React.createElement()works too. - Why use keys in lists? Efficient reconciliation.
classvsclassName? JSX usesclassNamebecauseclassis reserved in JS.
Part 2 - Components and Props
React apps are built from components: small, isolated, reusable chunks of UI.
Function Components
function Welcome() {
return <h1>Hello World</h1>;
}Arrow function:
const Welcome = () => <h1>Hello World</h1>;Component Tree and Hierarchy
function App() {
return (
<div>
<Search />
<List />
</div>
);
}Props - Passing Data Parent -> Child
function Greeting({ name }) {
return <h2>Hello {name}</h2>;
}
function App() {
return <Greeting name="Nishanth" />;
}Props are immutable.
One-Way Data Flow
If a child needs to affect parent state, pass a callback down.
function Child({ onClick }) {
return <button onClick={onClick}>Click Me</button>;
}
function Parent() {
const handleClick = () => alert('Clicked!');
return <Child onClick={handleClick} />;
}Do Not Mutate Props
function Profile(props) {
props.name = 'Changed'; // do not do this
return <p>{props.name}</p>;
}Destructuring Props
function Item({ title, url, author }) {
return (
<a href={url}>
{title} - {author}
</a>
);
}Nested destructuring:
function Item({ item: { title, url, author } }) {
return <a href={url}>{title} - {author}</a>;
}Spread and Rest Operators with Props
Spread:
const user = { name: 'Nishanth', role: 'Engineer' };
<UserCard {...user} />;Rest:
function Card({ title, ...rest }) {
return <div {...rest}>{title}</div>;
}Prop-Type Validation (vs TypeScript)
npm install prop-typesimport PropTypes from 'prop-types';
function Button({ label }) {
return <button>{label}</button>;
}
Button.propTypes = {
label: PropTypes.string.isRequired,
};Callback Handlers Across Components
function Search({ onSearch }) {
return <input onChange={(e) => onSearch(e.target.value)} />;
}
function App() {
const [query, setQuery] = useState('');
return (
<>
<Search onSearch={setQuery} />
<p>Searching for: {query}</p>
</>
);
}Interview Checkpoints
- What are props used for? Parent -> child data transfer.
- Can props be mutated? No.
- How does child -> parent communication work? Callback props.
- Props vs state? Props are external inputs; state is internal mutable data.
Part 3 - State Management in React
State is data that changes over time and triggers re-renders.
useState
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => setCount(count + 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
</div>
);
}React State Rules
- Call hooks only at the top level.
- State updates are batched/asynchronous.
- Never mutate state directly.
Lifting State Up
function Search({ query, onSearch }) {
return <input value={query} onChange={e => onSearch(e.target.value)} />;
}
function List({ query, items }) {
return (
<ul>
{items.filter(item => item.includes(query)).map(item => <li key={item}>{item}</li>)}
</ul>
);
}
function App() {
const [query, setQuery] = useState('');
const items = ['React', 'Redux', 'Hooks'];
return (
<>
<Search query={query} onSearch={setQuery} />
<List query={query} items={items} />
</>
);
}Controlled Components
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
);
}Uncontrolled Components
function UncontrolledForm() {
const inputRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
console.log(inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} type="text" />
<button type="submit">Submit</button>
</form>
);
}useReducer
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
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>
</>
);
}Interview Checkpoints
- Props vs state? Props in, state inside.
- Why lift state up? Share state across siblings.
- When useReducer? Complex transitions/interdependent state.
- Controlled component? Input controlled by React state.
Part 4 - React Hooks in Depth
Hooks let you use React features without class components.
Hook Rules
- Call hooks only at the top level.
- Call hooks only in React function components or custom hooks.
useEffect
import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Clicked ${count} times`;
}, [count]);
}Cleanup:
useEffect(() => {
const timer = setInterval(() => console.log('Tick'), 1000);
return () => clearInterval(timer);
}, []);Fetching Data with useEffect
function DataFetcher() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/posts');
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <ul>{data.map(item => <li key={item.id}>{item.title}</li>)}</ul>;
}useRef
function Timer() {
const countRef = useRef(0);
const handleClick = () => {
countRef.current++;
console.log(countRef.current);
};
return <button onClick={handleClick}>Count (check console)</button>;
}DOM ref:
function FocusInput() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} placeholder="Auto-focused input" />;
}useCallback
const handleSearch = useCallback((query) => {
console.log('Searching:', query);
}, []);useMemo
const sortedList = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);Custom Hooks
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadData = async () => {
const res = await fetch(url);
const json = await res.json();
setData(json);
setLoading(false);
};
loadData();
}, [url]);
return { data, loading };
}Interview Checkpoints
- useEffect vs useLayoutEffect? useEffect after paint; useLayoutEffect before paint.
- Why not async useEffect? It expects cleanup or void, not a Promise.
- useCallback vs useMemo? Function vs computed value.
- useRef vs useState? Ref persists without re-render.
Part 5 - Asynchronous Operations and Data Fetching
Fetch API Basics
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));Async/Await
async function getData() {
try {
const response = await fetch('https://api.example.com/posts');
if (!response.ok) throw new Error('Network error');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch failed:', error);
}
}Async Calls in Components
function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const loadPosts = async () => {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('Network response not ok');
const data = await res.json();
setPosts(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
loadPosts();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{posts.slice(0, 5).map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}useReducer for Async State (Pattern)
function dataReducer(state, action) {
switch (action.type) {
case 'FETCH_INIT':
return { ...state, isLoading: true, isError: false };
case 'FETCH_SUCCESS':
return { ...state, isLoading: false, data: action.payload };
case 'FETCH_FAILURE':
return { ...state, isLoading: false, isError: true };
default:
throw new Error();
}
}Cancel flag cleanup:
useEffect(() => {
let cancel = false;
const fetchData = async () => {
const res = await fetch(url);
const data = await res.json();
if (!cancel) setData(data);
};
fetchData();
return () => (cancel = true);
}, [url]);Parallel calls:
const [users, posts] = await Promise.all([
fetch('/users').then(r => r.json()),
fetch('/posts').then(r => r.json()),
]);Interview Checkpoints
- Why not async useEffect? Cleanup contract.
- Common async states? loading/error/data.
- useReducer vs multiple useState? Shared transitions.
- Why cleanup? Avoid setting state after unmount.
Part 6 - Styling in React
Inline Styles
function Button() {
return (
<button style={{ backgroundColor: 'blue', color: 'white', padding: '10px 20px' }}>
Click Me
</button>
);
}CSS Stylesheets
import './Button.css';
function Button() {
return <button className="btn-primary">Click Me</button>;
}CSS Modules
import styles from './Button.module.css';
function Button() {
return <button className={styles.primary}>Click Me</button>;
}Conditional Styling
<button className={isActive ? styles.active : styles.inactive}>
{isActive ? 'Active' : 'Inactive'}
</button>Using clsx:
npm install clsximport clsx from 'clsx';
<button className={clsx(styles.button, isActive && styles.active)}>
Click
</button>Styled Components (CSS-in-JS)
npm install styled-componentsimport styled from 'styled-components';
const Button = styled.button`
background: ${props => (props.primary ? 'blue' : 'gray')};
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
&:hover {
opacity: 0.9;
}
`;
function App() {
return <Button primary>Submit</Button>;
}Interview Checkpoints
- CSS Modules vs Styled Components? Scoped files vs CSS-in-JS with dynamic props.
- Why CSS Modules? Avoid global class collisions.
- Conditional styles? Ternary/AND/clsx.
- Inline styles drawbacks? No pseudo selectors/media queries.
Part 7 - Testing in React
Types of Tests
| Type | Purpose | | --- | --- | | Unit | test a single function/component | | Integration | test multiple components together | | E2E | full user flows in a real browser |
Jest
npm install --save-dev jestfunction sum(a, b) {
return a + b;
}
test('adds two numbers', () => {
expect(sum(2, 3)).toBe(5);
});React Testing Library
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();
});Mocking Fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([{ id: 1, title: 'Mock Post' }]),
})
);Snapshot Testing
import { render } from '@testing-library/react';
import App from './App';
test('matches snapshot', () => {
const { asFragment } = render(<App />);
expect(asFragment()).toMatchSnapshot();
});Coverage
npm test -- --coverageInterview Checkpoints
- Why RTL over shallow tests? Test behavior over implementation.
- When use waitFor/findBy? Async UI updates.
- Why avoid internal state tests? Fragile and implementation-coupled.
- Jest vs RTL? Runner/assertions vs UI interaction utilities.
Part 8 - Common Interview Questions and Practical Scenarios
Virtual DOM
React uses a Virtual DOM to batch and minimize expensive DOM operations.
Reconciliation
React diffs previous vs next trees; keys help list diffing.
Immutability
React relies on referential equality; avoid mutation.
Bad:
state.items.push(newItem);Good:
setItems([...state.items, newItem]);Performance Optimization
React.memofor componentsuseCallbackfor function identityuseMemofor expensive computed values- code splitting via
React.lazy
const Chart = React.lazy(() => import('./Chart'));Props vs State vs Context
| Concept | Owned By | Mutable | Purpose | | --- | --- | --- | --- | | Props | parent | no | inputs | | State | component | yes | internal data | | Context | app | yes | shared global-ish data |
const ThemeContext = createContext();
function App() {
return (
<ThemeContext.Provider value="dark">
<Header />
</ThemeContext.Provider>
);
}
function Header() {
const theme = useContext(ThemeContext);
return <h1 className={theme}>Dark Mode</h1>;
}Error Boundaries
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error('Caught error:', error, info);
}
render() {
if (this.state.hasError) return <h2>Something went wrong.</h2>;
return this.props.children;
}
}Debounce Search (Practical)
import { useState, useEffect } from 'react';
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 debounced = useDebounce(query, 500);
useEffect(() => {
if (debounced) fetch(`/api/search?q=${debounced}`);
}, [debounced]);
return <input onChange={e => setQuery(e.target.value)} />;
}Rapid-Fire
React.StrictModedouble-invokes some functions in dev to surface unsafe patterns.- Array index keys break reordering logic.
- Custom hooks share logic.
- setState in render causes infinite loops.
Mental Model
UI = f(state). State changes -> re-render -> diff -> minimal DOM updates.