Skip to content

Commit f8c1371

Browse files
committed
feat(hooks): add useViewportBreakpoint hook
1 parent f88930a commit f8c1371

25 files changed

+1060
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const { useBoolean, useEffectCompare, useWindowEvent } = require('@webeach/react
125125
+ [useTimeoutExtended](./docs/en/useTimeoutExtended.md)
126126
+ [useToggle](./docs/en/useToggle.md)
127127
+ [useUnmount](./docs/en/useUnmount.md)
128+
+ [useViewportBreakpoint](./docs/en/useViewportBreakpoint.md)
128129
+ [useWindowEvent](./docs/en/useWindowEvent.md)
129130

130131
### By category
@@ -197,6 +198,7 @@ const { useBoolean, useEffectCompare, useWindowEvent } = require('@webeach/react
197198
- [useMediaQuery](./docs/en/useMediaQuery.md)
198199
- [usePageTitle](./docs/en/usePageTitle.md)
199200
- [usePageVisibility](./docs/en/usePageVisibility.md)
201+
- [useViewportBreakpoint](./docs/en/useViewportBreakpoint.md)
200202

201203
#### Utilities
202204
- [useDemandStructure](./docs/en/useDemandStructure.md)

README.ru.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const { useBoolean, useEffectCompare, useWindowEvent } = require('@webeach/react
125125
+ [useTimeoutExtended](./docs/ru/useTimeoutExtended.md)
126126
+ [useToggle](./docs/ru/useToggle.md)
127127
+ [useUnmount](./docs/ru/useUnmount.md)
128+
- [useViewportBreakpoint](./docs/en/useViewportBreakpoint.md)
128129
+ [useWindowEvent](./docs/ru/useWindowEvent.md)
129130

130131
### По категориям
@@ -197,6 +198,7 @@ const { useBoolean, useEffectCompare, useWindowEvent } = require('@webeach/react
197198
- [useMediaQuery](./docs/ru/useMediaQuery.md)
198199
- [usePageTitle](./docs/ru/usePageTitle.md)
199200
- [usePageVisibility](./docs/ru/usePageVisibility.md)
201+
- [useViewportBreakpoint](./docs/ru/useViewportBreakpoint.md)
200202

201203
#### Служебные
202204
- [useDemandStructure](./docs/ru/useDemandStructure.md)

docs/en/useMediaQuery.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,4 @@ export function DesktopLandscape() {
147147
## See also
148148

149149
- [useResizeObserver](useResizeObserver.md)
150+
- [useViewportBreakpoint](useViewportBreakpoint.md)

docs/en/useResizeObserver.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,4 @@ export function ResponsiveGrid() {
166166

167167
- [useIntersectionObserver](useIntersectionObserver.md)
168168
- [useMediaQuery](useMediaQuery.md)
169+
- [useViewportBreakpoint](useViewportBreakpoint.md)

docs/en/useViewportBreakpoint.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# `useViewportBreakpoint`
2+
3+
## Description
4+
5+
`useViewportBreakpoint` — a React hook for tracking responsive breakpoints using `window.matchMedia`.
6+
7+
- Converts a breakpoint map into a sorted list and creates a `MediaQueryList` for each one (with caching).
8+
- Automatically updates the active breakpoint when the viewport width changes.
9+
- Returns `matches` — an object with all breakpoints and their state (true/false), and `activeBreakpoint` — the currently active breakpoint.
10+
- Supports `defaultBreakpoint` as a fallback, especially useful in SSR to provide a predictable value before hydration.
11+
- The returned structure is hybrid: available both as a tuple `[matches, activeBreakpoint]` and as an object `{ matches, activeBreakpoint }`.
12+
13+
---
14+
15+
## Signature
16+
17+
```ts
18+
function useViewportBreakpoint<BreakpointKey extends string | symbol>(
19+
breakpointMap: Record<BreakpointKey, number>,
20+
options?: UseViewportBreakpointOptions<BreakpointKey>,
21+
): UseViewportBreakpointReturn<BreakpointKey>;
22+
```
23+
24+
- **Parameters**
25+
- `breakpointMap: Record<BreakpointKey, number>` — map of breakpoint keys to their minimum width in pixels.
26+
- `options?: UseViewportBreakpointOptions` — optional settings:
27+
- `defaultBreakpoint?: BreakpointKey` — fallback key when no breakpoint matches.
28+
29+
- **Returns**: `UseViewportBreakpointReturn` — a hybrid structure with both tuple and object forms:
30+
- `matches: Record<BreakpointKey, boolean>` — map of all breakpoints and whether they currently match.
31+
- `activeBreakpoint: BreakpointKey | null` — the currently active breakpoint key, or `null` if none matches.
32+
- Tuple access: `[matches, activeBreakpoint]`.
33+
34+
---
35+
36+
## Examples
37+
38+
### 1) Basic usage (tuple)
39+
40+
```tsx
41+
import { useViewportBreakpoint } from '@webeach/react-hooks/useViewportBreakpoint';
42+
43+
export function ResponsiveLayout() {
44+
const [matches, active] = useViewportBreakpoint({
45+
mobile: 0,
46+
tablet: 768,
47+
desktop: 1200,
48+
});
49+
50+
return (
51+
<div>
52+
<p>Active breakpoint: {String(active)}</p>
53+
{matches.mobile && <MobileMenu />}
54+
{matches.desktop && <DesktopNav />}
55+
</div>
56+
);
57+
}
58+
```
59+
60+
### 2) Named access (object)
61+
62+
```tsx
63+
import { useViewportBreakpoint } from '@webeach/react-hooks/useViewportBreakpoint';
64+
65+
export function Sidebar() {
66+
const { matches, activeBreakpoint } = useViewportBreakpoint({
67+
narrow: 0,
68+
wide: 1000,
69+
});
70+
71+
return (
72+
<aside>
73+
<h2>Breakpoint: {String(activeBreakpoint)}</h2>
74+
{matches.narrow && <CollapsedSidebar />}
75+
{matches.wide && <ExpandedSidebar />}
76+
</aside>
77+
);
78+
}
79+
```
80+
81+
### 3) With defaultBreakpoint
82+
83+
```tsx
84+
const { matches, activeBreakpoint } = useViewportBreakpoint(
85+
{ sm: 0, md: 600, lg: 1200 },
86+
{ defaultBreakpoint: 'sm' },
87+
);
88+
89+
// If no media query matches, 'sm' will be returned as active (relevant in SSR).
90+
```
91+
92+
---
93+
94+
## Behavior
95+
96+
1. **Sorted breakpoints**
97+
- Breakpoints are sorted by their numeric width values (from smallest to largest).
98+
2. **Single active breakpoint**
99+
- Only one breakpoint can be active (`activeBreakpoint`) at any given time.
100+
3. **SSR safety**
101+
- On the server, empty values are returned, and the hook activates properly in the browser.
102+
4. **Media query caching**
103+
- Each `min-width` media query is cached to avoid creating duplicate `MediaQueryList` instances.
104+
5. **Stable return structure**
105+
- The returned object/tuple is stable thanks to `useDemandStructure`.
106+
6. **Fallback breakpoint**
107+
- If no media query matches and a `defaultBreakpoint` is provided, it is used as the active breakpoint.
108+
- Especially useful in SSR to have a predictable value before hydration in the browser.
109+
110+
---
111+
112+
## When to use
113+
114+
- Responsive layouts where you need to know the active breakpoint in React.
115+
- Conditional rendering of UI elements depending on viewport width.
116+
- Managing adaptive components (menus, sidebars, navigation, grids).
117+
118+
---
119+
120+
## When **not** to use
121+
122+
- If you only need CSS-based breakpoints without JS awareness — prefer pure CSS.
123+
- For extremely performance-sensitive contexts with dozens of listeners — consider optimizing.
124+
125+
---
126+
127+
## Common mistakes
128+
129+
1. **Expecting multiple breakpoints to be active**
130+
- Only one breakpoint can be `true` at a time.
131+
2. **Forgetting to provide a default breakpoint**
132+
- Without it, `activeBreakpoint` can be `null` when none matches.
133+
3. **Overlapping or unsorted values**
134+
- Always provide consistent ascending values for breakpoints.
135+
136+
---
137+
138+
## Typing
139+
140+
**Exported types**
141+
142+
- `UseViewportBreakpointMatches<BreakpointKey>`
143+
- Record of breakpoints and their boolean state.
144+
145+
- `UseViewportBreakpointOptions<BreakpointKey>`
146+
- Options object with `defaultBreakpoint`.
147+
148+
- `UseViewportBreakpointReturn<BreakpointKey>`
149+
- Hybrid: tuple `[matches, activeBreakpoint]` **and** object `{ matches, activeBreakpoint }`.
150+
151+
---
152+
153+
## See also
154+
155+
- [useResizeObserver](useResizeObserver.md)
156+
- [useMediaQuery](useMediaQuery.md)

docs/ru/useMediaQuery.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,4 @@ export function DesktopLandscape() {
147147
## Смотрите также
148148

149149
- [useResizeObserver](useResizeObserver.md)
150+
- [useViewportBreakpoint](useViewportBreakpoint.md)

docs/ru/useResizeObserver.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,4 @@ export function ResponsiveGrid() {
167167

168168
- [useIntersectionObserver](useIntersectionObserver.md)
169169
- [useMediaQuery](useMediaQuery.md)
170+
- [useViewportBreakpoint](useViewportBreakpoint.md)

docs/ru/useViewportBreakpoint.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# `useViewportBreakpoint`
2+
3+
## Описание
4+
5+
`useViewportBreakpoint` — React-хук для отслеживания брейкпоинтов (точек перелома) с помощью `window.matchMedia`.
6+
7+
- Преобразует карту брейкпоинтов в отсортированный список и создаёт для каждого `MediaQueryList` (с кешированием).
8+
- Автоматически обновляет активный брейкпоинт при изменении ширины окна.
9+
- Возвращает `matches` — объект со всеми брейкпоинтами и их состоянием (true/false) и `activeBreakpoint` — текущий активный брейкпоинт.
10+
- Поддерживает `defaultBreakpoint` как запасной вариант, особенно полезный при SSR для предсказуемого значения до монтирования.
11+
- Возвращаемая структура гибридная: доступна и как кортеж `[matches, activeBreakpoint]`, и как объект `{ matches, activeBreakpoint }`.
12+
13+
---
14+
15+
## Сигнатура
16+
17+
```ts
18+
function useViewportBreakpoint<BreakpointKey extends string | symbol>(
19+
breakpointMap: Record<BreakpointKey, number>,
20+
options?: UseViewportBreakpointOptions<BreakpointKey>,
21+
): UseViewportBreakpointReturn<BreakpointKey>;
22+
```
23+
24+
- **Параметры**
25+
- `breakpointMap: Record<BreakpointKey, number>` — карта брейкпоинтов: ключ → минимальная ширина в пикселях.
26+
- `options?: UseViewportBreakpointOptions` — дополнительные настройки:
27+
- `defaultBreakpoint?: BreakpointKey` — запасной ключ, который считается активным, если ни один брейкпоинт не подходит.
28+
29+
- **Возвращает**: `UseViewportBreakpointReturn` — гибридная структура с кортежем и объектом:
30+
- `matches: Record<BreakpointKey, boolean>` — карта всех брейкпоинтов с их состоянием.
31+
- `activeBreakpoint: BreakpointKey | null` — активный брейкпоинт или `null`, если ни один не подходит.
32+
- Кортеж: `[matches, activeBreakpoint]`.
33+
34+
---
35+
36+
## Примеры
37+
38+
### 1) Базовое использование (кортеж)
39+
40+
```tsx
41+
import { useViewportBreakpoint } from '@webeach/react-hooks/useViewportBreakpoint';
42+
43+
export function ResponsiveLayout() {
44+
const [matches, active] = useViewportBreakpoint({
45+
mobile: 0,
46+
tablet: 768,
47+
desktop: 1200,
48+
});
49+
50+
return (
51+
<div>
52+
<p>Активный брейкпоинт: {String(active)}</p>
53+
{matches.mobile && <MobileMenu />}
54+
{matches.desktop && <DesktopNav />}
55+
</div>
56+
);
57+
}
58+
```
59+
60+
### 2) Именованный доступ (объект)
61+
62+
```tsx
63+
import { useViewportBreakpoint } from '@webeach/react-hooks/useViewportBreakpoint';
64+
65+
export function Sidebar() {
66+
const { matches, activeBreakpoint } = useViewportBreakpoint({
67+
narrow: 0,
68+
wide: 1000,
69+
});
70+
71+
return (
72+
<aside>
73+
<h2>Брейкпоинт: {String(activeBreakpoint)}</h2>
74+
{matches.narrow && <CollapsedSidebar />}
75+
{matches.wide && <ExpandedSidebar />}
76+
</aside>
77+
);
78+
}
79+
```
80+
81+
### 3) С запасным брейкпоинтом
82+
83+
```tsx
84+
const { matches, activeBreakpoint } = useViewportBreakpoint(
85+
{ sm: 0, md: 600, lg: 1200 },
86+
{ defaultBreakpoint: 'sm' },
87+
);
88+
89+
// Если ни один брейкпоинт не подходит, будет возвращён 'sm' (актуально в SSR).
90+
```
91+
92+
---
93+
94+
## Поведение
95+
96+
1. **Сортировка брейкпоинтов**
97+
- Брейкпоинты сортируются по числовым значениям ширины (от меньшего к большему).
98+
2. **Один активный брейкпоинт**
99+
- В каждый момент времени активен только один брейкпоинт (`activeBreakpoint`).
100+
3. **SSR-безопасность**
101+
- На сервере возвращаются пустые значения, в браузере хук активируется корректно.
102+
4. **Кэширование медиа-запросов**
103+
- Каждый `min-width` медиа-запрос кэшируется, чтобы избежать создания дубликатов `MediaQueryList`.
104+
5. **Стабильная структура возврата**
105+
- Возвращаемый объект/кортеж стабилен благодаря `useDemandStructure`.
106+
6. **Запасной брейкпоинт**
107+
- Если ни один медиа-запрос не совпал и указан `defaultBreakpoint`, он используется как активный брейкпоинт.
108+
- Особенно полезно при SSR, чтобы иметь предсказуемое значение ещё до монтирования в браузере.
109+
110+
---
111+
112+
## Когда использовать
113+
114+
- Для адаптивных интерфейсов, когда нужно знать активный брейкпоинт в React.
115+
- Для условного рендера UI в зависимости от ширины окна.
116+
- Для управления адаптивными компонентами (меню, сайдбары, сетки).
117+
118+
---
119+
120+
## Когда **не** использовать
121+
122+
- Если достаточно только CSS-медиа-запросов без участия JS.
123+
- В особо производительных случаях с десятками слушателей — стоит оптимизировать.
124+
125+
---
126+
127+
## Частые ошибки
128+
129+
1. **Ожидание нескольких активных брейкпоинтов**
130+
- В один момент времени активен только один.
131+
2. **Отсутствие запасного брейкпоинта**
132+
- Без `defaultBreakpoint` `activeBreakpoint` может быть `null`.
133+
3. **Несогласованные значения**
134+
- Всегда указывайте возрастающие числа для брейкпоинтов.
135+
136+
---
137+
138+
## Типизация
139+
140+
**Экспортируемые типы**
141+
142+
- `UseViewportBreakpointMatches<BreakpointKey>`
143+
- Карта брейкпоинтов и их булевых состояний.
144+
145+
- `UseViewportBreakpointOptions<BreakpointKey>`
146+
- Опции с полем `defaultBreakpoint`.
147+
148+
- `UseViewportBreakpointReturn<BreakpointKey>`
149+
- Гибрид: кортеж `[matches, activeBreakpoint]` **и** объект `{ matches, activeBreakpoint }`.
150+
151+
---
152+
153+
## Смотрите также
154+
155+
- [useResizeObserver](useResizeObserver.md)
156+
- [useMediaQuery](useMediaQuery.md)

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@
328328
"import": "./lib/esm/hooks/useUnmount/index.js",
329329
"require": "./lib/cjs/hooks/useUnmount/index.js"
330330
},
331+
"./useViewportBreakpoint": {
332+
"types": "./lib/types/hooks/useViewportBreakpoint/index.d.ts",
333+
"import": "./lib/esm/hooks/useViewportBreakpoint/index.js",
334+
"require": "./lib/cjs/hooks/useViewportBreakpoint/index.js"
335+
},
331336
"./useWindowEvent": {
332337
"types": "./lib/types/hooks/useWindowEvent/index.d.ts",
333338
"import": "./lib/esm/hooks/useWindowEvent/index.js",

0 commit comments

Comments
 (0)