React Interview Questions
51 questions — 10 easy · 22 medium · 19 hard
JavaScript & TypeScript
(10)// Shallow copy - only first level
const shallow1 = { ...original };
const shallow2 = Object.assign({}, original);
const shallow3 = [...array];
// Deep copy
const deep1 = structuredClone(original); // Modern, native
const deep2 = JSON.parse(JSON.stringify(original)); // Old way
// JSON method limitations:
// - Loses functions, undefined, Symbol
// - Converts Date to string
// - Fails on circular references
Follow-up answer: No - functions are lost. Use structuredClone() (also loses functions) or manual/library deep clone.
Follow-up
Follow-up: You have an object with methods. Can you clone it with JSON.parse(JSON.stringify())?
- JavaScript is single-threaded, Event Loop enables async
- Macrotasks:
setTimeout,setInterval, I/O, UI rendering - Microtasks:
Promise.then/catch/finally,queueMicrotask,MutationObserver - Order: Call Stack → ALL Microtasks → ONE Macrotask → repeat
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2
Follow-up answer: React batches state updates and schedules renders. Understanding microtasks helps debug why state doesn't update immediately and how concurrent features prioritize work.
Follow-up
Follow-up: Why is this important for React?
// Closure = function + its lexical environment
function createCounter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count,
};
}
// React practical example - stale closure problem
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// ❌ Stale closure - count is always 0
console.log(count);
}, 1000);
return () => clearInterval(id);
}, []); // Empty deps = closure captures initial count
// ✅ Fix with ref
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current); // Always current
}, 1000);
return () => clearInterval(id);
}, []);
}
const obj = {
name: 'Object',
regular: function() {
console.log(this.name); // 'Object' - this = caller
},
arrow: () => {
console.log(this.name); // undefined - this = lexical (outer scope)
},
};
// React context - why arrow functions matter in callbacks
class Component extends React.Component {
name = 'Component';
// ❌ Regular method - 'this' is undefined in callback
handleClick() {
console.log(this.name);
}
// ✅ Arrow function - 'this' is component instance
handleClick = () => {
console.log(this.name);
}
render() {
// Without arrow function or binding, this.handleClick loses context
return <button onClick={this.handleClick}>Click</button>;
}
}
let anyVal: any = getData();
anyVal.foo.bar(); // ✅ No error - completely unsafe
let unknownVal: unknown = getData();
unknownVal.foo; // ❌ Error - must narrow first
// Narrowing unknown
if (typeof unknownVal === 'string') {
unknownVal.toUpperCase(); // ✅ Now it's string
}
// Use any: migrating JS, truly dynamic data, quick prototyping
// Use unknown: type-safe "I don't know yet" - API responses, user input
// React example - safe API response handling
async function fetchUser(): Promise<unknown> {
const res = await fetch('/api/user');
return res.json();
}
function isUser(data: unknown): data is User {
return typeof data === 'object' && data !== null && 'id' in data;
}
// Interface - extendable, declaration merging
interface User {
name: string;
}
interface User { // Merges with above
age: number;
}
// Type - more flexible
type ID = string | number; // Unions
type Callback = (data: string) => void; // Functions
type Point = [number, number]; // Tuples
type Readonly<T> = { readonly [K in keyof T]: T[K] }; // Mapped types
| Feature | Interface | Type |
|---|---|---|
| Extend/Inherit | ✅ | ✅ |
| Declaration merging | ✅ | ❌ |
| Unions, Intersections | ❌ | ✅ |
| Mapped/Conditional types | ❌ | ✅ |
Rule of thumb: Interface for objects/classes, Type for everything else.
// Basic generic
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// API Response pattern
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
totalPages: number;
totalItems: number;
};
}
// Generic hook pattern
function useApi<T>(url: string): { data: T | null; loading: boolean; error: Error | null } {
// implementation
}
// Usage
const { data } = useApi<User[]>('/api/users');
interface User {
id: string;
name: string;
email: string;
age?: number;
}
// Partial - all optional (great for updates)
type UpdateUserDTO = Partial<User>;
// Required - all required
type CompleteUser = Required<User>;
// Pick - select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// Omit - exclude properties
type CreateUserDTO = Omit<User, 'id'>;
// Record - create object type with specific keys
type RolePermissions = Record<'admin' | 'user' | 'guest', string[]>;
// Practical combo: form state
type FormState<T> = {
values: T;
errors: Partial<Record<keyof T, string>>;
touched: Partial<Record<keyof T, boolean>>;
};
// Built-in guards
function process(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}
// Custom type guard
interface SuccessResponse<T> { success: true; data: T; }
interface ErrorResponse { success: false; error: string; }
type ApiResult<T> = SuccessResponse<T> | ErrorResponse;
function isSuccess<T>(result: ApiResult<T>): result is SuccessResponse<T> {
return result.success === true;
}
// React usage
function UserProfile({ result }: { result: ApiResult<User> }) {
if (isSuccess(result)) {
return <div>{result.data.name}</div>; // TS knows it's SuccessResponse
}
return <div>Error: {result.error}</div>; // TS knows it's ErrorResponse
}
// Method 1: Inline props type (preferred)
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
children?: React.ReactNode;
}
function Button({ label, onClick, variant = 'primary', children }: ButtonProps) {
return <button onClick={onClick}>{label}{children}</button>;
}
// Generic component
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return <ul>{items.map(item => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>;
}
// Event handlers
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
CSS
(6)/* Flexbox (most common) */
.parent {
display: flex;
justify-content: center;
align-items: center;
}
/* Grid */
.parent {
display: grid;
place-items: center;
}
/* Absolute positioning */
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.sticky-header {
position: sticky;
top: 0;
z-index: 100;
}
When sticky fails:
- Parent has
overflow: hidden,scroll, orauto - No
top/bottom/left/rightvalue set - Parent has no defined height
display: flexon parent sometimes causes issues
z-indexonly works on positioned elements- Stacking context is like a "layer group" - z-index only compares within same context
Creates new stacking context:
positionwithz-index(not auto)opacity< 1transform,filter,perspectiveisolation: isolate
/* Common bug: */
.parent { position: relative; z-index: 1; }
.child { position: relative; z-index: 9999; }
/* Child is still behind elements with z-index: 2 outside parent */
/* Fix with isolation */
.modal-backdrop { isolation: isolate; }
/* Fluid typography with clamp() */
h1 {
font-size: clamp(1.5rem, 4vw, 3rem);
}
/* Responsive grid without breakpoints */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
/* Container-based sizing */
.card {
width: min(100%, 400px);
padding: max(1rem, 3%);
}
/* CSS Container Queries */
.card-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.card { flex-direction: row; }
}
/* ✅ GPU-accelerated (composite layer) */
transform: translateX(100px);
transform: scale(1.1);
transform: rotate(45deg);
opacity: 0.5;
/* ❌ Triggers layout/paint (slow) */
left: 100px;
top: 50px;
width: 200px;
margin: 10px;
/* Example: Animate position */
/* ❌ Bad */
.animate { left: 0; transition: left 0.3s; }
/* ✅ Good */
.animate { transform: translateX(0); transition: transform 0.3s; }
| Method | Visible | Screen Reader | Takes Space |
|---|---|---|---|
display: none |
❌ | ❌ | ❌ |
visibility: hidden |
❌ | ❌ | ✅ |
opacity: 0 |
❌ | ✅ | ✅ |
.sr-only |
❌ | ✅ | ❌ |
aria-hidden="true" |
✅ | ❌ | ✅ |
/* Screen-reader only (accessible hide) */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
React
(18)- Virtual DOM is lightweight JS representation of real DOM
- When state changes → new Virtual DOM tree created
- React diffs (reconciles) old vs new tree
- Only changed parts update in real DOM
Key heuristics:
- Different element types → rebuild entire subtree
- Same type → update attributes only
- Keys identify which items changed in lists
Follow-up answer:
Fiber is React's reconciliation engine (since React 16):
- Work can be split into chunks (time-slicing)
- Rendering can be paused, aborted, restarted
- Enables concurrent features (Suspense, transitions)
- Each fiber node represents a unit of work
Follow-up
Follow-up: What is the Fiber architecture?
// ❌ Bad - index as key
{items.map((item, index) => <Item key={index} {...item} />)}
// ✅ Good - stable unique ID
{items.map((item) => <Item key={item.id} {...item} />)}
Problems with index:
- Reordering: wrong items get updated
- Filtering: components don't remount
- State bugs: state sticks to wrong item
Index is okay when:
- List is static
- Items have no state
- Items are never reordered/filtered
- Only call at top level - not in loops, conditions, nested functions
- Only call in React functions - components or custom hooks
// ❌ Bad - conditional hook
function Component({ show }) {
if (show) {
const [value, setValue] = useState(0);
}
}
// ✅ Good
function Component({ show }) {
const [value, setValue] = useState(0);
// Use value conditionally, not the hook
}
Why? React identifies hooks by call order. Conditional calls break this.
const [count, setCount] = useState(0);
// Direct update
setCount(5);
// Functional update - when new state depends on previous
setCount(prev => prev + 1);
// ❌ Bug: stale closure
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
// Result: +1, not +2 (both use same stale count)
};
// ✅ Fix: functional updates
const handleClick = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// Result: +2
};
// Lazy initialization for expensive computation
const [data, setData] = useState(() => expensiveCalculation());
useEffect(() => {
const subscription = api.subscribe(userId);
const timer = setInterval(fetchData, 5000);
// Cleanup runs before next effect AND on unmount
return () => {
subscription.unsubscribe();
clearInterval(timer);
};
}, [userId]);
Pitfalls:
// ❌ Missing dependency - stale closure
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // Always logs initial count!
}, 1000);
return () => clearInterval(timer);
}, []); // count missing
// ❌ Object in deps - infinite loop
useEffect(() => {
fetchData(options);
}, [options]); // New object every render!
// ✅ Fix: extract primitives
}, [options.page, options.limit]);
// useMemo: memoize computed VALUE
const sortedUsers = useMemo(
() => users.sort((a, b) => a.name.localeCompare(b.name)),
[users]
);
// useCallback: memoize FUNCTION reference
const handleClick = useCallback((id: string) => {
setSelected(id);
}, []);
When to use:
- Expensive calculations
- Referential equality for memoized children
- Dependencies of other hooks
When NOT to use:
// ❌ Overkill - simple calculation
const doubled = useMemo(() => value * 2, [value]);
// ❌ No memoized children consuming this
<button onClick={useCallback(() => setOpen(true), [])}>
// ❌ Primitives are already compared by value
<Child count={count} /> // number, no memo needed
Rule: Profile first, optimize where needed.
// 1. DOM reference
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();
// 2. Persist value across renders without re-render
const renderCount = useRef(0);
renderCount.current++;
// 3. Store previous value
function usePrevious<T>(value: T) {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// 4. Store interval/timeout IDs
const timerRef = useRef<NodeJS.Timeout>();
// 5. Avoid stale closures
const latestCallback = useRef(callback);
latestCallback.current = callback;
useEffect(() => {
const handler = () => latestCallback.current();
}, []);
// 6. Track if component is mounted
const isMounted = useRef(true);
useEffect(() => {
return () => { isMounted.current = false; };
}, []);
| useEffect | useLayoutEffect | |
|---|---|---|
| Timing | After paint | Before paint |
| Blocks paint | No | Yes |
| Use case | Most effects | DOM measurements, prevent flicker |
| SSR | Safe | Warning |
// useLayoutEffect: measure DOM before user sees it
useLayoutEffect(() => {
const height = ref.current.getBoundingClientRect().height;
setHeight(height); // No flicker
}, []);
// useTransition: mark STATE UPDATES as non-urgent
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
setQuery(e.target.value); // Urgent: input updates immediately
startTransition(() => {
setResults(filterResults(e.target.value)); // Non-urgent: can wait
});
};
// useDeferredValue: defer a VALUE you receive
function List({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
// deferredQuery "lags behind" query during typing
return <ExpensiveList query={deferredQuery} />;
}
Key difference:
useTransition- you control when to defer (wrap setState)useDeferredValue- React defers a value you receive as prop
Use cases:
- Search results while typing
- Tab switching with heavy content
- Filtering large lists
// Custom hook for async operations
function useAsync<T>(asyncFn: () => Promise<T>, deps: unknown[] = []) {
const [state, setState] = useState<{
data: T | null;
loading: boolean;
error: Error | null;
}>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let cancelled = false;
setState(prev => ({ ...prev, loading: true, error: null }));
asyncFn()
.then(data => {
if (!cancelled) {
setState({ data, loading: false, error: null });
}
})
.catch(error => {
if (!cancelled) {
setState({ data: null, loading: false, error });
}
});
return () => {
cancelled = true;
};
}, deps);
return state;
}
// Usage
function UserProfile({ userId }: { userId: string }) {
const { data: user, loading, error } = useAsync(
() => fetchUser(userId),
[userId]
);
if (loading) return <Spinner />;
if (error) return <Error error={error} />;
return <Profile user={user} />;
}
Follow-up answer:
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
Follow-up
Follow-up: Hook for debounced value
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<ErrorPage />}>
<App />
</ErrorBoundary>
Why no hooks? There's no hook equivalent for componentDidCatch or getDerivedStateFromError. Error boundaries need to catch errors during rendering, which isn't possible with current hook APIs.
What error boundaries DON'T catch:
- Event handlers (use try/catch)
- Async code (promises, setTimeout)
- SSR errors
- Errors in the error boundary itself
// Components share implicit state via context
const TabsContext = createContext<{
activeTab: string;
setActiveTab: (tab: string) => void;
} | null>(null);
function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function Tab({ id, children }: { id: string; children: React.ReactNode }) {
const ctx = useContext(TabsContext);
if (!ctx) throw new Error('Tab must be used within Tabs');
return (
<button
className={ctx.activeTab === id ? 'active' : ''}
onClick={() => ctx.setActiveTab(id)}
>
{children}
</button>
);
}
function TabPanel({ id, children }: { id: string; children: React.ReactNode }) {
const ctx = useContext(TabsContext);
if (!ctx) throw new Error('TabPanel must be used within Tabs');
return ctx.activeTab === id ? <div>{children}</div> : null;
}
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
// Usage
<Tabs defaultTab="profile">
<Tabs.Tab id="profile">Profile</Tabs.Tab>
<Tabs.Tab id="settings">Settings</Tabs.Tab>
<Tabs.Panel id="profile">Profile content</Tabs.Panel>
<Tabs.Panel id="settings">Settings content</Tabs.Panel>
</Tabs>
// Render Props - pass rendering logic as a function
interface MouseTrackerProps {
children: (position: { x: number; y: number }) => React.ReactNode;
}
function MouseTracker({ children }: MouseTrackerProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return <>{children(position)}</>;
}
// Usage
<MouseTracker>
{({ x, y }) => <div>Mouse: {x}, {y}</div>}
</MouseTracker>
Render Props vs Custom Hooks:
| Render Props | Custom Hooks |
|---|---|
| Works in class components | Functional only |
| Can conditionally render | Must follow rules of hooks |
| More boilerplate | Cleaner API |
| Visible in JSX tree | Invisible logic sharing |
Use Render Props when:
- Need to support class components
- Want explicit control over what renders
- Component handles both logic AND rendering decisions
Use Custom Hooks when:
- Just sharing stateful logic
- Modern codebase (functional components)
- Want cleaner, more reusable code
Server Components (RSC):
- Render ONLY on server, never ship to client
- Can directly access databases, file system, APIs
- Zero client-side JS bundle impact
- Can't use hooks, state, or event handlers
SSR (Server-Side Rendering):
- Renders to HTML on server for initial load
- Component JS still ships to client for hydration
- Component becomes interactive after hydration
// Server Component (default in Next.js App Router)
async function UserList() {
const users = await db.query('SELECT * FROM users'); // Direct DB access
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
// Client Component - must opt-in
'use client';
function Counter() {
const [count, setCount] = useState(0); // Can use state
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
When to use Server Components:
- Data fetching
- Accessing backend resources
- Large dependencies (markdown, syntax highlighting)
- Sensitive data (API keys)
When to use Client Components:
- Interactivity (onClick, onChange)
- State (useState, useReducer)
- Browser APIs (localStorage, geolocation)
- Effects (useEffect)
// Suspense shows fallback while children are loading
<Suspense fallback={<Spinner />}>
<UserProfile userId={id} />
</Suspense>
// Nested Suspense for granular loading states
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</Suspense>
</Suspense>
With data fetching (using a library like TanStack Query or Next.js):
// Component "suspends" while data loads
function UserProfile({ userId }: { userId: string }) {
// useSuspenseQuery throws a promise while loading
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <div>{user.name}</div>; // Only renders when data ready
}
// Parent handles loading state
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userId="123" />
</Suspense>
Benefits:
- Declarative loading states
- Avoid "loading waterfalls" (parallel fetching)
- Streaming SSR (show content as it loads)
- Better code splitting
// Dynamic import creates separate chunk
const Dashboard = React.lazy(() => import('./Dashboard'));
const Settings = React.lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Named export
const Dashboard = React.lazy(() =>
import('./Dashboard').then(module => ({ default: module.Dashboard }))
);
// Preloading on hover
const preloadDashboard = () => import('./Dashboard');
<Link to="/dashboard" onMouseEnter={preloadDashboard}>
Dashboard
</Link>
// With error boundary
<ErrorBoundary fallback={<LoadError />}>
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
</ErrorBoundary>
// React Hook Form + Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
type FormData = z.infer<typeof schema>;
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isDirty },
reset,
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { email: '', password: '', confirmPassword: '' },
});
const onSubmit = async (data: FormData) => {
await login(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<input type="password" {...register('confirmPassword')} />
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
<button disabled={isSubmitting || !isDirty}>
{isSubmitting ? 'Loading...' : 'Submit'}
</button>
</form>
);
}
Why React Hook Form?
- Minimal re-renders (uncontrolled under the hood)
- Built-in validation with schema libraries
- Great TypeScript support
- Handles complex cases (dynamic fields, nested forms)
// Controlled - React owns the state
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
);
}
// Uncontrolled - DOM owns the state
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
console.log(inputRef.current?.value);
};
return <input ref={inputRef} defaultValue="initial" />;
}
| Controlled | Uncontrolled |
|---|---|
| Re-renders on every keystroke | No re-renders |
| Instant validation | Validation on submit |
| Dynamic input manipulation | Simple forms |
| Required for complex forms | File inputs (always uncontrolled) |
Use uncontrolled when:
- Simple forms with just submit
- File inputs
- Integrating with non-React code
- Performance-critical (many inputs)
Data Fetching & State
(4)// ❌ Plain useEffect - lots of boilerplate
function Users() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchUsers()
.then(data => { if (!cancelled) setUsers(data); })
.catch(err => { if (!cancelled) setError(err); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, []);
// No caching, no refetching, no deduplication...
}
// ✅ TanStack Query - handles everything
function Users() {
const { data: users, isLoading, error, refetch } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
retry: 3,
});
if (isLoading) return <Spinner />;
if (error) return <Error error={error} />;
return <UserList users={users} />;
}
TanStack Query benefits:
- Caching - automatic, configurable
- Deduplication - same query = single request
- Background refetch - stale-while-revalidate
- Optimistic updates - instant UI feedback
- Pagination/Infinite scroll - built-in
- Devtools - inspect cache state
function UserProfile({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const updateMutation = useMutation({
mutationFn: (data: UpdateUserDTO) => updateUser(userId, data),
// Optimistic update
onMutate: async (newData) => {
await queryClient.cancelQueries({ queryKey: ['user', userId] });
const previous = queryClient.getQueryData(['user', userId]);
queryClient.setQueryData(['user', userId], (old: User) => ({
...old,
...newData,
}));
return { previous };
},
onError: (err, newData, context) => {
// Rollback on error
queryClient.setQueryData(['user', userId], context?.previous);
},
onSettled: () => {
// Refetch to ensure sync
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
const handleSave = (data: UpdateUserDTO) => {
updateMutation.mutate(data);
};
return (
<form>
{/* form fields */}
<button disabled={updateMutation.isPending}>
{updateMutation.isPending ? 'Saving...' : 'Save'}
</button>
</form>
);
}
| Solution | Use Case | Pros | Cons |
|---|---|---|---|
useState |
Local UI state | Simple | Local only |
useReducer |
Complex local state | Predictable | Still local |
Context |
Rarely changing global | Built-in | All consumers re-render |
| Redux/RTK | Large apps | DevTools, middleware | Boilerplate |
| Zustand | Simple global | Minimal, no boilerplate | Smaller ecosystem |
| TanStack Query | Server state | Cache, sync, refetch | Async only |
| Jotai/Recoil | Atomic state | Fine-grained updates | Learning curve |
Banking app recommendation:
Auth/User state Zustand or Context (rarely changes)
Server data TanStack Query (accounts, transactions)
UI state Local useState
Complex forms React Hook Form
Audit/logging needs Redux (time-travel, middleware)
// Problem: ALL consumers re-render when ANY context value changes
const AppContext = createContext({
user: null,
theme: 'light',
notifications: [],
});
// Solution 1: Split contexts by update frequency
const UserContext = createContext(null);
const ThemeContext = createContext('light');
const NotificationsContext = createContext([]);
// Solution 2: Memoize value object
function Provider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(
() => ({ user, login, logout }),
[user] // Only re-creates when user changes
);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
// Solution 3: Memoize consumers
const UserDisplay = memo(function UserDisplay() {
const { user } = useContext(UserContext);
return <div>{user?.name}</div>;
});
// Solution 4: Use state management library for frequent updates
// Zustand, Jotai - fine-grained subscriptions
Routing
(2)// Route configuration with data loading
const router = createBrowserRouter([
{
path: '/users/:id',
element: <UserProfile />,
loader: async ({ params }) => {
// Runs before component renders
return fetchUser(params.id);
},
action: async ({ request, params }) => {
// Handles form submissions
const formData = await request.formData();
return updateUser(params.id, Object.fromEntries(formData));
},
errorElement: <ErrorPage />,
},
]);
// Component uses loader data
function UserProfile() {
const user = useLoaderData() as User;
const navigation = useNavigation(); // pending states
return (
<div className={navigation.state === 'loading' ? 'loading' : ''}>
<h1>{user.name}</h1>
<Form method="post">
<input name="email" defaultValue={user.email} />
<button type="submit">Update</button>
</Form>
</div>
);
}
Benefits:
- Data loads before render (no loading states in component)
- Automatic revalidation after mutations
- Parallel data loading for nested routes
- Built-in error handling per route
// Protected route wrapper
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return <LoadingSpinner />;
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
// Usage in routes
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
</Routes>
// Or with layout route
<Routes>
<Route element={<ProtectedLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
function ProtectedLayout() {
const { user } = useAuth();
if (!user) return <Navigate to="/login" />;
return <Outlet />;
}
// Role-based protection
function RequireRole({ role, children }: { role: string; children: React.ReactNode }) {
const { user } = useAuth();
if (!user?.roles.includes(role)) {
return <Navigate to="/unauthorized" />;
}
return <>{children}</>;
}
Performance
(4)React DevTools Profiler
- Record interaction
- Identify slow components
- Find unnecessary re-renders
- Check "Why did this render?"
Chrome DevTools Performance
- Main thread blocking
- Long tasks (>50ms)
- JavaScript execution time
Lighthouse
- Core Web Vitals (LCP, INP, CLS)
- Performance score
Bundle Analysis
npm run build -- --stats npx webpack-bundle-analyzer stats.jsonNetwork Tab
- Large payloads
- Waterfall (sequential requests)
- Slow API calls
// Shallow compares props, skips re-render if same
const MemoizedRow = React.memo(function Row({ data, onClick }: RowProps) {
return <div onClick={onClick}>{data.name}</div>;
});
// Custom comparison
const MemoizedRow = React.memo(Row, (prevProps, nextProps) => {
return prevProps.data.id === nextProps.data.id;
});
Use when:
- Component renders frequently with same props
- Component is expensive to render
- Parent re-renders often
Don't use when:
- Props change every render anyway
- Component is cheap
- Premature optimization
Common mistake:
// ❌ Doesn't help - onClick is new every render
<MemoizedRow onClick={() => handleClick(id)} />
// ✅ Fix with useCallback
const handleClick = useCallback((id: string) => {...}, []);
<MemoizedRow onClick={handleClick} />
// 1. Virtualization - only render visible items
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Transaction[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 60,
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: virtualRow.start,
height: virtualRow.size,
width: '100%',
}}
>
<TransactionRow data={items[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}
// 2. Additional optimizations:
// - Pagination (server-side)
// - Infinite scroll
// - Memoize row components
// - Stable keys (not index)
// - Avoid inline functions/objects in props
<!-- Scripts -->
<script src="app.js"></script> <!-- Blocks parsing -->
<script src="app.js" async></script> <!-- Parallel load, execute when ready -->
<script src="app.js" defer></script> <!-- Parallel load, execute after DOM -->
<!-- Resource hints -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="font.woff2" as="font" crossorigin>
<link rel="prefetch" href="next-page.js">
<link rel="preconnect" href="https://api.bank.com">
| Load Priority | Execute | |
|---|---|---|
| Normal script | High, blocks | Immediately |
async |
High, parallel | When ready (any order) |
defer |
Low, parallel | After DOM, in order |
Security
(3)Storage options:
- ❌
localStorage- accessible via XSS - ❌
sessionStorage- also XSS vulnerable - ✅ HttpOnly Cookie - not accessible to JS
- ✅ In-memory + silent refresh - most secure
// Recommended pattern
let accessToken: string | null = null;
async function refreshToken() {
const response = await fetch('/auth/refresh', {
credentials: 'include', // Sends HttpOnly cookie
});
const { accessToken: newToken } = await response.json();
accessToken = newToken;
}
// Axios interceptor
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
await refreshToken();
return api.request(error.config);
}
return Promise.reject(error);
}
);
React's built-in protection:
const userInput = '<script>alert("xss")</script>';
<div>{userInput}</div> // Renders as text, not script
Danger zones:
// ❌ dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userContent }} />
// ❌ href with user input
<a href={userProvidedUrl}>Link</a> // javascript: URLs
Prevention:
// Sanitize if you must render HTML
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }} />
// Validate URLs
const isSafeUrl = (url: string) => {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
};
// CSP Header
Content-Security-Policy: default-src 'self'; script-src 'self'
Prevention:
- CSRF Token - server generates, client sends with requests
fetch('/api/transfer', {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken },
body: JSON.stringify(data),
});
- SameSite Cookies
Set-Cookie: session=abc; SameSite=Strict; Secure; HttpOnly
- Verify Origin/Referer headers
Testing
(2)import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('LoginForm', () => {
it('submits with valid credentials', async () => {
const onSubmit = vi.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={onSubmit} />);
// Query by accessible role/label
await user.type(screen.getByLabelText(/email/i), 'test@test.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@test.com',
password: 'password123',
});
});
});
it('shows validation errors', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
});
});
Key principles:
- Test behavior, not implementation
- Query by accessibility (role, label, text)
- Use
userEventoverfireEvent - Avoid testing internal state
// Mock Service Worker (MSW) - recommended
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.get('/api/users', () => {
return HttpResponse.json([
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
]);
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: '3', ...body }, { status: 201 });
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('displays users', async () => {
render(<UserList />);
expect(await screen.findByText('John')).toBeInTheDocument();
expect(screen.getByText('Jane')).toBeInTheDocument();
});
test('handles error', async () => {
server.use(
http.get('/api/users', () => {
return HttpResponse.json({ error: 'Server error' }, { status: 500 });
})
);
render(<UserList />);
expect(await screen.findByText(/error loading users/i)).toBeInTheDocument();
});
Architecture
(2)src/
├─ app/ # App-level setup
│ ├─ App.tsx
│ ├─ routes.tsx
│ └─ providers.tsx
│
├─ features/ # Feature-based modules
│ ├─ accounts/
│ │ ├─ components/
│ │ ├─ hooks/
│ │ ├─ api/
│ │ ├─ types.ts
│ │ └─ index.ts
│ ├─ transactions/
│ └─ payments/
│
├─ shared/ # Shared across features
│ ├─ components/
│ ├─ hooks/
│ ├─ utils/
│ └─ types/
│
├─ lib/ # External integrations
│ ├─ api/
│ └─ analytics/
│
└─ styles/
Key principles:
- Feature-based, not type-based
- Clear boundaries between features
- Shared code only when needed
- Index files for public API
Immediately: Rotate/invalidate the secret
Remove from history:
# BFG Repo-Cleaner
bfg --delete-files 'config.json'
bfg --replace-text passwords.txt
# Force push
git push --force --all
- Prevention:
.gitignorefor.envfiles- Pre-commit hooks (
git-secrets) - GitHub secret scanning
- Environment variables in CI/CD
Use these questions in your next interview
Import all 51 questions into Intervy with one click. Add scoring rubrics, organize by template, and conduct structured interviews.
Try Intervy Free