默认只显示问题 —— 先自己在心里答一遍,再展开对答案。答不上来就标「不会」,下次抽认卡会先抽它。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.
In one line: pull “logic that carries state” out so it can be reused. The name must start with use.
Why the use prefix is mandatory — this is the point being tested, and it is not about style:
ESLint uses the prefix to recognise it as a hook so it can enforce the rules of hooks (react-hooks/rules-of-hooks). Without the prefix, you can break those rules inside it and nobody warns you.
It is also a signal to whoever reads the code: this function may hold state, so there are limits on where you may call it.
The key idea: you reuse the logic, not the state. Two components that each call useCounter() get two completely independent pieces of state. To share state you need Context or a state library. This is a frequent follow-up and a lot of people get it wrong.
When to extract one: the same combination of useState and useEffect shows up in two or more places; or the effect logic in one component has grown long enough that you can no longer read the component itself.
Follow-up: “What can a custom hook return?” — anything. The convention is “return an array like useState does” (so the caller can rename freely) and “return an object once there are three or more values” (so nobody has to remember the order). “Can it call other hooks?” — yes, that is the whole point; the same two rules still apply.
Open with this sentence, then list the techniques:“First I use the React DevTools Profiler to find which component is slow and how many times it renders, then I decide what to touch.” Reciting useMemo straight away sounds like you memorised an answer sheet.
Three families of technique:
① Render less
React.memo + useMemo + useCallbackused as a set (#346)
Push state down — put frequently changing state in the small component that actually needs it instead of lifting it to the top and re-rendering the whole tree. This often helps far more than adding memo.
Compose with children — when the parent re-renders, children passed in as a prop are not rebuilt.
Split your contexts — put too much in one context and changing any field re-renders every consumer.
② Download less
Code splitting — React.lazy + Suspense, split per route (#347)
Import third-party libraries piecemeal, not import _ from "lodash"
Run a bundle analyzer to see who is taking up the space
③ Compute less, paint less
Virtualise long lists — render only the few dozen rows in the viewport. On a ten-thousand-row list this beats every other optimisation put together.
Use stable ids as list keys (#330)
Debounce input, throttle search
React 18’s useTransition / useDeferredValue — keep the heavy work from blocking typing
Follow-up: “How do you know there are wasted renders?” — the Profiler’s “Highlight updates”, or <Profiler onRender>; and remember StrictMode renders twice in development, so do not mistake that for a bug.
In one line: five ways, each with a clear trade-off.
Approach
Upside
Cost
Plain CSS / SCSS files
Free, and every CSS feature is available
Class names are global, so they collide
CSS Modules
Hashed class names, isolated by default
Dynamic styles need CSS variables
Inline style
The most direct way to use a dynamic value
No pseudo-classes, media queries or animations; a new object every render
CSS-in-JS (styled-components)
props can drive the styles, and scoping needs no extra work
Runtime cost, and SSR needs extra setup
Atomic (Tailwind)
No naming, and the output size stays under control
Very long class strings in JSX, and the team needs conventions
The recommended way to do dynamic styles — this is the bonus point: use a CSS variable, not an inline style. Put only the variable inline and leave the actual rule in the CSS file — you get the dynamic value and keep pseudo-classes and media queries. That is how this site’s dark mode works (flip the data-theme attribute and the whole set of CSS variables swaps).
Follow-up: “Why do inline styles hurt performance?” — every render creates a new object, which breaks memo on the child, and the browser cannot cache it as a rule. If you must use one, stabilise it with useMemo.
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.
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.