Câu hỏi Reactjs Hay

Câu hỏi Reactjs Hay

 Câu hỏi Reactjs Hay



1️⃣ React Rendering Behavior

A. “Why does a component re-render?”

Short spoken answer:

“In React, a component re-renders whenever its inputs change: props, state, or any context it consumes. It can also re-render if its parent re-renders and React decides this subtree might need updating.”

Deeper points you can add:

A function component will re-run when:

  1. Its state changes (setState / setX from useState, useReducer).

  2. Its props change (from the parent’s render).

  3. Context values it uses change (useContext).

  4. Key changes (React treats it as a new component).

  5. External subscriptions (e.g. Redux, Zustand) indicate a change and cause a re-render of that connected component.

Important nuance:

“React always re-runs the render function when these inputs change, but that doesn’t mean the DOM always changes afterward. Reconciliation determines what actually updates in the DOM.”


B. “How does React decide when to skip rendering?”

Careful: two things here – re-running the component vs updating the DOM.

1. Skipping DOM updates (reconciliation):

“After rendering to virtual DOM, React diffs the new tree with the previous one. If an element has the same type and key and its props are shallow-equal, React reuses the existing DOM node and only updates what actually changed.”

Key rules:

  • Same element type + same key → React does a shallow prop diff and only updates changed props.

  • Different type or key → React throws away that subtree and recreates it.

2. Skipping the render call for a component:

“At the component level, React can skip calling the function entirely if I use optimizations like React.memo, PureComponent, or custom shouldComponentUpdate. They all rely on shallow comparison of props (and sometimes state).”

So you can say:

“By default, React will re-run the component whenever the parent re-renders, but it will try to minimize actual DOM changes via the diffing algorithm. If I want to avoid the render call itself, I use tools like React.memo and make sure my props are referentially stable.”


C. “React.memo, useMemo, useCallback — when (and when NOT) to use them?”

1. React.memo

How to explain:

React.memo is a higher-order component that memoizes the result of a component’s render. If the props are shallow-equal between renders, React skips calling that component.”

const UserRow = React.memo(function UserRow({ user, onEdit }) { // ... });

Use when:

  • The component:

    • Is pure (render output depends only on props).

    • Is re-rendering often due to parent updates.

    • Has non-trivial work inside (heavy tree, expensive calculations).

Avoid / be careful when:

  • Props are almost always changing → shallow compare cost + no benefit.

  • You pass new inline objects/arrays/functions all the time → memo is useless unless you also stabilize those references (useCallback, useMemo).

  • For very small, cheap components – extra comparison overhead can be worse than the re-render.


2. useMemo

How to explain:

useMemo memoizes a value. I use it for expensive calculations or to keep referential equality for derived data.”

const filteredUsers = useMemo( () => users.filter(u => u.active), [users] );

Use when:

  • You have expensive computation that would otherwise run on every render.

  • You need stable references for:

    • Derived arrays / objects used as props for memoized children.

    • Dependencies in useEffect or useCallback.

Avoid when:

  • The computation is cheap → useMemo adds overhead and complexity.

  • You use it everywhere “just in case” → this is a common anti-pattern.

You can say:

“I use useMemo as a targeted optimization after I observe a real performance issue, not as default boilerplate.”


3. useCallback

How to explain:

useCallback memoizes a function reference. It’s useful when passing callbacks to memoized children, or as stable dependencies for hooks like useEffect.”

const handleClick = useCallback(() => { doSomething(id); }, [id]);

Use when:

  • You pass a callback to a child wrapped with React.memo, memoized list items, etc.

  • A hook depends on a function (e.g. useEffect, useEventListener), and you need a stable reference to avoid re-running the effect unnecessarily.

Avoid when:

  • The function is not passed down or used in dependencies → no benefit.

  • You’re wrapping every handler by default → mental and performance overhead.

Nice one-liner:

React.memo skips component renders, useMemo memoizes values, and useCallback memoizes functions. I only use them where they reduce real work, not as decoration.”


2️⃣ How the Browser Works

A. “Critical Rendering Path – what is it?”

Good spoken answer:

“The Critical Rendering Path is the sequence the browser follows to turn HTML, CSS, and JS into pixels on the screen. Minimizing work on this path improves load performance and perceived speed.”

Steps you can list:

  1. Download & parse HTML → DOM

  2. Download & parse CSS → CSSOM

  3. Combine DOM + CSSOM → Render Tree

  4. Layout (calculate positions & sizes)

  5. Paint (draw pixels for each node)

  6. Composite (merge layers into the final image on screen)

Quick extra:

“Blocking CSS or synchronous JS can delay this pipeline, so in practice I care about reducing critical CSS, deferring non-critical scripts, and avoiding render-blocking resources.”


B. “Layout → Paint → Composite — what happens here?”

You can say:

“Once the browser has the render tree, it goes through three main steps per frame:

  1. Layout – compute geometry: positions, sizes, box model.

  2. Paint – draw text, borders, backgrounds, shadows for each element onto layers.

  3. Composite – the GPU or compositor combines layers and applies transforms to produce the final frame.”

Why it matters for you as a front-end dev:

  • Layout is expensive, especially when it cascades through big parts of the tree.

  • Paint is also relatively heavy (especially shadows, gradients, complex borders).

  • Compositing is cheaper, usually GPU-accelerated and isolated to layers.


C. “What triggers reflow (layout)?”

Reflow = layout. It happens when the browser needs to recalculate element geometry.

Examples that trigger reflow:

  • Changing layout-related styles:

    • width, height, margin, padding, border, position, display, font-size, etc.

  • Manipulating the DOM structure:

    • Adding/removing nodes, changing content, changing classes that affect layout.

  • Reading layout properties after writes (layout thrashing):

    • Accessing offsetHeight, getBoundingClientRect(), scrollHeight, etc., after changing styles → forces sync layout to give you the correct value.

Nice explanation:

“When I change something that affects box geometry, or I force the browser to measure layout right after a change, I trigger a reflow. On large pages or inside animations, that’s expensive, so I batch DOM writes and read layout carefully.”


D. “Why are transform + opacity preferred for animations?”

This is a classic performance question.

Answer like this:

“Animating transform and opacity is preferred because they can usually be done purely in the compositing step, without triggering layout or paint.”

Details you can mention:

  • Properties like top, left, width, height, margin:

    • Affect layout → reflow + repaint + composite on every frame.

  • Properties like background-color, box-shadow:

    • Don’t change layout but do require repaint + composite.

  • transform and opacity (when the element is on its own layer):

    • Can be updated by the compositor only, often via GPU.

    • No layout, no paint → smoother 60fps animations on lower-end devices.

So you can sum up:

“For performant animations I stick to transform (e.g. translate, scale) and opacity, let the browser promote that element to a layer, and avoid touching layout-affecting properties inside animations.”



====


💻 Coding Tasks 1️⃣ Implement debounce() from scratch function debounce(fn, delay) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } 2️⃣ Convert a nested category list into a flattened structure Tests recursion + data transformation + clean thinking.
ChatGPT said:

Nice, these are very “front-end interview” flavoured. Let’s turn them into answers you can code + explain confidently.


1️⃣ Implement debounce() from scratch

They already hinted at the classic version. You can both implement and explain it well.

Minimal implementation (what you already wrote, just cleaned):

function debounce(fn, delay) { let timerId; return function debounced(...args) { // preserve `this` for methods const context = this; clearTimeout(timerId); timerId = setTimeout(() => { fn.apply(context, args); }, delay); }; }

How to EXPLAIN this in the interview

debounce returns a wrapped version of the function. Every time it’s called, it clears the previous timer and sets a new one. The original function only runs if there are no new calls within the given delay. This is useful for things like search-as-you-type, resize, or scroll handlers to avoid running expensive logic on every event.”

Then add one line that shows seniority:

“I also preserve this using apply, so it works correctly with object methods, not just standalone functions.”

Slightly more advanced version (leading vs trailing)

If they push you or you want to impress:

function debounce(fn, delay, options = { leading: false, trailing: true }) { let timerId = null; let lastArgs; let lastThis; let calledLeading = false; return function debounced(...args) { lastArgs = args; lastThis = this; const callNow = options.leading && !timerId; clearTimeout(timerId); timerId = setTimeout(() => { timerId = null; if (options.trailing && !callNow) { fn.apply(lastThis, lastArgs); } calledLeading = false; }, delay); if (callNow && !calledLeading) { calledLeading = true; fn.apply(lastThis, lastArgs); } }; }

Explanation:

“Sometimes I want the function to fire immediately on the first call, not after the delay. That’s leading. trailing covers the usual case where it runs after the quiet period. The extended version supports both, similar to Lodash’s debounce.”

That’s more than enough to look very comfortable here.


2️⃣ Flatten a nested category list

Typical structure they give you might look like this:

const categories = [ { id: 1, name: "Electronics", children: [ { id: 2, name: "Phones", children: [] }, { id: 3, name: "Laptops", children: [{ id: 4, name: "Gaming Laptops", children: [] }] } ] }, { id: 5, name: "Books", children: [{ id: 6, name: "Fiction", children: [] }] } ];

Say they want a flat array like:

[ { id: 1, name: "Electronics", parentId: null }, { id: 2, name: "Phones", parentId: 1 }, { id: 3, name: "Laptops", parentId: 1 }, { id: 4, name: "Gaming Laptops", parentId: 3 }, { id: 5, name: "Books", parentId: null }, { id: 6, name: "Fiction", parentId: 5 } ]

Recursive solution (clean & interview-friendly)

function flattenCategories(nodes, parentId = null, result = []) { for (const node of nodes) { const { id, name, children = [] } = node; result.push({ id, name, parentId }); if (children.length > 0) { flattenCategories(children, id, result); } } return result; }

Usage:

const flat = flattenCategories(categories); console.log(flat);

How to explain it

“I treat the structure as a tree. I do a depth-first traversal: for each node, I push a simplified record {id, name, parentId} into the result, then recursively process its children with the current id as parentId. Time complexity is O(n) because each node is visited exactly once.”

You can add:

“I pass the result array down the recursion to avoid repeated concatenation and unnecessary allocations.”

Iterative version (if they ask “without recursion”)

function flattenCategoriesIterative(nodes) { const result = []; const stack = nodes.map(node => ({ node, parentId: null })); while (stack.length > 0) { const { node, parentId } = stack.pop(); const { id, name, children = [] } = node; result.push({ id, name, parentId }); // push children in reverse to keep original order for (let i = children.length - 1; i >= 0; i--) { stack.push({ node: children[i], parentId: id }); } } return result; }

Explanation:

“This is the same DFS logic but using an explicit stack instead of the call stack. It avoids recursion depth issues on very deep trees.”


If you want, next we can prep:

  • A throttle implementation (often asked together with debounce)

  • More data-transformation tasks (group-by, tree-building from flat list, etc.)

  • A short “narrative” where you walk through your code out loud like in a real interview.

 

Comment