diff --git a/.claude/README.md b/.claude/README.md index 8c6c4517..e3fd80e2 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -12,6 +12,7 @@ CI sharding, MCP extensions). | Chromium piece | Here | Loaded when | |-------------------------|----------------------------------------|--------------------------------------| | `ai_policy.md` | [`ai-policy.md`](./ai-policy.md) | Read it before shipping AI-assisted code | +| API style guide | [`api-design.md`](./api-design.md) | Before adding/changing a public hook signature | | `common.md` workflow | `## AI Agent System` in root `CLAUDE.md` | Always (project memory) | | `knowledge_base.md` | [`knowledge-base.md`](./knowledge-base.md) | Always (it's the router) | | Skills | [`skills/`](./skills) | Auto-activated by relevant requests | diff --git a/.claude/api-design.md b/.claude/api-design.md new file mode 100644 index 00000000..df9f6cca --- /dev/null +++ b/.claude/api-design.md @@ -0,0 +1,119 @@ +# API Design Spec — `@reactuses/core` + +The contract every public hook signature must follow. It adapts *The Little Manual of API +Design* (Blanchette, Trolltech — the Qt design booklet, mirrored at `/papers/api-design` on the +site) to a React-hooks library, and encodes the conventions this repo already follows so new +hooks don't drift. **Read this before adding or changing a public signature.** It is referenced +from `CLAUDE.md` and the `new-hook` skill. + +> The manual's five marks of a good API: **easy to learn**, **leads to readable code**, **hard to +> misuse**, **easy to extend**, **complete**. "Minimal" and "consistent" aren't on that list — +> they serve the five. Consistency ≈ *conceptual integrity*: one coherent design, not many good +> but unaligned ideas. + +--- + +## 1. Signature shape (manual §4.11 — property-based APIs) + +A hook signature is `(subject, [essential args], options?)`: + +```ts +useX(subject, options?) // most hooks +useX(subject, essential, options?) // when one more arg is genuinely required +``` + +- **Lead with the subject** — the thing the hook operates on (a `target`, a `key`, a `value`, + a `callback`). Not configuration. +- **All configuration goes in a single trailing `options` object**, named `options`, always + last, always optional with sensible defaults. Users set only what they want to change, in any + order, and the call site stays readable without comments. +- **Do not add a 3rd/4th bare positional value.** `useFavicon(href, baseUrl, rel)` is the + anti-pattern (manual's Qt-3 `QSlider(8,128,1,6,…)`): unreadable at the call site. Fold extras + into `options`. +- **`options` position is fixed.** It is always the *last* parameter. A required-config object + in the middle (as `useCookie` once had `options` before `defaultValue`) silently mis-types + arguments and breaks consistency with sibling hooks. + +Mirror a native API's positional shape **only** when users already know it +(`useEventListener(event, handler, target, options)` ≈ `addEventListener`) — manual §3.3 +(reuse familiar APIs) balanced against §4.6 (don't be a slave to the underlying API). + +## 2. Naming (manual §4.1–4.6) + +| Rule | Do | Don't | +|---|---|---| +| Self-explanatory, reads like English (§4.1) | `useScroll(target, options)` | one-letter / cryptic params | +| No abbreviations (§4.4), except the common set `min max dir rect prev ref` | `defaultValue` | `defVal`, `def` | +| Specific over general (§4.5) | `UseScrollOptions` | `Options` | +| `set`/`on` prefix means *only* setter/handler (§4.3) | `setValue`, `onScroll` | `set`-prefixing a non-setter | +| No typos — a published name is **locked forever** (§3.8, the `SHStripMneumonic` story) | review names before export | `useWindowsFocus`, `defauleValue` | + +**The `default*` vs `initial*` distinction is intentional — keep it** (flattening it would be +the *false consistency* §4.3 warns against): + +- **`default*`** — a fallback for state read from the environment, used when the real value is + unavailable (SSR / hydration). Hooks reading external state: `useCookie`, `useLocalStorage`, + `useMediaQuery`, `usePreferredDark`, `useDocumentVisibility`, `useWindowFocus`. +- **`initial*`** — the seed of internal mutable state the hook then owns. Stateful hooks: + `useBoolean`, `useCounter`, `useToggle`, `useMap`. + +Suffix: use **`Value`** for a scalar/boolean, **`State`** only when it is a `useState`-style +container (object or generic `S`, as in `useRafState`). Prefer `Value` when unsure. + +**DOM target parameter** is always named **`target`** (type `BasicTarget<…>`), never +`targetElement` / `el` / `element`. + +## 3. Return shape (manual §2.2 — readable code) + +- **Tuple** `[value, setter]` or `[a, b]` — for ≤2 values, or the `useState`-like + `[state, set]` idiom. Idiomatic React; lets callers name positions. +- **Object** `{ named, fields }` — for ≥3 fields, or heterogeneous fields where position would + be ambiguous (manual §4.1 warns positional `[number, number]` hides which is which). +- **Pick one shape per concept and hold it.** Two hooks reporting the same kind of thing must + return the same shape (see the `useElementSize` / `useWindowSize` deviation below). + +## 4. Semantics (manual §4.7–4.9) + +- **SSR-safe by default (§4.7, §4.12 "the best API is no API").** Never touch `window`/`document` + at module top level. Guard with `isBrowser` / `defaultWindow`; the hook should return a sane + server value with zero extra user code. See `knowledge-base.md`. +- **Boolean options default to `false`** and read affirmatively (`exact`, `preventDefault`), + user opts in. +- **No clever side effects (§4.8).** No hidden format auto-detection, no one setter quietly + flipping unrelated state. Least surprise. +- **Edge cases are covered by tests (§4.9)** — empty/undefined target, unmount, repeated calls. +- **Reuse the underlying API's option type when wrapping it** — `ResizeObserverOptions`, + `IntersectionObserverInit`, `AddEventListenerOptions`, `Cookies.CookieAttributes`. This is + deliberate (§3.3), not an inconsistency. + +## 5. Process (manual §3.x) + +- Write the call-site example first; let it shape the signature (§3.2). +- Find a sibling hook and match it (§3.3) — this file + `knowledge-base.md` are the router. +- **When in doubt, leave it out (§3.9).** A param renamed later is cheap; a wrong one shipped + is locked. Don't add an option until it's actually needed. + +--- + +## Known deviations (grandfathered) + +Real inconsistencies that predate this spec. They are **not** runtime-fixed yet because the fix +is either purely cosmetic (param-name only — non-breaking, converge opportunistically) or a +return-shape change that needs a deliberate major-version decision. Do **not** copy these; do +**not** mass-rewrite them in unrelated PRs. + +| Deviation | Hooks | Spec rule | Fix path | +|---|---|---|---| +| `Value`/`State` suffix drift | `defaultState` (×5: `useMediaQuery`, `usePreferred*`, `useReducedMotion`), `initialState` (booleans in `useIdle`, `useScrollLock`) | §2 suffix rule | param-rename, non-breaking — converge incrementally | +| Size returns disagree | `useElementSize` → `[w, h]` tuple vs `useWindowSize` → `{ width, height }` object | §3 one-shape-per-concept | breaking — decide at a major bump | +| Trailing bare positionals | `useFavicon(href, baseUrl, rel)` | §1 no 3rd bare value | fold into `options` at a major bump | +| Required mid-positional config | `useSticky(target, params, scrollElement?)` — `params` required, not named `options`, not last | §1 options-last | reshape at a major bump | + +## Checklist for a new/changed public signature + +1. Subject first; all config in a trailing optional `options` object. +2. No new bare positional beyond the 2nd argument. +3. Names: no abbreviations, no typos, `default*` vs `initial*` used correctly, DOM target = `target`. +4. Return shape matches the rule **and** any sibling hook of the same concept. +5. SSR-safe with no extra user code; boolean options default `false`. +6. A readable call-site example exists and reads without comments. diff --git a/.claude/knowledge-base.md b/.claude/knowledge-base.md index 39199a61..55124a72 100644 --- a/.claude/knowledge-base.md +++ b/.claude/knowledge-base.md @@ -16,6 +16,7 @@ referenced from `CLAUDE.md`, so treat it as always-in-context. | When the task involves…​ | Read / do this | |---|---| | **Adding or editing a hook** | Conventions live in `packages/core/src/utils/` + existing hooks. Use the **[new-hook](./skills/new-hook/SKILL.md)** skill. Every hook is a folder `packages/core/src/useX/` with `index.ts` + `interface.ts` (+ `index.spec.ts`). | +| **Designing or changing a public signature** | Read **[api-design.md](./api-design.md)** — the contract for parameter order (`(subject, …, options?)`), naming (`default*` vs `initial*`, DOM target = `target`, no abbreviations/typos), return shape (tuple vs object), and SSR defaults. A shipped name is locked forever. | | **Touching `window` / `document` (SSR safety)** | This is the #1 source of bugs. Use `defaultWindow` / `defaultDocument` from `packages/core/src/utils/browser.ts`, or `isBrowser` / `isNavigator` from `packages/core/src/utils/is.ts`. For subscribed external state, prefer the `use-sync-external-store/shim` pattern with a server fallback (see `useLocationSelector`, `useColorMode`). Never read `window.x` at module top level without a guard. | | **Attaching event listeners** | Reuse the `useEventListener` hook — do **not** hand-roll `addEventListener`/`removeEventListener`. For non-hook contexts, `on()` / `off()` in `utils/browser.ts` already null-check the target. | | **Keeping a callback fresh without re-subscribing** | Use `useLatest` (store the latest fn in a ref) to avoid stale closures. | diff --git a/.claude/skills/new-hook/SKILL.md b/.claude/skills/new-hook/SKILL.md index dcf281c0..96fefa8c 100644 --- a/.claude/skills/new-hook/SKILL.md +++ b/.claude/skills/new-hook/SKILL.md @@ -22,6 +22,10 @@ built in this repo today. Pair it with **hook-test** and **hook-docs** afterward 3. Check the [knowledge base](../../knowledge-base.md) reuse cheat-sheet — much of what you need (`useLatest`, `useUnmount`, `useEventListener`, `defaultWindow`, `isBrowser`) is already written. Reuse before writing. +4. Read **[api-design.md](../../api-design.md)** and apply it to the signature: subject first, + all config in a trailing optional `options` object, DOM target named `target`, `default*` + (SSR fallback) vs `initial*` (mutable seed) used correctly, the right return shape (tuple + vs object), and **no typos — a shipped name is locked forever**. ## Step 1 — Create the hook folder diff --git a/CLAUDE.md b/CLAUDE.md index bddac362..2017354f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,6 +29,7 @@ hooks library, not a 35M-line codebase. Map: [`.claude/README.md`](.claude/READM **Where to look:** - [`.claude/knowledge-base.md`](.claude/knowledge-base.md) — task → file/utility router. **Start here**; it tells you which existing code already solves your task. +- [`.claude/api-design.md`](.claude/api-design.md) — the public-signature contract (naming, options object, return shape, SSR defaults). **Read before adding or changing any hook signature.** - [`.claude/ai-policy.md`](.claude/ai-policy.md) — you own every line you ship; understand it before review. - Skills (auto-activate): `new-hook`, `hook-test`, `hook-docs`, `pr-description`. - Commands: `/new-hook`, `/pre-pr`, `/pr-desc`. diff --git a/packages/core/changelog.md b/packages/core/changelog.md index c890e37e..960ce3ba 100644 --- a/packages/core/changelog.md +++ b/packages/core/changelog.md @@ -1,5 +1,29 @@ # ChangeLog +## 7.0.0 (June 30, 2026) + +API-design consistency pass (see `.claude/api-design.md`). **Breaking changes** — all are +renames/reorders with no behavior change; migration is mechanical. + +- **`useWindowsFocus` → `useWindowFocus`** — the published export had a stray `s` + (the concept is "window focus", not "windows focus"). Its `defauleValue` parameter is + also corrected to `defaultValue`. + ```diff + - import { useWindowsFocus } from '@reactuses/core' + + import { useWindowFocus } from '@reactuses/core' + ``` +- **`useCookie` argument order** — now `useCookie(key, defaultValue?, options?)`, aligning + with `useLocalStorage` / `useSessionStorage`. Previously `options` came before + `defaultValue`, which silently mis-typed a string passed in the `useLocalStorage` shape. + ```diff + - useCookie('token', { path: '/' }, 'guest') + + useCookie('token', 'guest', { path: '/' }) + ``` +- **`useScrollIntoView` / `useSticky`** — first parameter renamed `targetElement` → `target`, + matching the 20 other DOM-target hooks. Positional callers are unaffected. +- Fixed JSDoc typos surfaced in editor autocomplete (`usePreferredDark`, + `usePreferredLanguages`, `useRafFn`, `useCookie`). + ## 1.0.0 (September 18 ,2022) - Initial public release diff --git a/packages/core/package.json b/packages/core/package.json index 3e02d2b5..8c95d969 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@reactuses/core", - "version": "6.4.0", + "version": "7.0.0", "description": "Collection of 100+ essential React Hooks with TypeScript support, tree-shaking, and SSR compatibility. Sensors, browser APIs, state management, animations, and more.", "author": { "name": "lianwenwu", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f4076a8b..dcf3cb7f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -88,7 +88,7 @@ import { useUpdate } from './useUpdate' import { useUpdateEffect } from './useUpdateEffect' import { useUpdateLayoutEffect } from './useUpdateLayoutEffect' import { useWebNotification } from './useWebNotification' -import { useWindowsFocus } from './useWindowFocus' +import { useWindowFocus } from './useWindowFocus' import { useWindowScroll } from './useWindowScroll' import { useWindowSize } from './useWindowSize' import { useClipboard } from './useClipboard' @@ -178,7 +178,7 @@ export { useDraggable, useElementBounding, useElementVisibility, - useWindowsFocus, + useWindowFocus, useWindowSize, useWindowScroll, useClipboard, diff --git a/packages/core/src/useCookie/index.spec.ts b/packages/core/src/useCookie/index.spec.ts index b3d6c57b..6be55bb3 100644 --- a/packages/core/src/useCookie/index.spec.ts +++ b/packages/core/src/useCookie/index.spec.ts @@ -9,7 +9,7 @@ describe('useCookie', () => { options?: Cookies.CookieAttributes, ) => renderHook(() => { - const [state, setState, refresh] = useCookie(key, options, defaultValue) + const [state, setState, refresh] = useCookie(key, defaultValue, options) return { state, setState, diff --git a/packages/core/src/useCookie/index.ts b/packages/core/src/useCookie/index.ts index 0815f3cf..b3031f77 100644 --- a/packages/core/src/useCookie/index.ts +++ b/packages/core/src/useCookie/index.ts @@ -38,8 +38,8 @@ function getInitialState(key: string, defaultValue?: string) { export const useCookie: UseCookie = ( key: string, - options: Cookies.CookieAttributes = defaultOptions, defaultValue?: string, + options: Cookies.CookieAttributes = defaultOptions, ) => { const [cookieValue, setCookieValue] = useState( getInitialState(key, defaultValue), diff --git a/packages/core/src/useCookie/interface.ts b/packages/core/src/useCookie/interface.ts index 44cb17af..281b6e98 100644 --- a/packages/core/src/useCookie/interface.ts +++ b/packages/core/src/useCookie/interface.ts @@ -9,7 +9,7 @@ import type Cookies from 'js-cookie' * @returns_en A tuple with the following elements: * - The current value of the cookie. * - A function to update the value of the cookie. - * - A function to refresh the value of the cookie, incase other events change it. + * - A function to refresh the value of the cookie, in case other events change it. * @returns_zh-Hant 包含以下元素的元組: * - cookie 的當前值。 * - 更新 cookie 值的函數。 @@ -22,18 +22,18 @@ export type UseCookie = ( * @en key */ key: string, - /** - * @zh 透传给 `js-cookie` 的参数 - * @zh-Hant 透傳給 `js-cookie` 的參數 - * @en option pass to `js-cookie` - */ - options?: Cookies.CookieAttributes, /** * @zh 默认值,ssr必须传递 * @zh-Hant 預設值,ssr必須傳遞 * @en defaultValue, must be required in ssr */ - defaultValue?: string + defaultValue?: string, + /** + * @zh 透传给 `js-cookie` 的参数 + * @zh-Hant 透傳給 `js-cookie` 的參數 + * @en option pass to `js-cookie` + */ + options?: Cookies.CookieAttributes ) => readonly [ UseCookieState, ( diff --git a/packages/core/src/usePreferredDark/interface.ts b/packages/core/src/usePreferredDark/interface.ts index 726f8558..557e59fc 100644 --- a/packages/core/src/usePreferredDark/interface.ts +++ b/packages/core/src/usePreferredDark/interface.ts @@ -8,7 +8,7 @@ export type UsePreferredDark = ( /** * @zh 默认值 * @zh-Hant 預設值 - * @en defaule value + * @en default value */ defaultState?: boolean ) => boolean diff --git a/packages/core/src/usePreferredLanguages/interface.ts b/packages/core/src/usePreferredLanguages/interface.ts index 661342bc..f1e4d360 100644 --- a/packages/core/src/usePreferredLanguages/interface.ts +++ b/packages/core/src/usePreferredLanguages/interface.ts @@ -8,6 +8,6 @@ export type UsePreferredLanguages = ( /** * @zh 默认值 * @zh-Hant 預設值 - * @en defaule value + * @en default value */ defaultLanguages?: string[] ) => string[] diff --git a/packages/core/src/useRafFn/interface.ts b/packages/core/src/useRafFn/interface.ts index 571f7e8b..2148aa6f 100644 --- a/packages/core/src/useRafFn/interface.ts +++ b/packages/core/src/useRafFn/interface.ts @@ -22,7 +22,7 @@ export type UseRafFn = ( callback: FrameRequestCallback, /** * @zh 立即执行 - * @en immediatly start + * @en immediately start */ initiallyActive?: boolean ) => readonly [() => void, () => void, () => boolean] diff --git a/packages/core/src/useScrollIntoView/index.ts b/packages/core/src/useScrollIntoView/index.ts index 229a568f..822fba81 100644 --- a/packages/core/src/useScrollIntoView/index.ts +++ b/packages/core/src/useScrollIntoView/index.ts @@ -16,7 +16,7 @@ import type { UseScrollIntoView, UseScrollIntoViewAnimation, UseScrollIntoViewPa const listenerOptions = { passive: true } export const useScrollIntoView: UseScrollIntoView = ( - targetElement: BasicTarget, + target: BasicTarget, { duration = 1250, axis = 'y', @@ -40,7 +40,7 @@ export const useScrollIntoView: UseScrollIntoView = ( } } - const element = getTargetElement(targetElement) + const element = getTargetElement(target) const scrollIntoView = ({ alignment = 'start', diff --git a/packages/core/src/useScrollIntoView/interface.ts b/packages/core/src/useScrollIntoView/interface.ts index 6340853b..e3e3dba0 100644 --- a/packages/core/src/useScrollIntoView/interface.ts +++ b/packages/core/src/useScrollIntoView/interface.ts @@ -18,7 +18,7 @@ export type UseScrollIntoView = ( * @zh-Hant dom對象 * @en dom element */ - targetElement: BasicTarget, + target: BasicTarget, /** * @zh 可选参数 * @en optional params diff --git a/packages/core/src/useSticky/index.ts b/packages/core/src/useSticky/index.ts index e2122d73..7bb70980 100644 --- a/packages/core/src/useSticky/index.ts +++ b/packages/core/src/useSticky/index.ts @@ -8,9 +8,9 @@ import { useStableTarget } from '../utils/useStableTarget' import { useLatest } from '../useLatest' import type { UseStickyParams } from './interface' -export function useSticky(targetElement: BasicTarget, { axis = 'y', nav = 0 }: UseStickyParams, scrollElement?: BasicTarget): [boolean, React.Dispatch>] { +export function useSticky(target: BasicTarget, { axis = 'y', nav = 0 }: UseStickyParams, scrollElement?: BasicTarget): [boolean, React.Dispatch>] { const [isSticky, setSticky] = useState(false) - const { key: targetKey, ref: targetRef } = useStableTarget(targetElement) + const { key: targetKey, ref: targetRef } = useStableTarget(target) const { key: scrollKey, ref: scrollRef } = useStableTarget(scrollElement) const axisRef = useLatest(axis) const navRef = useLatest(nav) diff --git a/packages/core/src/useSticky/interface.ts b/packages/core/src/useSticky/interface.ts index 9ace577d..c199b592 100644 --- a/packages/core/src/useSticky/interface.ts +++ b/packages/core/src/useSticky/interface.ts @@ -18,7 +18,7 @@ export type UseSticky = ( * @zh-Hant dom元素 * @en dom element */ - targetElement: BasicTarget, + target: BasicTarget, /** * @zh 可选参数 * @en optional params diff --git a/packages/core/src/useWindowFocus/index.ts b/packages/core/src/useWindowFocus/index.ts index f2ad4f81..85b1f6b7 100644 --- a/packages/core/src/useWindowFocus/index.ts +++ b/packages/core/src/useWindowFocus/index.ts @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react' import { useEventListener } from '../useEventListener' -export function useWindowsFocus(defauleValue = false): boolean { - const [focused, setFocused] = useState(defauleValue) +export function useWindowFocus(defaultValue = false): boolean { + const [focused, setFocused] = useState(defaultValue) useEffect(() => { setFocused(window.document.hasFocus()) diff --git a/packages/core/src/useWindowFocus/interface.ts b/packages/core/src/useWindowFocus/interface.ts index 796607de..d473fd95 100644 --- a/packages/core/src/useWindowFocus/interface.ts +++ b/packages/core/src/useWindowFocus/interface.ts @@ -1,14 +1,14 @@ /** - * @title useWindowsFocus + * @title useWindowFocus * @returns 窗口是否聚焦 * @returns_en whether window focus * @returns_zh-Hant 窗口是否聚焦 */ -export type UseWindowsFocus = ( +export type UseWindowFocus = ( /** * @zh 默认值 * @zh-Hant 預設值 - * @en defauleValue + * @en default value */ - defauleValue?: boolean + defaultValue?: boolean ) => boolean diff --git a/packages/website-astro/src/content/blog-zh-hans/2026-05-09-react-browser-tab-ux.md b/packages/website-astro/src/content/blog-zh-hans/2026-05-09-react-browser-tab-ux.md index 6889fac4..c12a0bd0 100644 --- a/packages/website-astro/src/content/blog-zh-hans/2026-05-09-react-browser-tab-ux.md +++ b/packages/website-astro/src/content/blog-zh-hans/2026-05-09-react-browser-tab-ux.md @@ -220,16 +220,16 @@ function ManualFocus() { 跟前面一样的故事——三个事件监听器、一次初始读取、一个 SSR 的坑。 -### ReactUse 写法:useWindowsFocus +### ReactUse 写法:useWindowFocus -[`useWindowFocus`](https://reactuse.com/element/usewindowfocus/)(导出名是 `useWindowsFocus`,遗留命名保留了下来)返回一个布尔值,并在挂载时再同步一次。 +[`useWindowFocus`](https://reactuse.com/element/usewindowfocus/) 返回一个布尔值,并在挂载时再同步一次。 ```tsx import { useEffect } from "react"; -import { useWindowsFocus } from "@reactuses/core"; +import { useWindowFocus } from "@reactuses/core"; function FreshFeed() { - const focused = useWindowsFocus(); + const focused = useWindowFocus(); const [items, setItems] = useState([]); useEffect(() => { @@ -329,7 +329,7 @@ import { useTitle, useFavicon, useDocumentVisibility, - useWindowsFocus, + useWindowFocus, usePageLeave, useWebNotification, } from "@reactuses/core"; @@ -356,7 +356,7 @@ export function AttentionAwareChat() { }, [visibility, fetchFeed]); // 4: 聚焦时全量刷新 - const focused = useWindowsFocus(); + const focused = useWindowFocus(); useEffect(() => { if (focused) fetchFeed(); }, [focused, fetchFeed]); diff --git a/packages/website-astro/src/content/blog-zh-hant/2026-05-09-react-browser-tab-ux.md b/packages/website-astro/src/content/blog-zh-hant/2026-05-09-react-browser-tab-ux.md index e9b8d133..76d91604 100644 --- a/packages/website-astro/src/content/blog-zh-hant/2026-05-09-react-browser-tab-ux.md +++ b/packages/website-astro/src/content/blog-zh-hant/2026-05-09-react-browser-tab-ux.md @@ -220,16 +220,16 @@ function ManualFocus() { 跟前面一樣的故事——三個事件監聽器、一次初始讀取、一個 SSR 的坑。 -### ReactUse 寫法:useWindowsFocus +### ReactUse 寫法:useWindowFocus -[`useWindowFocus`](https://reactuse.com/element/usewindowfocus/)(導出名是 `useWindowsFocus`,遺留命名保留了下來)返回一個布爾值,並在掛載時再同步一次。 +[`useWindowFocus`](https://reactuse.com/element/usewindowfocus/) 返回一個布爾值,並在掛載時再同步一次。 ```tsx import { useEffect } from "react"; -import { useWindowsFocus } from "@reactuses/core"; +import { useWindowFocus } from "@reactuses/core"; function FreshFeed() { - const focused = useWindowsFocus(); + const focused = useWindowFocus(); const [items, setItems] = useState([]); useEffect(() => { @@ -329,7 +329,7 @@ import { useTitle, useFavicon, useDocumentVisibility, - useWindowsFocus, + useWindowFocus, usePageLeave, useWebNotification, } from "@reactuses/core"; @@ -356,7 +356,7 @@ export function AttentionAwareChat() { }, [visibility, fetchFeed]); // 4: 聚焦時全量刷新 - const focused = useWindowsFocus(); + const focused = useWindowFocus(); useEffect(() => { if (focused) fetchFeed(); }, [focused, fetchFeed]); diff --git a/packages/website-astro/src/content/blog/2026-05-09-react-browser-tab-ux.md b/packages/website-astro/src/content/blog/2026-05-09-react-browser-tab-ux.md index d39e51af..b4a08eb6 100644 --- a/packages/website-astro/src/content/blog/2026-05-09-react-browser-tab-ux.md +++ b/packages/website-astro/src/content/blog/2026-05-09-react-browser-tab-ux.md @@ -220,16 +220,16 @@ function ManualFocus() { Same story as before — three event listeners, an initial-state read, an SSR pitfall. -### The ReactUse Way: useWindowsFocus +### The ReactUse Way: useWindowFocus -[`useWindowFocus`](https://reactuse.com/element/usewindowfocus/) (exported as `useWindowsFocus` — the legacy name is preserved) returns a boolean and re-syncs on mount. +[`useWindowFocus`](https://reactuse.com/element/usewindowfocus/) returns a boolean and re-syncs on mount. ```tsx import { useEffect } from "react"; -import { useWindowsFocus } from "@reactuses/core"; +import { useWindowFocus } from "@reactuses/core"; function FreshFeed() { - const focused = useWindowsFocus(); + const focused = useWindowFocus(); const [items, setItems] = useState([]); useEffect(() => { @@ -329,7 +329,7 @@ import { useTitle, useFavicon, useDocumentVisibility, - useWindowsFocus, + useWindowFocus, usePageLeave, useWebNotification, } from "@reactuses/core"; @@ -356,7 +356,7 @@ export function AttentionAwareChat() { }, [visibility, fetchFeed]); // 4: full refresh on focus - const focused = useWindowsFocus(); + const focused = useWindowFocus(); useEffect(() => { if (focused) fetchFeed(); }, [focused, fetchFeed]); diff --git a/packages/website-astro/src/content/docs-zh-hans/element/useWindowFocus.mdx b/packages/website-astro/src/content/docs-zh-hans/element/useWindowFocus.mdx index 0d76c9ba..e51614d4 100644 --- a/packages/website-astro/src/content/docs-zh-hans/element/useWindowFocus.mdx +++ b/packages/website-astro/src/content/docs-zh-hans/element/useWindowFocus.mdx @@ -3,11 +3,11 @@ title: useWindowFocus 用法与示例 sidebar_label: useWindowFocus description: "useWindowFocus 是一个 React hook,可响应式地跟踪浏览器窗口是否获得焦点,通过 window 的 focus 和 blur 事件更新。" --- -# useWindowsFocus +# useWindowFocus 使用 `window.onfocus` and `window.onblur` 事件跟踪页面焦点 -`useWindowsFocus` 通过监听 `window.onfocus` 和 `window.onblur` 事件响应式地跟踪浏览器窗口当前是否获得焦点。它返回一个布尔值,窗口获得焦点时为 `true`,未获得焦点时为 `false`。你可以选择提供初始状态的默认值。 +`useWindowFocus` 通过监听 `window.onfocus` 和 `window.onblur` 事件响应式地跟踪浏览器窗口当前是否获得焦点。它返回一个布尔值,窗口获得焦点时为 `true`,未获得焦点时为 `false`。你可以选择提供初始状态的默认值。 ### 使用场景 @@ -26,7 +26,7 @@ description: "useWindowFocus 是一个 React hook,可响应式地跟踪浏览 ```tsx live function Demo() { - const focus = useWindowsFocus(); + const focus = useWindowFocus(); return (

diff --git a/packages/website-astro/src/content/docs-zh-hans/state/useCookie.mdx b/packages/website-astro/src/content/docs-zh-hans/state/useCookie.mdx index d25d0369..d75a4942 100644 --- a/packages/website-astro/src/content/docs-zh-hans/state/useCookie.mdx +++ b/packages/website-astro/src/content/docs-zh-hans/state/useCookie.mdx @@ -37,8 +37,8 @@ function Demo() { const cookieName = "cookie-key"; const [cookieValue, updateCookie, refreshCookie] = useCookie( cookieName, - defaultOption, "default-value", + defaultOption, ); const updateButtonClick = () => { @@ -81,7 +81,7 @@ function Demo() { ```tsx live noInline function CookiePanel({ label }) { - const [value, updateCookie] = useCookie("shared-demo-cookie", { path: "/" }, "A"); + const [value, updateCookie] = useCookie("shared-demo-cookie", "A", { path: "/" }); return (

diff --git a/packages/website-astro/src/content/docs-zh-hant/state/useCookie.mdx b/packages/website-astro/src/content/docs-zh-hant/state/useCookie.mdx index 64a3faaa..511e69d7 100644 --- a/packages/website-astro/src/content/docs-zh-hant/state/useCookie.mdx +++ b/packages/website-astro/src/content/docs-zh-hant/state/useCookie.mdx @@ -37,8 +37,8 @@ function Demo() { const cookieName = "cookie-key"; const [cookieValue, updateCookie, refreshCookie] = useCookie( cookieName, - defaultOption, "default-value", + defaultOption, ); const updateButtonClick = () => { @@ -81,7 +81,7 @@ function Demo() { ```tsx live noInline function CookiePanel({ label }) { - const [value, updateCookie] = useCookie("shared-demo-cookie", { path: "/" }, "A"); + const [value, updateCookie] = useCookie("shared-demo-cookie", "A", { path: "/" }); return (

diff --git a/packages/website-astro/src/content/docs/state/useCookie.mdx b/packages/website-astro/src/content/docs/state/useCookie.mdx index 19ff34df..a5bb1619 100644 --- a/packages/website-astro/src/content/docs/state/useCookie.mdx +++ b/packages/website-astro/src/content/docs/state/useCookie.mdx @@ -38,8 +38,8 @@ function Demo() { const cookieName = "cookie-key"; const [cookieValue, updateCookie, refreshCookie] = useCookie( cookieName, - defaultOption, - "default-value" + "default-value", + defaultOption ); const updateButtonClick = () => { @@ -81,7 +81,7 @@ Two components using the same cookie key stay in sync within the tab — click a ```tsx live noInline function CookiePanel({ label }) { - const [value, updateCookie] = useCookie("shared-demo-cookie", { path: "/" }, "A"); + const [value, updateCookie] = useCookie("shared-demo-cookie", "A", { path: "/" }); return (