Insights into Company
← React

Hooks

Understand the idea. Compare it. Explain it. Test yourself.

Question 1 of 5

How does useState work?

Solution and explanation

What it is

useState gives a function component a piece of state that survives re-renders.

const [count, setCount] = useState(0);
  • count is the current value for this render.
  • setCount(newValue) schedules a re-render with the new value.
  • 0 is the initial value, used only on the first render.

How React stores it

React keeps a list of hooks for each component instance and matches them **by call order**. That is why hooks must be called at the top level, never inside conditions or loops:

if (loggedIn) { const [x] = useState(); }   // wrong: order changes between renders

Updates are asynchronous and batched

function handleClick() {
  setCount(count + 1);
  setCount(count + 1);   // still count + 1: both read the same old value
}
function handleClickFixed() {
  setCount(c => c + 1);
  setCount(c => c + 1);  // + 2: functional update uses the latest value
}

Use the **functional form** whenever the new state depends on the old state.

Objects and arrays

State must be replaced, not mutated:

const [form, setForm] = useState({ name: '', email: '' });
setForm(prev => ({ ...prev, email: 'a@b.com' }));

const [skills, setSkills] = useState([]);
setSkills(prev => [...prev, 'React']);               // add
setSkills(prev => prev.filter(s => s !== 'PHP'));    // remove

React compares with Object.is; mutating the same object keeps the same reference, so React skips the re-render.

Lazy initial state

const [draft, setDraft] = useState(() => JSON.parse(localStorage.getItem('draft') ?? '{}'));

The function runs only on the first render, so expensive work is not repeated.

Summary

useState returns the current value and a setter; updates are batched, use functional updates for derived values, and never mutate objects or arrays.

Practice explaining this
Question 2 of 5

How does useEffect work?

Solution and explanation

What it is

useEffect runs **side effects** after React has updated the screen: fetching data, subscriptions, timers, analytics, manually touching the DOM. Rendering must stay pure; effects are where you talk to the outside world.

useEffect(() => {
  // effect
  return () => { /* cleanup */ };
}, [dependencies]);

When it runs

Dependency arrayRuns
noneafter every render
[]once after the first render (mount); cleanup on unmount
[jobId]after the first render and whenever jobId changes

Before re-running, React calls the previous cleanup. In development with <StrictMode> React mounts, unmounts and mounts again to expose missing cleanups.

Fetching data correctly

function JobDetail({ jobId }) {
  const [job, setJob] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    fetch(`/api/jobs/${jobId}`, { signal: controller.signal })
      .then(r => { if (!r.ok) throw new Error(r.status); return r.json(); })
      .then(setJob)
      .catch(e => { if (e.name !== 'AbortError') setError(e); });
    return () => controller.abort();     // cancels the old request when jobId changes
  }, [jobId]);

  if (error) return <p>Could not load job</p>;
  return job ? <h1>{job.title}</h1> : <p>Loading…</p>;
}

Without the abort, a slow old response could overwrite the newer one (a race condition).

Subscriptions and timers

useEffect(() => {
  const id = setInterval(() => setSeconds(s => s + 1), 1000);
  return () => clearInterval(id);
}, []);

When you do not need an effect

  • Deriving data from props/state: compute it during render.
  • Responding to a user event: do the work in the event handler.
  • Server data in larger apps: use TanStack Query or SWR (caching, retries, deduplication).

useLayoutEffect

Runs synchronously after DOM changes but before the browser paints; use it only to measure layout (tooltip position) to avoid flicker.

Summary

useEffect synchronises a component with external systems after render; declare every dependency, and always clean up subscriptions, timers and in-flight requests.

Practice explaining this
Question 3 of 5

What are effect dependencies?

Solution and explanation

What they are

The dependency array tells React **which values the effect reads**. After each render React compares every dependency with its previous value using Object.is; if any changed, the old cleanup runs and the effect runs again.

useEffect(() => {
  fetchJobs({ city, page });
}, [city, page]);   // re-fetch when city or page changes

Rule: list everything the effect uses

Every prop, state variable and function defined in the component that the effect reads must be in the array. The ESLint rule react-hooks/exhaustive-deps checks this. Leaving one out creates a **stale closure**: the effect keeps using an old value.

// Bug: always logs the first query
useEffect(() => {
  const id = setInterval(() => console.log(query), 1000);
  return () => clearInterval(id);
}, []);

Objects and functions as dependencies

Objects and functions are recreated on every render, so they are "different" each time and the effect runs endlessly.

const filters = { city, remote };          // new object every render
useEffect(() => { load(filters); }, [filters]);   // runs after every render

Fixes:

  1. Depend on primitives: [city, remote].
  2. Move the object or function **inside** the effect.
  3. Memoise: const filters = useMemo(() => ({ city, remote }), [city, remote]);
  4. Wrap callbacks in useCallback.

Avoiding a dependency legitimately

  • Use a functional update: setCount(c => c + 1) does not need count.
  • Use useRef for values you read but that should not re-trigger the effect.
  • React 19's useEffectEvent separates "reactive" from "non-reactive" logic.

Infinite loop example

useEffect(() => { setJobs(data); }, [jobs]);   // setJobs changes jobs → effect runs again

Summary

Dependencies are the effect's inputs. Include every reactive value it reads; keep them primitive or memoised so the effect runs exactly when its inputs change.

Practice explaining this
Question 4 of 5

When should useMemo and useCallback be used?

Solution and explanation

What they do

  • useMemo(fn, deps) caches the **result** of a calculation until a dependency changes.
  • useCallback(fn, deps) caches the **function itself** (same reference) until a dependency changes. It equals useMemo(() => fn, deps).
const visibleJobs = useMemo(
  () => jobs.filter(j => j.salary >= minSalary).sort(bySalary),
  [jobs, minSalary]
);

const handleApply = useCallback(id => apply(id, userId), [userId]);

When they help

  1. **Expensive calculations:** filtering or sorting thousands of rows, building chart data. Measure first: if it takes under ~1 ms, memoising adds overhead without benefit.
  2. **Keeping references stable for memoised children:**
const JobRow = React.memo(function JobRow({ job, onApply }) { ... });
// Without useCallback, onApply is a new function every render, so every JobRow re-renders.
<JobRow job={job} onApply={handleApply} />
  1. **Values used as effect dependencies:** a memoised object or callback stops an effect from running after every render.
  2. **Context values:** value={useMemo(() => ({ user, logout }), [user])} prevents all consumers re-rendering.

When not to use them

  • For cheap calculations (a + b, a small map).
  • On every function "just in case": it makes code harder to read and costs memory and comparisons.
  • To fix bugs: they are a performance hint, not a guarantee; React may discard the cache.

React Compiler

React 19's compiler can memoise automatically, so in new projects manual useMemo/useCallback is needed less.

Summary

Use useMemo for expensive derived values and useCallback for functions passed to memoised children or used as dependencies; profile before adding them elsewhere.

Practice explaining this
Question 5 of 5

How do you create a custom hook?

Solution and explanation

What it is

A custom hook is a function whose name starts with use and which calls other hooks. It lets several components **share stateful logic** (not state itself: each component calling the hook gets its own copy).

Example: fetching data

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    fetch(url, { signal: controller.signal })
      .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
      .then(d => { setData(d); setError(null); })
      .catch(e => { if (e.name !== 'AbortError') setError(e); })
      .finally(() => setLoading(false));
    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

function JobsPage() {
  const { data, loading, error } = useFetch('/api/jobs?city=Pune');
  if (loading) return <Spinner />;
  if (error) return <p>{error.message}</p>;
  return <JobList jobs={data} />;
}

Example: debounced search input

function useDebounce(value, delay = 400) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query);
const { data } = useFetch(`/api/jobs?q=${encodeURIComponent(debouncedQuery)}`);

The API is called only after the user stops typing for 400 ms.

Other useful custom hooks

  • useLocalStorage(key, initial): state that persists in the browser.
  • useAuth(): reads the user and login/logout functions from context.
  • useOnClickOutside(ref, handler): closes dropdowns.

Rules

  • Name must start with use so the linter applies the rules of hooks.
  • Call hooks unconditionally at the top level inside it.
  • Return what callers need: a value, a tuple [value, setValue] or an object.

Summary

Custom hooks extract reusable stateful logic into a useX function; each caller gets independent state, and components stay short and readable.

Practice explaining this