默认只显示问题 —— 先自己在心里答一遍,再展开对答案。答不上来就标「不会」,下次抽认卡会先抽它。Only the question shows by default. Answer it in your head first, then open the answer. Mark the ones you miss; the flashcard round puts those first.
0 / 105道自评过self-assessed
0会Got it0模糊Shaky0不会No idea105还没做Not seen
正在读这台浏览器里的标记…Reading your marks from this browser…
标记不是打分,是给下一轮排队Marks are a queue, not a score
标「不会」的题会排到抽认卡最前面,标「会」的进低频池排最后。所以别客气 —— 觉得答得磕磕巴巴就标「模糊」。准备好了就去抽认卡。“No idea” jumps to the front of the next round; “got it” drops to the back. So be honest — if it came out shaky, mark it shaky. Then go run a flashcard round.
找一道题Find one按方向、掌握状态筛Filter by topic and markReact 与生态React & ecosystem
In one line: the core is concurrent rendering — React can interrupt, pause and resume a render so a higher-priority update goes first.
Five concrete changes — covering three or four properly is enough:
Automatic batching — React 17 only batched inside React event handlers, so several setState calls in a setTimeout, a Promise or a native event each triggered their own render. 18 batches all of them. This is the change you notice most easily.
useTransition — mark an update as “not urgent”. The classic case is an input next to a heavy list of search results: typing stays smooth and the list catches up.
useDeferredValue — same goal, from the value side: give me a version of this value that lags behind.
The new root API — createRoot replaces ReactDOM.render, and until you switch you get none of the concurrent features.
Suspense works on the server — streaming and selective hydration. Plus useId (ids that match between server and client) and useSyncExternalStore (for state libraries).
Follow-up: “What changed for StrictMode in 18?” — it now does an extra mount, unmount, mount, so effect cleanup functions actually run. The point is to expose components that forgot to write cleanup (see #332) and to prepare for the future Offscreen feature. “What do you watch out for when upgrading?” — switch to createRoot; automatic batching can change the behaviour of old code that assumed “every setState renders right away” (use flushSync to escape a batch when you truly need to).
In one line: it makes a component load on demand — the bundler cuts it into its own chunk, and the chunk is fetched only when you actually render it. It needs a Suspense around it to supply a placeholder.
Why you need it: by default an SPA is one bundle, so the first screen downloads the code for every page (see #321). Splitting per route is the highest-value cut — opening the home page should not download the settings page.
Four things to watch:
What you pass to lazy must return a Promise with a default export — so it pairs with default export; a named export needs a small wrapper.
The path in a dynamic import() cannot be a fully dynamic variable — the bundler has to be able to analyse it at build time.
Handle the failure case — if the network drops the chunk never arrives, so wrap it in an error boundary (see #333). Most people leave this out, so it is a bonus point.
Do not split too finely — every chunk is another request.
Follow-up: “How do you avoid a loading flash when switching pages?” — preload: call that import() once when the mouse hovers the link (the module gets cached). Or use React 18’s useTransition to keep the old page on screen until the new one is ready.
In one line: a checking component that only does anything in development, and it exposes unsafe code by deliberately doing things twice. In a production build it does nothing.
It does three things:
Calls your render function twice — which surfaces side effects during render (mutating an outer variable, mutating props directly). Because the render phase can be interrupted and re-run (see #353), it has to be pure.
Since React 18: effects run mount, unmount, mount — specifically to expose effects with no cleanup function. If mounting twice breaks your component (a duplicated subscription, a doubled timer), that is a real bug.
Warns about deprecated APIs and legacy ref patterns.
The most important sentence: a problem caused by double rendering is not StrictMode’s problem, it is your code’s. Most people’s first instinct is to turn it off — the right instinct is to fix the side effect.
Follow-up: “Is it normal for my log to print twice?” — yes in development, because the render function ran twice; it will not in production. “What about my request firing twice?” — take that one seriously: it means your effect is not cleaning up properly. Harmless for an idempotent GET, but it is also telling you to ask whether you guarded against races. The ignore flag plus abort pattern from our fetch variant is exactly what makes it behave correctly under StrictMode too.
会追问:「怎么让用户能恢复?」—— 兜底 UI 里给一个「重试」按钮, 把边界的 state 重置 (或者换一个 key强制重建子树)。react-error-boundary这个库提供了 resetErrorBoundary。
In one line: a class component that catches render errors in its subtree and renders a fallback UI instead of letting the whole app go blank.
How you write one: implement getDerivedStateFromError (to switch to the fallback) and componentDidCatch (to report the error). It still has to be a class component — there is no hook equivalent.
What it does not catch — this is the real question:
Errors inside event handlers — those do not happen during render. Use your own try/catch.
Errors in async code — setTimeout, Promise callbacks (the same reason as #310).
Server-side rendering.
Errors thrown by the boundary itself.
Where to put them:get the granularity right. One at the root only means a single broken widget still wipes out the page; put one around each independent block — each route, and each widget that can fail on its own (sidebar, chart, comments). Then one broken block leaves the rest usable.
Follow-up: “How does the user recover?” — give the fallback a “Retry” button that resets the boundary’s state (or changes a key to force the subtree to rebuild). The react-error-boundary library gives you resetErrorBoundary for this.
In one line: it gives an SPA the concept of a URL — it maps the address bar to whichever component is rendered, and switches between them without a page reload.
It solves four things:
Mapping URLs to components
Back and forward work — it takes over the history API
Deep links are shareable — send one to someone and it opens that page directly
Nested routes — the layout layer and the content layer stay separate (Outlet)
The core API:BrowserRouter, Routes / Route, Link / NavLink, useNavigate, useParams, useSearchParams, Outlet.
Three practical points:
Use Link, not <a> — an anchor really does reload the whole page, which throws away the point of an SPA.
BrowserRouter needs the server to fall back to index.html for every path, otherwise refreshing a nested route 404s. If you cannot configure the server, use HashRouter. This one comes up a lot.
Pair it with React.lazy to split code per route (#347).
Follow-up: “How do you do route guards?” — React Router has none built in; you write a wrapper component yourself: if the user is not signed in, render <Navigate to="/login" />.
This is an open question, and a list with trade-offs beats a pile of rules. Six, most important first:
Update immutably. Always build a new object or array, never edit in place — otherwise React compares references, sees nothing changed, and the UI does not update. This is the single most common source of bugs in React.
If you can compute it, do not store it in state. Derive it on the spot; do not use useEffect to keep two copies in sync — one fact stored twice will drift.
Keep state at the lowest level that works. Too high and you get prop drilling and wasted renders; too low and siblings cannot reach it.
Anything an effect sets up has to be torn down. Timers, listeners, subscriptions, in-flight requests. The test: if the effect contains setInterval, addEventListener, subscribe or fetch, it must return something.
Use stable business ids as list keys, not the index.
Measure before optimising. Do not wrap everything in useMemo by default.
Two more on the engineering side: keep components small and single-purpose (one component, one job); and use TypeScript — the shape of the props is the component’s contract, and writing it down beats remembering it.
If the interviewer wants something more specific, you can say: “I keep eslint-plugin-react-hooks on and I do not silence the exhaustive-deps warning with a comment — it is right almost every time, and wanting to get around it usually means the code needs restructuring.” That one really shows hands-on experience.
In one line — and this is the key sentence:they do not solve the same problem. Context is a delivery mechanism (how a value reaches something deep in the tree); Redux is a state management solution (how state is organised, changed and debugged).
“Can Context replace Redux?” — strictly speaking, Context plus useReducer covers Redux’s basic features, but three things are missing:
No fine-grained subscription. When the context changes, every consumer re-renders, even one that only reads a single field. Redux’s useSelector re-renders only when the slice you selected changes. That is the biggest practical difference.
No middleware. Handling async, logging and persistence in one place is all yours to build.
No DevTools. No time travel, no action log, no state diff.
Context
Redux
Good for
Theme, current user, locale — rarely changes
Changes often, read and written in many places, needs a debuggable trail
Subscription granularity
The whole value
Per selector
Extra dependency
None
Yes
Boilerplate
Little
Some (far less since Redux Toolkit)
Follow-up: “Do people still use Redux?” — answer honestly: plenty of projects moved pure client state to Zustand or Jotai (lighter); server data goes to TanStack Query or SWR (caching, deduping and retries are their day job, and Redux doing it is a stretch). Redux’s remaining strength is complex client state with a lot of interaction logic that you need to be able to trace, and you should always use Redux Toolkit rather than write it by hand.
In one line:a one-way loop — the view dispatches an action, middleware handles it, a reducer computes the new state, the store updates, and the subscribed components re-render.
Five roles:
store — the single container for state, exposing getState, dispatch and subscribe.
action — a plain object that describes what happened and must have a type. It only describes; it does nothing.
reducer — (state, action) => newState, and it has to be pure.
middleware — intercepts the action on its way to the reducer (#354).
selector — picks the part of the store a component needs.
What Redux Toolkit (RTK) changed — you have to cover this, because nobody hand-writes Redux any more:
createSlicegenerates the reducer, the action creators and the action types at once, cutting more than half the boilerplate.
Immer is built in, so you can write what looks like a mutation — state.list.push(x) — and still get a new object out. Just remember this only holds inside createSlice.
thunk and DevTools are wired up by default.
createAsyncThunk handles the three async states (pending / fulfilled / rejected).
Follow-up: “Why does it have to be one-way?” — because there is exactly one path a change can take, so when something breaks you can walk back through the action log step by step. In a two-way binding framework, “who actually changed this value” is often unanswerable.
JavaScript现在真正会写的 Redux示意Illustrative
1// RTK 的 slice:reducer + actions 一次生成
2consttodos=createSlice({
3name:"todos",
4initialState:[],
5reducers:{
6add(state,action){
7state.push(action.payload);// 看着是 mutate,Immer 会产出新 state
8},
9toggle(state,action){
10constt=state.find((x)=>x.id===action.payload);
11if(t)t.done=!t.done;
12},
13},
14});
15
16exportconst{add,toggle}=todos.actions;
17
18// 组件里
19constlist=useSelector((s)=>s.todos);// 只订阅这一部分
20constdispatch=useDispatch();
21dispatch(add({id:Date.now(),text,done:false}));
1// An RTK slice generates the reducer and the actions together
2consttodos=createSlice({
3name:"todos",
4initialState:[],
5reducers:{
6add(state,action){
7state.push(action.payload);// it looks like a mutation; Immer produces a new state
8},
9toggle(state,action){
10constt=state.find((x)=>x.id===action.payload);
11if(t)t.done=!t.done;
12},
13},
14});
15
16exportconst{add,toggle}=todos.actions;
17
18// In the component
19constlist=useSelector((s)=>s.todos);// subscribes to this part only
Single source of truth — one store for the whole app. Why: one copy of the state cannot disagree with itself, and the entire app state can be serialised — which is what makes “save and restore the session” and SSR dehydration possible.
State is read-only — you change it only by dispatching an action. Why:it narrows “who can change state” down to one entrance, so every change can be logged, intercepted and replayed.
Changes are made by pure functions (reducers) — (state, action) => newState. Why: a pure function always gives the same result for the same state and action, so it can be replayed and tested, and that is what makes time-travel debugging work.
The three are one package: a single source of truth makes state serialisable, read-only makes changes loggable, and pure reducers make changes replayable — together they add up to time travel in DevTools. Tying them together like this is much stronger than reciting them one by one.
Follow-up: “What can and cannot a reducer do?” — it cannot mutate the state it was given, make requests, read Date.now() or Math.random(), or dispatch other actions. All of that belongs in middleware or an action creator. “Does state.push() in RTK break rule 2?” — no. Immer hands you a draft proxy, records your edits, and produces a new object; the original state is untouched.
In one line: middleware is a layer between “an action is dispatched” and “the reducer receives it”. It can intercept, rewrite, delay or even swallow an action.
The signature is curried three levels deep — the shape itself gets asked about: store => next => action => {}. Several middlewares chain together through next, and it is the same pattern as Express middleware.
Why you need it: because the reducer has to be pure, async work and side effects have nowhere to live. Middleware is the place reserved for side effects.
The common ones:
redux-thunk — lets you dispatch a function instead of only an object, and do your async work inside it. Simplest option, and RTK installs it by default.
redux-saga — describes complex async flows with generators (cancellable, retryable, able to orchestrate several requests). Powerful, but a steep learning curve.
redux-logger — prints the state before and after each action.
Follow-up: “thunk or saga?” — thunk is enough for most projects; saga only earns its complexity when you need cancellation, debouncing or real flow orchestration. “Could you write one?” — yes, and interviewers often ask you to write a logger on the spot.
In one line: TS is a superset of JS — it adds static types, and after compilation it is ordinary JS with nothing of TS left at runtime.
JavaScript
TypeScript
Type checking
Fails at runtime
Reported at compile time
Build step
Not needed
Needed (tsc / esbuild / SWC)
IDE support
Guesswork
Precise completion, go-to-definition, rename
Refactoring
Search for strings
Change one place and every incompatible use lights up
Stress the “nothing of TS at runtime” line, because two important conclusions follow from it:
Types cannot validate anything at runtime. Whether the data an API returns really matches the interface you wrote is beyond TS — for that you need something like zod. This is the biggest beginner misconception.
An as assertion is just “trust me” and checks nothing. Overusing as and any is the same as turning TS off.
The costs — bring them up yourself: an extra build step, a learning curve (generics, conditional types, unknown vs any), writing your own declarations when a library ships none, and error messages for complex types that are hard to read.
Follow-up: “interface or type?” — interface can be declared again and merged and suits object and class contracts; type can do unions, intersections, mapped and conditional types, so it is more capable. In practice: “interface for object shapes, type for everything else” — but a consistent team choice matters more than which one you pick.
In one line:without running the code, purely by analysing the source, it finds places where the types do not line up. “Static” just means “at compile time, not at run time”.
Four concrete benefits — give examples, do not speak in the abstract:
Errors surface earlier — a typo like user.nmae, a forgotten null case, a missing argument: they go red in the editor instead of arriving as a user report after release.
Types are documentation — a signature says what it wants and what it gives back. And this documentation cannot go stale, because changing the code without changing the types fails the build.
Refactoring with confidence — rename one field and every affected place errors. This is TS’s most underrated value, and on a large codebase it is more useful than bug prevention.
Editor power — accurate completion, jump to definition, safe rename.
The limits — saying them is what shows you get it:it only guarantees the types are right, not that the logic is — fully typed code can still calculate the wrong salary. And it has no reach over external data at runtime (see #355), so type checking is not a substitute for tests.
Follow-up: “Do you turn on strict?” — always on a new project. The most valuable piece is strictNullChecks — it turns an entire class of “forgot the null check” runtime errors into compile errors. A real example to go with it:npm run build on the source project for the React course fails precisely because tsc reports 10 errors (the test file is missing the vitest globals) — which shows type checking is part of the build, not an optional lint.
99 道来自面试题库 #269–#387;TypeScript 深度那 6 道是 DrillLab 自出的(senior 补强,卡片上有标注)。答案都是 DrillLab 写的,所以讲解里的代码块一律标「示意」。每道题都能点回它出处的那一节课。99 questions come from the interview bank (#269–#387); the 6 TypeScript deep-dive ones are DrillLab-made (marked on the card). All answers are written by DrillLab, so every code block here is labelled “demo”. Each card links back to the lesson it came from.