\n\n\n`;\n\nconst serve = (ctx) => ctx.route('http://app.test/**', (route) => {\n const u = new URL(route.request().url());\n if (u.pathname === '/prefs.js') return route.fulfill({ contentType: 'text/javascript', body: prefsJs });\n return route.fulfill({ contentType: 'text/html', body: html });\n});\n\nfor (const bt of [chromium, firefox, webkit]) {\n const browser = await bt.launch();\n const ctx = await browser.newContext();\n await serve(ctx);\n const a = await ctx.newPage();\n await a.goto('http://app.test/');\n await a.waitForFunction(() => window.api);\n\n // Round trip, defaults and validation.\n const r1 = await a.evaluate(() => {\n const { loadPrefs, savePrefs } = window.api;\n const first = loadPrefs(); // nothing stored yet\n savePrefs({ theme: 'dark', fontSize: 18 });\n return { first, saved: loadPrefs() };\n });\n assert.deepEqual(r1.first, { theme: 'light', fontSize: 16 });\n assert.deepEqual(r1.saved, { theme: 'dark', fontSize: 18 });\n\n // Corrupted and hostile values.\n const bad = await a.evaluate(() => {\n const { loadPrefs } = window.api;\n const out = {};\n for (const [name, text] of [['truncated', '{\"theme\":\"da'], ['wrongType', '\"hello\"'], ['null', 'null'], ['badFields', '{\"theme\":\"neon\",\"fontSize\":\"huge\",\"extra\":1}'], ['partial', '{\"fontSize\":20}']]) {\n localStorage.setItem('prefs:v1', text);\n out[name] = loadPrefs();\n }\n return out;\n });\n assert.deepEqual(bad.truncated, { theme: 'light', fontSize: 16 });\n assert.deepEqual(bad.wrongType, { theme: 'light', fontSize: 16 });\n assert.deepEqual(bad.null, { theme: 'light', fontSize: 16 });\n assert.deepEqual(bad.badFields, { theme: 'light', fontSize: 16 });\n assert.deepEqual(bad.partial, { theme: 'light', fontSize: 20 });\n\n // First paint: the head script has already set data-theme when the body starts.\n await a.evaluate(() => window.api.savePrefs({ theme: 'dark', fontSize: 18 }));\n await a.reload();\n assert.equal(await a.evaluate(() => window.themeAtBodyStart), 'dark');\n\n // Another tab writes; this tab hears about it.\n const b = await ctx.newPage();\n await b.goto('http://app.test/');\n await b.waitForFunction(() => window.api);\n await b.evaluate(() => window.api.savePrefs({ theme: 'light', fontSize: 24 }));\n await a.waitForFunction(() => window.seen.length >= 1);\n assert.deepEqual(await a.evaluate(() => window.seen.at(-1)), { theme: 'light', fontSize: 24 });\n assert.equal(await b.evaluate(() => window.seen.length), 0, 'the writing tab gets no storage event');\n\n // clear() in another tab fires one event whose key is null; this tab resets to defaults.\n const before = await a.evaluate(() => window.seen.length);\n await b.evaluate(() => localStorage.clear());\n await a.waitForFunction((n) => window.seen.length > n, before);\n assert.deepEqual(await a.evaluate(() => window.seen.at(-1)), { theme: 'light', fontSize: 16 });\n\n // Storage that throws on every call: reads fall back to defaults, writes report false, memory keeps the value.\n const blocked = await a.evaluate(() => {\n const names = ['getItem', 'setItem'];\n const saved = names.map((n) => Storage.prototype[n]);\n names.forEach((n) => { Storage.prototype[n] = () => { throw new DOMException('blocked', 'SecurityError'); }; });\n const { loadPrefs, savePrefs } = window.api;\n const defaults = loadPrefs();\n const ok = savePrefs({ theme: 'dark', fontSize: 20 });\n const fromMemory = loadPrefs();\n names.forEach((n, i) => { Storage.prototype[n] = saved[i]; });\n return { defaults: defaults.theme, ok, fromMemory };\n });\n assert.deepEqual(blocked.defaults, 'light');\n assert.equal(blocked.ok, false);\n assert.deepEqual(blocked.fromMemory, { theme: 'dark', fontSize: 20 });\n\n // Storage works again: the next successful write clears the in-memory copy.\n const recovered = await a.evaluate(() => ({ ok: window.api.savePrefs({ theme: 'light', fontSize: 12 }), loaded: window.api.loadPrefs() }));\n assert.equal(recovered.ok, true);\n assert.deepEqual(recovered.loaded, { theme: 'light', fontSize: 12 });\n\n // The HTML specification puts the SecurityError on the localStorage getter itself, so also make the property access throw.\n const noProperty = await a.evaluate(() => {\n const saved = Object.getOwnPropertyDescriptor(window, 'localStorage');\n Object.defineProperty(window, 'localStorage', { configurable: true, get() { throw new DOMException('blocked', 'SecurityError'); } });\n const { loadPrefs, savePrefs } = window.api;\n const defaults = loadPrefs();\n const ok = savePrefs({ theme: 'dark', fontSize: 22 });\n const fromMemory = loadPrefs();\n if (saved) Object.defineProperty(window, 'localStorage', saved); else delete window.localStorage;\n return { defaults, ok, fromMemory, restored: window.api.savePrefs({ theme: 'light', fontSize: 12 }) };\n });\n assert.deepEqual(noProperty.defaults, { theme: 'light', fontSize: 16 });\n assert.equal(noProperty.ok, false);\n assert.deepEqual(noProperty.fromMemory, { theme: 'dark', fontSize: 22 });\n assert.equal(noProperty.restored, true);\n\n // Quota: fill storage with a filler key until setItem throws, then check savePrefs reports false.\n const quota = await a.evaluate(() => {\n localStorage.clear();\n let errName = null, n = 0;\n // Fill with shrinking chunks until even a few bytes no longer fit.\n for (let size = 1024 * 1024; size >= 1; size = Math.floor(size / 2)) {\n for (;;) {\n try { localStorage.setItem('filler' + n++, 'x'.repeat(size)); } catch (e) { errName = e.name; break; }\n }\n }\n const i = n;\n const ok = window.api.savePrefs({ theme: 'dark', fontSize: 14 });\n localStorage.clear();\n return { errName, ok, fillerKeys: i };\n });\n assert.equal(quota.ok, false);\n console.log(bt.name(), JSON.stringify({ quotaErrorName: quota.errName, saveReturnedOnFullStorage: quota.ok, storageBlocked: { writeOk: blocked.ok, readBack: blocked.fromMemory }, propertyBlocked: { writeOk: noProperty.ok, readBack: noProperty.fromMemory } }));\n await browser.close();\n}\nconsole.log('all preferences checks passed');\n\nOutput (the quota error name is what each engine throws when storage is full):\n\nchromium {\"quotaErrorName\":\"QuotaExceededError\",\"saveReturnedOnFullStorage\":false,\"storageBlocked\":{\"writeOk\":false,\"readBack\":{\"theme\":\"dark\",\"fontSize\":20}},\"propertyBlocked\":{\"writeOk\":false,\"readBack\":{\"theme\":\"dark\",\"fontSize\":22}}}\nfirefox {\"quotaErrorName\":\"QuotaExceededError\",\"saveReturnedOnFullStorage\":false,\"storageBlocked\":{\"writeOk\":false,\"readBack\":{\"theme\":\"dark\",\"fontSize\":20}},\"propertyBlocked\":{\"writeOk\":false,\"readBack\":{\"theme\":\"dark\",\"fontSize\":22}}}\nwebkit {\"quotaErrorName\":\"QuotaExceededError\",\"saveReturnedOnFullStorage\":false,\"storageBlocked\":{\"writeOk\":false,\"readBack\":{\"theme\":\"dark\",\"fontSize\":20}},\"propertyBlocked\":{\"writeOk\":false,\"readBack\":{\"theme\":\"dark\",\"fontSize\":22}}}\nall preferences checks passed\n\nTrade-offs and pitfalls\n\nStore only what is small and non-sensitive. localStorage is synchronous (it blocks the main thread while it reads or writes), readable by any script on the origin, and sent nowhere automatically; never put tokens or personal data in it.\nDo not trust the stored value because you wrote it. Users, extensions and older app versions edit it; the validation is the real defence.\nFeature detection by typeof localStorage is not enough. The object can exist and still throw on use, which is why the code wraps the calls and does not just check for the property.\nWrite on change, not on every keystroke, and debounce sliders such as font size (wait until the user has stopped moving the slider for a short moment, then write once).\nUse a cookie when the server needs the value. localStorage and sessionStorage both stay in the browser and are never sent with requests, so neither helps here. A server-rendered page cannot read localStorage, so a theme needed in the first HTML response belongs in a cookie.\nComplexity: each read or write is O(size of the JSON string), which here is a few dozen bytes.\n\nRunning the code\n\nmkdir prefs && cd prefs # save prefs.js and test-prefs.mjs here\necho '{\"type\":\"module\"}' > package.json\nnpm i playwright@1.48.2 && npx playwright install chromium firefox webkit\nnode test-prefs.mjs\n\nRun in the mcr.microsoft.com/playwright:v1.48.2-jammy container (Node 20), where the browsers are already installed."}},{"@type":"Question","name":"What metrics are commonly used as autoscaling triggers, both reactive and predictive? Weigh the pros and cons of CPU, memory, request rate, end-to-end latency, and custom application metrics like queue length or pending jobs as autoscaler inputs.","acceptedAnswer":{"@type":"Answer","text":"Direct answer\n\nReactive autoscaling scales based on what's happening right now (a metric crosses a threshold); predictive autoscaling scales based on a forecast of what's about to happen. Neither is inherently better; they answer different questions, and the metric you feed either one matters more than which mode you pick. The strongest signals are the ones closest to actual user-facing backlog or experience (request latency, queue depth) rather than machine-level resource stats (CPU, memory), which correlate with load but can lag or miss it entirely for I/O-bound services.\n\nStructured elaboration\n\nMetric | What it measures | Strengths | Weaknesses\n\nCPU utilization | Compute saturation on the instance | Simple, available everywhere, correlates well with compute-bound work | Noisy on short bursts; a poor signal for I/O-bound or network-bound services; can lag actual user impact\nMemory usage | Memory pressure on the instance | Stable, slow-changing; catches memory-bound workloads and prevents out-of-memory failures | Changes too slowly to trigger timely scale-out for a fast spike; conservative thresholds lead to overprovisioning\nRequest rate (RPS/QPS, requests or queries per second) | Incoming demand volume | Direct measure of load; maps naturally to concurrency for stateless services | Doesn't capture that requests vary wildly in cost; needs to be split by endpoint or payload size to stay meaningful\nEnd-to-end latency | What the user actually experiences | Aligned with the service level objective (SLO, the measurable target you've committed to, e.g. \"P95 (95th-percentile) under 300ms\"); can catch saturation that CPU/memory miss (e.g., a downstream dependency slowing down) | Reactive scaling on latency is inherently a lagging response, since latency has already degraded by the time it crosses a threshold; affected by factors outside the service's own control\nCustom metrics (queue length, pending jobs) | Actual backlog of work waiting to be done | Often the most predictive signal available, since it reflects the work still outstanding, not just current resource pressure; enables precise \"workers needed\" math | Requires instrumentation; stale or delayed metric reporting produces bad scaling decisions; adds a dependency the autoscaler now trusts\n\nReactive vs. predictive, and why the metric choice compounds. A reactive policy watching CPU will always be a step behind, because CPU rises only after load has already increased. A reactive policy watching queue depth is closer to real time, because a growing backlog is itself the leading indicator of \"we're falling behind,\" not a downstream symptom of it. Predictive scaling (using historical traffic patterns or short-term forecasting to act before load arrives) reduces this lag structurally, but it only works well when it's built on a metric with a real predictable pattern (daily/weekly seasonality in request rate, for instance); it doesn't fix the fact that CPU is a noisy, lagging signal to forecast against in the first place.\n\nTarget-tracking math. A common reactive scaling pattern (used by cloud provider target-tracking policies and by Kubernetes' Horizontal Pod Autoscaler alike) scales replica count proportionally to how far the current metric is from target:\n\n \n\nIf 10 replicas are running at 80% average CPU against a 50% target:\n\n \n\nThe policy adds 6 replicas in this step. This same formula works for any metric with a sensible linear relationship to load (request rate, queue depth per worker), which is why the choice of which metric feeds it matters more than the formula itself: feed it a noisy or lagging metric and it will confidently compute the wrong target.\n\nWorked example\n\nA web-facing auto scaling group (ASG) targets 50% CPU with a 60-second scale-out cooldown and a 300-second scale-in cooldown, intentionally asymmetric: react to a spike quickly, but wait much longer before removing capacity. Without that asymmetry, consider what happens if CPU oscillates naturally between 45% and 65% around a single 50% threshold with equal cooldowns on both directions: the group scales out when CPU ticks above 50%, the added capacity immediately drags average CPU back below 50%, the group scales back in, CPU climbs again, and the fleet thrashes (adds and removes instances repeatedly) without ever settling. The fix has two parts: a hysteresis band (scale out above, say, 70%; scale in below, say, 30%, rather than a single shared threshold) so normal noise doesn't cross both boundaries, and a longer scale-in cooldown than scale-out cooldown, so the group is quick to protect against real load but slow and conservative about giving capacity back. That second part is the \"safe scale-down\" half of the policy: it's just as important as reacting fast to a spike, because a policy that scales in as aggressively as it scales out will flap on exactly the kind of noise a real production CPU curve always has.\n\nTrade-offs & pitfalls\n\nRelying on a single metric is brittle: a service can be CPU-healthy while its queue backs up (I/O-bound work) or CPU-saturated while user latency is fine (CPU-bound but well within budget); combining two or three signals catches what any one alone would miss.\nCustom metrics add real value but add a real dependency: if the metric pipeline itself lags or goes stale, the autoscaler is making decisions on old data and can either overreact to a stale spike or fail to react to a live one.\nSymmetric cooldowns (same wait time for scale-out and scale-in) are a common oscillation trap; scale-in should almost always be more conservative than scale-out.\nPredictive scaling built on a metric with no real seasonal pattern (or one whose pattern just changed, e.g., after a product launch) will confidently mis-forecast; it needs the reactive layer as a backstop, not as a replacement."}},{"@type":"Question","name":"A useEffect fetches data using a prop but keeps showing results for the previous prop value. Explain how the dependency list works, what bugs missing or extra dependencies cause, and how you would debug it.","acceptedAnswer":{"@type":"Answer","text":"Direct answer\n\nStale results after a prop change (the screen shows data for an earlier input) have two possible causes that look identical on screen: the effect did not re-run (a missing dependency), or it did re-run but an older response arrived last and overwrote the newer one (a race condition, no cleanup). An effect is a function React runs after the screen has updated, used for work outside rendering such as fetching. The dependency list is the array of reactive values (props, state and anything declared in the component body) that the effect reads; after each render React compares every entry with its previous value using Object.is and re-runs the effect only if one differs. A correct fetch effect lists every value it reads, and returns a cleanup function that marks the old request as obsolete. Debug in this order: did the effect re-run (a new request in the Network panel)? If not, fix the dependencies. If it did, were the responses out of order? If so, add cleanup.\n\nWhat the dependency list does\n\nReact's documentation: the dependencies are \"the list of all reactive values referenced inside of the setup code,\" and \"React will compare each dependency with its previous value using the Object.is comparison. If you omit this argument, your Effect will re-run after every commit of the component.\" (A commit is the step where React applies a render's result to the screen.) So there are three shapes:\n\n[id]: re-run when id changes (what a fetch keyed on a prop needs).\n[]: run once after mount. If the effect reads id, it keeps using the id from the first render forever. This is the \"closure\" problem: a closure is a function together with the variables it captured where it was created, so a function created in a render captures that render's values, and an effect that never re-runs keeps seeing old ones (a stale closure).\nNo array: run after every render.\n\nReproducing each bug\n\nThe program below uses a fake API whose responses the test resolves by hand, so the order is controlled (no timers, no real network). It renders one component per failure mode. The first three components differ only in their dependency list and cleanup. Reading the harness: fetchItem returns a promise that stays unresolved until the test calls the matching function stored in pending[id], and it records every call in calls and every cancellation in aborted. act(...) from Testing Library runs the code inside it and applies the resulting React updates before returning, so the next line sees the updated screen; flush is a small helper that wraps a resolve call in act and waits one promise tick. mock.timers.enable from node:test replaces setInterval with a fake clock that only advances when the test calls mock.timers.tick(1000), so case 5 needs no real waiting.\n\nimport { JSDOM } from 'jsdom';\nimport { mock } from 'node:test';\nimport assert from 'node:assert/strict';\nconst dom = new JSDOM('', { url: 'http://localhost' });\nglobalThis.window = dom.window; globalThis.document = dom.window.document;\nglobalThis.IS_REACT_ACT_ENVIRONMENT = true;\nconst React = (await import('react')).default;\nconst { render, act, cleanup, waitFor } = await import('@testing-library/react');\nconst { useState, useEffect, StrictMode } = React;\nconst h = React.createElement;\n\n// Fake API: every call returns a promise the test resolves by hand, so response order is under control.\nlet calls, pending, aborted;\nconst reset = () => { calls = []; pending = {}; aborted = []; };\nconst fetchItem = (id, signal) => new Promise((resolve) => {\n calls.push(id);\n pending[id] = (v) => resolve(v);\n signal?.addEventListener('abort', () => aborted.push(id));\n});\n\n// 1. Missing dependency: effect reads id but the list is []\nfunction MissingDep({ id }) {\n const [data, setData] = useState('loading');\n useEffect(() => { fetchItem(id).then(setData); }, []); // bug: [] although id is used\n return h('p', null, id=${id} shows ${data});\n}\n// 2. Right dependency, no cleanup: responses can arrive out of order\nfunction NoCleanup({ id }) {\n const [data, setData] = useState('loading');\n useEffect(() => { fetchItem(id).then(setData); }, [id]);\n return h('p', null, id=${id} shows ${data});\n}\n// 3. Right dependency + cleanup: ignore flag and AbortController\nfunction Fixed({ id }) {\n const [data, setData] = useState('loading');\n useEffect(() => {\n let ignore = false;\n const ctrl = new AbortController();\n setData('loading');\n fetchItem(id, ctrl.signal).then((r) => { if (!ignore) setData(r); });\n return () => { ignore = true; ctrl.abort(); };\n }, [id]);\n return h('p', null, id=${id} shows ${data});\n}\n// 4. Extra dependency: an object created in render is a new value every time\nlet loopCalls = 0;\nfunction ObjectDep({ id, objectDep }) {\n const [data, setData] = useState(null);\n const params = { id }; // new object on every render\n useEffect(() => {\n loopCalls++;\n if (loopCalls > 25) return; // safety stop so the demo terminates\n Promise.resolve({ id, fetchedAt: loopCalls }).then(setData); // a fresh object each time, so state changes\n }, objectDep ? [params] : [id]);\n return h('p', null, fetches so far ${loopCalls}, data ${data ? data.id : '-'});\n}\n// 5. Stale closure: an interval created once reads count from the first render\nconst logged = [];\nfunction StaleClosure({ deps }) {\n const [count, setCount] = useState(0);\n useEffect(() => { const t = setInterval(() => logged.push(${deps}:${count}), 1000); return () => clearInterval(t); },\n deps === 'empty' ? [] : [count]);\n return h('button', { onClick: () => setCount((c) => c + 1) }, 'inc');\n}\n\nconst text = (c) => c.querySelector('p').textContent;\nconst flush = (fn) => act(async () => { fn(); await Promise.resolve(); });\n\n// 1\nreset();\nlet r = render(h(MissingDep, { id: 1 }));\nawait flush(() => pending1);\nr.rerender(h(MissingDep, { id: 2 }));\nconsole.log('1 missing dep :', text(r.container), '| fetches:', calls.join(','));\nassert.equal(text(r.container), 'id=2 shows data-1'); assert.deepEqual(calls, [1]);\ncleanup();\n\n// 2 and 3: id 1 is slow, id 2 is fast; responses arrive 2 then 1\nfor (const [name, C] of [['2 no cleanup ', NoCleanup], ['3 ignore + abort ', Fixed]]) {\n reset();\n r = render(h(C, { id: 1 }));\n r.rerender(h(C, { id: 2 }));\n await flush(() => pending2);\n await flush(() => pending1);\n console.log(name + ':', text(r.container), '| fetches:', calls.join(','), '| aborted:', aborted.join(',') || 'none');\n if (C === NoCleanup) assert.equal(text(r.container), 'id=2 shows data-1');\n else { assert.equal(text(r.container), 'id=2 shows data-2'); assert.deepEqual(aborted, [1]); }\n cleanup();\n}\n\n// 3a: the reset line. After id 1 has loaded, switching to id 2 must not show data-1 while id 2 is pending\nreset();\nr = render(h(Fixed, { id: 1 }));\nawait flush(() => pending1);\nr.rerender(h(Fixed, { id: 2 }));\nconsole.log('3a reset on change :', text(r.container), '| fetches:', calls.join(','));\nassert.equal(text(r.container), 'id=2 shows loading');\ncleanup();\n\n// 3b: StrictMode runs setup, cleanup, setup again on mount\nfor (const [name, C] of [['3b StrictMode, Fixed ', Fixed], ['3b StrictMode, NoCleanup', NoCleanup]]) {\n reset();\n r = render(h(StrictMode, null, h(C, { id: 7 })));\n console.log(name + ':', 'fetches:', calls.join(','), '| aborted:', aborted.join(',') || 'none');\n assert.deepEqual(calls, [7, 7]); assert.deepEqual(aborted, C === Fixed ? [7] : []);\n cleanup();\n}\n\n// 4\nfor (const objectDep of [true, false]) {\n loopCalls = 0;\n r = render(h(ObjectDep, { id: 5, objectDep }));\n await waitFor(() => assert.ok(objectDep ? loopCalls >= 25 : loopCalls >= 1));\n await act(async () => { await new Promise((res) => setTimeout(res, 50)); });\n console.log(4 ${objectDep ? '[params] object dep' : '[id] primitive dep '}:, 'effect ran', loopCalls, 'times for one id');\n if (objectDep) assert.ok(loopCalls > 25); else assert.equal(loopCalls, 1);\n cleanup();\n}\n\n// 5\nmock.timers.enable({ apis: ['setInterval'] });\nfor (const deps of ['empty', 'count']) {\n r = render(h(StaleClosure, { deps }));\n const btn = r.container.querySelector('button');\n await act(async () => { btn.click(); btn.click(); }); // count becomes 2\n act(() => mock.timers.tick(1000));\n cleanup();\n}\nmock.timers.reset();\nconsole.log('5 interval logs :', logged.join(' | '));\nassert.deepEqual(logged, ['empty:0', 'count:2']);\nconsole.log('all assertions passed');\n\nRun in a node:22 container with the packages under \"Running the code\", it prints:\n\n1 missing dep : id=2 shows data-1 | fetches: 1\n2 no cleanup : id=2 shows data-1 | fetches: 1,2 | aborted: none\n3 ignore + abort : id=2 shows data-2 | fetches: 1,2 | aborted: 1\n3a reset on change : id=2 shows loading | fetches: 1,2\n3b StrictMode, Fixed : fetches: 7,7 | aborted: 7\n3b StrictMode, NoCleanup: fetches: 7,7 | aborted: none\n4 [params] object dep: effect ran 26 times for one id\n4 [id] primitive dep : effect ran 1 times for one id\n5 interval logs : empty:0 | count:2\nall assertions passed\n\nThe lines read as follows.\n\n1. Missing dependency ([] although the effect reads id): the prop changed to 2 but the screen shows data-1 and only one request was ever made. This is the symptom in the question; the Network panel shows no second request.\n2. Right dependency, no cleanup: two requests (ids 1 and 2). Id 1 was slow, id 2 fast, responses arrived 2 then 1, and the late response for the old prop overwrote the newer data: id=2 shows data-1. The Network panel shows both requests and the older one finishing last. In real apps this appears on slow or flaky connections and when typing quickly in a search box.\n3. Right dependency + cleanup: the screen shows data-2. The cleanup of the previous effect set ignore = true (so its .then does nothing) and aborted its AbortController (an object whose signal lets the request be cancelled; the log shows request 1 was aborted). React's useEffect page shows this same ignore pattern and says it \"ensures your code doesn't suffer from 'race conditions': network responses may arrive in a different order than you sent them.\"\n3a. Reset on change: once id 1 has loaded and the prop becomes 2, the setData('loading') line makes the screen read id=2 shows loading while request 2 is pending. Deleting that line leaves id=2 shows data-1 on screen until request 2 answers, which is the symptom in the question even though the effect re-ran and no race occurred.\n3b. StrictMode: StrictMode is a wrapper component that, in development builds only, runs each effect's setup, cleanup, then setup again once on mount to expose missing cleanups. The log shows two fetches for id 7 in both components. With the cleanup, the first one is aborted and its result ignored, so the doubled setup is harmless. Without it (NoCleanup), aborted stays empty and both requests remain live, with nothing to stop the first from writing its result.\n4. Extra or unstable dependency: [params], where params = { id } is created in the component body, is a new object every render, so Object.is says it changed on every render. Since the effect sets state, which renders again, which creates a new object, the effect re-runs in a loop. Step by step: render 1 builds params object A, the effect runs (run 1) and queues setData with a fresh object, so state changes; render 2 builds object B, which is not Object.is-equal to A, so run 2 starts and again queues a fresh object; render 3 builds object C, and so on. The demo's own cap stopped it at 26 runs for a single id; without the cap it continues until something breaks (rate limits, a frozen tab). With the primitive [id] as the dependency, the effect ran once.\n5. Stale closure in a timer: an interval created in an effect with [] reads count from the first render and logs empty:0 after two clicks; with [count] the effect is re-created each time and logs count:2.\n\nRemoving the technique in the demo is cases 1 and 2: no dependency, or dependency without cleanup, and both assertions confirm the wrong output.\n\nThe fix, in order\n\n1. List every reactive value the effect reads ([id]), and let the react-hooks/exhaustive-deps rule check it (a rule from eslint-plugin-react-hooks; ESLint is the tool that reports likely code mistakes while you type or in CI). React's page says a linter configured for React \"will verify that every reactive value is correctly specified as a dependency.\"\n2. Return a cleanup that ignores the previous request's result and aborts it (case 3).\n3. Reset the visible state when the input changes (the setData('loading') line) so the old result is not shown under the new prop while waiting (case 3a).\n4. Keep dependencies primitive or stable: pass id rather than an object built in render, or build the object inside the effect.\n5. Prefer a data-fetching library or framework loader over hand-written effects for anything beyond a prototype. React says that if you use a framework, its data fetching \"will be a lot more efficient than writing Effects manually.\"\n\nHow to debug it\n\nAdd a console.log('effect', id) as the first line of the effect. No log after the prop change means a dependency problem; a log means the effect ran and the problem is the response.\nOpen the Network panel and pick a slower preset from its throttling dropdown, which delays responses so a race becomes visible. A missing request points to dependencies; two requests with the older one finishing last point to the race.\nIn React DevTools, check that the prop really changed and which component holds the state.\nLog id inside the .then and compare it with the current prop: a mismatch confirms a late response.\nIf you see two requests on mount only in development, that is the StrictMode check, not the bug.\n\nTrade-offs and pitfalls\n\nDo not silence the lint rule to stop a re-fetch loop; fix the unstable value instead. A suppressed warning is how the stale-closure bug in case 5 reaches production.\nAn ignore flag stops the stale write but not the request; AbortController also stops the network work if the API supports it. Use both, or at least the flag.\nStable and identical-looking dependencies are different things: two { id: 5 } objects are not equal under Object.is.\nThis run covers one component per failure mode with hand-controlled responses; it shows the mechanism, not how often each bug appears in production.\n\nRunning the code\n\nnpm i react@18.3.1 react-dom@18.3.1 @testing-library/react@16.0.1 @testing-library/dom@10.4.0 jsdom@25.0.1\nnode --disable-warning=ExperimentalWarning effect-deps.mjs # prints the lines above and \"all assertions passed\"\n\nSave the listing as effect-deps.mjs beside a package.json containing { \"type\": \"module\" }. Tested on node:22 (v22.23)."}}]}

Lyft Full-Stack Developer (Junior Level) Interview Preparation Guide

Full-Stack Developer
Lyft
Junior
6 rounds
Updated 6/16/2026

Lyft's junior full-stack developer interview process typically consists of an initial recruiter screening, followed by technical phone screens covering coding and system fundamentals, and onsite rounds that include coding assessments, system design discussions (at junior level, focused on basic architectural patterns), and behavioral/cultural fit evaluation. The process is designed to evaluate both technical competency across frontend and backend stacks and alignment with Lyft's values around building reliable transportation platforms.

Interview Rounds

1

Recruiter Screening

2

Technical Phone Screen - Frontend Fundamentals

3

Technical Phone Screen - Backend Fundamentals

4

Onsite Interview - Full-Stack Coding Challenge

5

Onsite Interview - System Design (Foundations)

6

Onsite Interview - Behavioral and Culture Fit

Frequently Asked Full-Stack Developer Interview Questions

Backend-for-Frontend and Client-Specific API DesignEasyTechnical
33 practiced

A mobile detail screen always needs an object but only sometimes needs its large related collection, such as comments. How would you design a single endpoint so the client can get the object with or without the collection? Show a sample request and response and explain the trade-offs.

RESTful API DesignEasyTechnical
126 practiced

Explain the architectural difference between a RESTful API and an RPC-style API: how URLs are used, whether the HTTP verb carries meaning, statelessness, resource orientation, and what each style implies for caching and for how tightly the client and server are coupled. Give one situation where you would prefer RPC over REST in a backend service.

Clear Written and Verbal CommunicationEasyTechnical
73 practiced

After a working meeting, write a concise summary (3-6 sentences) that captures the decision made, who owns each follow-up, the deadlines, and any question that is still open.

JavaScript and TypeScript FundamentalsEasyTechnical
50 practiced

Explain lexical scoping and the scope chain. Take a small example with three nested functions and say, for each variable reference, which scope it resolves to and why.

SQL Query FundamentalsEasyTechnical
41 practiced

A user reports that SELECT * FROM orders WHERE customer_id = 12345; returns no rows, but you know customer_id 12345 exists. List at least four distinct reasons this can happen and the SQL checks you would run to diagnose each.

Python ProgrammingMediumTechnical
18 practiced

A report generator currently scans the same task records multiple times to answer different summary questions, and the runtime is starting to hurt as the team grows. How would you reason about the bottleneck, and what would you change in the data flow so repeated lookups get faster without making the codebase hard to maintain?

Cross-Functional CollaborationMediumTechnical
38 practiced

Legal sign-off is going to take three weeks, but the team wants to ship in one. How do you manage that timeline without steamrolling legal's concerns?

DOM Manipulation and Browser APIsEasyTechnical
92 practiced

Describe how to store and retrieve a JavaScript object representing user preferences (e.g., { theme: 'dark', fontSize: 16 }) using localStorage. Include code that handles serialization and a safe read that tolerates missing or corrupted data.

Scalability Patterns and TechniquesEasyTechnical
36 practiced

What metrics are commonly used as autoscaling triggers, both reactive and predictive? Weigh the pros and cons of CPU, memory, request rate, end-to-end latency, and custom application metrics like queue length or pending jobs as autoscaler inputs.

Frontend Component and State ArchitectureMediumTechnical
88 practiced

A useEffect fetches data using a prop but keeps showing results for the previous prop value. Explain how the dependency list works, what bugs missing or extra dependencies cause, and how you would debug it.

Want to create your own tailored preparation guide using our deep research?

Get Started for Free

Interview-Ready Courses

Visual-first, interactive, structured learning paths

Browse Full-Stack Developer jobs

AI-enriched listings across hundreds of company career pages

Explore Jobs