All jobs / Full-stack developer
Frontend interview questions
Frontend questions a Full-stack developer can be asked, from first job to senior. Open one to see the strongest answer and why it works.
What is JSX, and what does the browser actually receive?
The strongest answer: A syntax for describing UI that compiles to function calls creating element objects; the browser never sees JSX — a build step turns it into plain JavaScript
Why it works: Right. Sugar over createElement; the compiler does the translation.
- JSX compiles to element-creating calls.
- Expressions in braces are JavaScript.
- className, not class.
- A build step is required.
What is the difference between props and state?
The strongest answer: Props are inputs passed from a parent and read-only in the child; state is data a component owns and changes over time, and changing it re-renders the component
Why it works: Right. Owned and mutable versus given and read-only.
- Props: passed in, read-only.
- State: owned, changed with a setter.
- Setting state re-renders.
- Lift state up when siblings share it.
What does useState return, and why must you call the setter instead of assigning?
The strongest answer: A pair: the current value and a setter; React re-renders only when the setter is called, so assigning to the variable changes nothing on screen
Why it works: Right. The setter is how React learns something changed.
- const [value, setValue] = useState(initial).
- Assigning the variable does nothing.
- Setter schedules a re-render.
- Functional updates when the next depends on the previous.
Why does React warn about a missing key when you render a list?
The strongest answer: Keys let React match each rendered item to its previous version across renders, so reordering, inserting or deleting does not scramble state and DOM; a stable, unique id per item is the right key, not the array index
Why it works: Right. Identity across renders; the index breaks when the list changes order.
- Key = stable identity per item.
- Use ids, not the index.
- Index keys break on reorder or delete.
- Keys must be unique among siblings.
What is a controlled input?
The strongest answer: An input whose value comes from state and whose onChange updates that state, so React is the single source of truth for what the field shows
Why it works: Right. Value from state, change into state.
- value={state} onChange={set}.
- React owns the field's value.
- Uncontrolled: the DOM owns it, read via ref.
- Do not switch between the two.
The first screen is a list loaded from `GET /items`.
What states does the screen need?
The strongest answer: Loading, empty, error and data — and the error must be retryable, not a blank page
Why it works: Exactly. Those four states are the contract of any GET. Skipping one is how you ship a flicker or a lie.
- Every fetch has loading, empty, error and data.
- Do not flash an empty list while the request is in flight.
- Errors get a message and a retry.
- The happy path is one of four, not the only one.
What is useEffect for?
The strongest answer: Running side effects after render — fetching data, subscribing, syncing with something outside React — with a cleanup function and a dependency list that says when to run again
Why it works: Right. After render, for things React itself does not do.
- Effects run after render.
- Return a cleanup.
- Dependencies decide when it re-runs.
- Not for deriving state.
What happens if useEffect has an empty dependency array, and if it has none at all?
The strongest answer: Empty array: runs once after the first render; no array: runs after every render; the array lists the values the effect reads so it re-runs when they change
Why it works: Right. The array is the list of things the effect depends on.
- []: after the first render only.
- No array: after every render.
- [a, b]: when a or b changes.
- Lint keeps the list honest.
Saving a profile can fail with a 400 (`email taken`) or a 500.
What does the UI show?
The strongest answer: A field message for a 400 the API explained, and a retryable 'something went wrong' for a 500 — never the stack trace
Why it works: Exactly. 400s are often the user's to fix; 500s are ours. The stack trace is for logs, not the page.
- Map status to a human next step, not to a dump.
- 400 with a field: show it on that field.
- 500 / network: apologise and offer retry — no internals.
- Never leave the button spinning; always end the request in the UI.
How do you render something only when a condition holds?
The strongest answer: A JavaScript expression in JSX: cond && <X />, a ternary for two branches, or an early return — JSX is expressions, so ordinary logic works
Why it works: Right. Expressions in braces; if statements go above the return.
- && for one branch, ternary for two.
- Early return for the whole component.
- Watch 0 && rendering a zero.
- CSS hiding is not conditional rendering.
Why does onClick={handleClick()} fire during render instead of on click?
The strongest answer: Because the parentheses call the function immediately while rendering; pass the function itself — onClick={handleClick} — or an arrow that calls it
Why it works: Right. Pass a function, do not call it.
- Pass the function, not its result.
- Arrow when you need arguments.
- Events are camelCase.
- The event object is React's synthetic event.
Two sibling components need the same piece of data. Where does it live?
The strongest answer: In their closest common parent, passed down as props with a setter passed to whoever changes it — lifting state up
Why it works: Right. One owner above both.
- Lift to the common parent.
- Pass value down, setter down.
- One source of truth.
- Context when the prop chain gets long.
Why does a component return a fragment instead of a wrapping div?
The strongest answer: A component must return one root; a fragment groups children without adding a DOM node, so the layout and CSS are not disturbed by a wrapper that exists only for React
Why it works: Right. Group without a wrapper element.
- One root per return.
- Fragment adds no DOM node.
- <>...</> shorthand.
- Keyed fragments in lists.
Why does items.push(x); setItems(items) not re-render?
The strongest answer: Because the array is the same object and React compares by reference; create a new array — setItems([...items, x]) — so the reference changes
Why it works: Right. New object, new reference, re-render.
- State is compared by reference.
- Never mutate state in place.
- Spread into a new array or object.
- Same for nested objects.
The user submits 'create project'. A teammate wants the new card to appear before the response.
When do you update the list?
The strongest answer: After the API confirms, unless we designed a true optimistic path that can undo — a create is not a toggle
Why it works: Exactly. Optimistic UI is a technique with rollback. Blindly painting a create that failed is a lie.
- Writes wait for the server, or they optimistic-and-rollback.
- Creates and payments wait.
- Toggles can be optimistic if you can undo.
- Never invent a success the API did not send.
The API process is down. The UI still loads.
What should the UI do?
The strongest answer: Show a clear outage state with retry, and do not pretend cached or empty data is live — the API being down is a product event
Why it works: Exactly. An outage that looks like 'you have no projects' or 'you were logged out' is a worse bug than a banner.
- Down ≠ empty ≠ logged out.
- Name the outage and offer retry.
- Keep the last good data only if you label it stale.
- Never send people to login because the API process died.
A Next-style SSR page flashes and React logs a hydration mismatch on the header.
What do you fix first?
The strongest answer: Make the first client render match the HTML the server sent — same data, same locale, no `window`-only branches on that path
Why it works: Exactly. Hydration is a diff against the server HTML. Auth, clocks and locale are the usual liars — give the client the same snapshot.
- First client paint must equal server HTML.
- Pass the same user, locale and time snapshot.
- Keep `window` and `localStorage` out of that first render.
- Do not paper over the warning.
A long profile form is open. The user hits Save and the network is slow.
What does the form do?
The strongest answer: Disable submit while in flight, keep the draft if the call fails, and warn before leave if they still have unsaved edits
Why it works: Exactly. Three bugs, one widget: double-submit, lost draft, and a closed tab. Handle all three.
- Lock the button for the request.
- Keep the draft on failure.
- Warn on leave if dirty.
- Do not empty the form on click.
Two people edit the same note. The second save returns 409 Conflict.
What does the UI do with 409?
The strongest answer: Show their version and the server version, and ask which wins — do not silently overwrite and do not retry the same body
Why it works: Exactly. 409 means two writers. The product decision is merge or pick — not a tighter loop.
- 409 is a conversation, not a retry.
- Show both copies or a merge.
- Send an ETag / version on the next attempt.
- Do not discard in-progress edits.
A teammate throws inside `fetch` so the React error boundary can show the outage.
What is the difference?
The strongest answer: An error boundary catches render throws; a failed fetch is data you must handle in the request path — do not throw in the fetch to 'reuse' the boundary for an API 500
Why it works: Exactly. Boundaries are for unexpected render failures. A 500 is an expected state of a request. Mixing them turns every outage into a white screen.
- Boundary = render/unknown.
- Fetch = loading/error/data.
- Do not throw on HTTP to be 'clever'.
- Keep the rest of the page alive.
A burst of autosaves returns 429 from the API gateway.
What does the UI do with 429?
The strongest answer: Back off using `Retry-After` (or a jittered default), tell the user we are slowing down, and do not tight-loop the same write
Why it works: Exactly. 429 is a contract: wait. The header is the join between gateway and spinner. A tight loop is how you DDoS yourself.
- Honour `Retry-After`.
- Jitter if the header is missing.
- Explain the wait; do not login-bounce.
- Never tight-loop a 429.