Пошук уроків, статей та іншого контенту
Розглянете типи аргументів, результатів і стану для повторно використовуваних власних хуків.
Custom Hook — це функція, назва якої починається з use і яка може використовувати інші хуки React.
Типізація власного хука допомагає:
перевіряти типи аргументів під час виклику;
описувати значення, які повертає хук;
правильно типізувати внутрішній стан;
отримувати підказки IDE;
зафіксувати зрозумілий контракт для повторного використання.
Типи власного хука варто розглядати на трьох рівнях:
аргументи функції;
внутрішній стан;
результат, який повертається.
Аргументи хука типізуються так само, як аргументи звичайної TypeScript-функції.
import { useState } from "react";
function useCounter(initialValue: number): {
count: number;
increment: () => void;
} {
const [count, setCount] = useState(initialValue);
function increment() {
setCount((currentCount) => currentCount + 1);
}
return {
count,
increment,
};
}Тепер TypeScript не дозволить передати рядок замість числа:
const counter = useCounter(10);
// Помилка TypeScript:
// Argument of type 'string' is not assignable to parameter of type 'number'
const invalidCounter = useCounter("10");Необов’язкові аргументи позначаються знаком ? або мають значення за замовчуванням.
function useCounter(initialValue: number = 0) {
const [count, setCount] = useState(initialValue);
function increment() {
setCount((currentCount) => currentCount + 1);
}
return {
count,
increment,
};
}
useCounter();
useCounter(5);У цьому прикладі initialValue завжди має тип number, але його можна не передавати.
Тип стану часто можна вивести автоматично:
const [count, setCount] = useState(0);TypeScript визначить тип count як number.
Проте для складніших або порожніх початкових значень тип краще вказати явно.
Порожній масив не містить достатньо інформації для визначення типу його елементів:
const [items, setItems] = useState<string[]>([]);Тепер TypeScript знає, що в items можуть бути лише рядки.
setItems(["React", "TypeScript"]);
// Помилка TypeScript:
// Argument of type 'number' is not assignable to parameter of type 'string'
setItems([1, 2, 3]);Для масиву об’єктів спочатку створюють окремий тип:
type User = {
id: number;
name: string;
};
const [users, setUsers] = useState<User[]>([]);nullЯкщо стан спочатку порожній, але пізніше міститиме об’єкт, потрібно явно вказати обидва можливі типи:
type User = {
id: number;
name: string;
};
const [user, setUser] = useState<User | null>(null);Без | null TypeScript може неправильно зрозуміти початковий стан або не дозволити встановити null.
Власний хук може повертати об’єкт, масив або кортеж. Для повторно використовуваних хуків бажано явно описувати тип результату.
type UseCounterResult = {
count: number;
increment: () => void;
decrement: () => void;
};
function useCounter(initialValue: number = 0): UseCounterResult {
const [count, setCount] = useState<number>(initialValue);
function increment() {
setCount((currentCount) => currentCount + 1);
}
function decrement() {
setCount((currentCount) => currentCount - 1);
}
return {
count,
increment,
decrement,
};
}Використання:
function Counter() {
const { count, increment, decrement } = useCounter(10);
return (
<div>
<p>Значення: {count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}Тип результату гарантує, що хук завжди повертає:
count типу number;
increment — функцію без аргументів;
decrement — функцію без аргументів.
Об’єкт часто є зручнішим за масив, оскільки його властивості мають іменовані ключі.
Хук може повертати кортеж, якщо порядок значень є частиною його API.
type UseToggleResult = [
value: boolean,
toggle: () => void,
reset: () => void,
];
function useToggle(initialValue: boolean = false): UseToggleResult {
const [value, setValue] = useState<boolean>(initialValue);
function toggle() {
setValue((currentValue) => !currentValue);
}
function reset() {
setValue(initialValue);
}
return [value, toggle, reset];
}Використання:
function Menu() {
const [isOpen, toggle, reset] = useToggle();
return (
<div>
<button onClick={toggle}>
{isOpen ? "Закрити" : "Відкрити"}
</button>
<button onClick={reset}>Скинути</button>
</div>
);
}Кортеж потрібно описувати саме як кортеж. Якщо не вказати тип результату, TypeScript може вивести звичайний масив, у якого кожен елемент має об’єднаний тип:
function useToggleWithoutType() {
const [value, setValue] = useState(false);
function toggle() {
setValue((currentValue) => !currentValue);
}
return [value, toggle];
}У такому випадку результат може бути виведений приблизно як масив типу (boolean | (() => void))[], а не як [boolean, () => void].
Для коротких кортежів можна використати as const, але явний тип результату зазвичай краще документує контракт хука.
Якщо хук повертає функцію для зміни стану, її тип залежить від способу оновлення стану.
Для простого setter-а можна використати типи React:
import type { Dispatch, SetStateAction } from "react";
type UseQueryResult = {
query: string;
setQuery: Dispatch<SetStateAction<string>>;
};Dispatch<SetStateAction<string>> дозволяє передати:
setQuery("React");
setQuery((currentQuery) => `${currentQuery} TypeScript`);Такий тип відповідає setter-у, який повертає useState.
Якщо хук приховує setter і повертає лише власні операції, краще описати конкретні функції:
type UseCounterResult = {
count: number;
increment: () => void;
incrementBy: (amount: number) => void;
};Це обмежує API хука лише необхідними операціями.
Generic дає змогу створити хук, який працює з різними типами даних і зберігає їхню типізацію.
Наприклад, хук затримки значення може працювати з рядками, числами, об’єктами або масивами:
import { useEffect, useState } from "react";
function useDebouncedValue<T>(value: T, delayMs: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timerId = window.setTimeout(() => {
setDebouncedValue(value);
}, delayMs);
return () => {
window.clearTimeout(timerId);
};
}, [value, delayMs]);
return debouncedValue;
}Під час виклику TypeScript визначає T з типу першого аргументу:
const debouncedText = useDebouncedValue("React", 300);
// string
const debouncedNumber = useDebouncedValue(42, 300);
// number
const debouncedUser = useDebouncedValue(
{ id: 1, name: "Anna" },
300,
);
// { id: number; name: string }У цьому хук не втрачає тип початкового значення.
У наступному прикладі:
useDebouncedValue<T> має узагальнений аргумент і результат;
useProductSearch має аргумент зі значенням за замовчуванням;
масив товарів має тип Product[];
результат useProductSearch описаний окремим типом;
useState явно типізований там, де це покращує читабельність.
import {
useEffect,
useMemo,
useState,
} from "react";
import type {
Dispatch,
SetStateAction,
} from "react";
type Product = {
id: number;
name: string;
category: string;
};
const products: Product[] = [
{
id: 1,
name: "React",
category: "Frontend",
},
{
id: 2,
name: "TypeScript",
category: "Frontend",
},
{
id: 3,
name: "Node.js",
category: "Backend",
},
];
function useDebouncedValue<T>(
value: T,
delayMs: number,
): T {
const [debouncedValue, setDebouncedValue] =
useState<T>(value);
useEffect(() => {
const timerId = window.setTimeout(() => {
setDebouncedValue(value);
}, delayMs);
return () => {
window.clearTimeout(timerId);
};
}, [value, delayMs]);
return debouncedValue;
}
type UseProductSearchResult = {
query: string;
setQuery: Dispatch<SetStateAction<string>>;
results: Product[];
isWaiting: boolean;
};
function useProductSearch(
initialQuery: string = "",
): UseProductSearchResult {
const [query, setQuery] = useState<string>(
initialQuery,
);
const debouncedQuery = useDebouncedValue(
query,
300,
);
const results = useMemo<Product[]>(() => {
const normalizedQuery = debouncedQuery
.trim()
.toLowerCase();
if (normalizedQuery === "") {
return products;
}
return products.filter((product) => {
const searchableText = [
product.name,
product.category,
]
.join(" ")
.toLowerCase();
return searchableText.includes(normalizedQuery);
});
}, [debouncedQuery]);
return {
query,
setQuery,
results,
isWaiting: query !== debouncedQuery,
};
}
export default function ProductSearch() {
const {
query,
setQuery,
results,
isWaiting,
} = useProductSearch();
return (
<section>
<label htmlFor="product-search">
Пошук товарів
</label>
<input
id="product-search"
value={query}
onChange={(event) => {
setQuery(event.target.value);
}}
placeholder="Введіть назву або категорію"
/>
{isWaiting && <p>Оновлення результатів...</p>}
<ul>
{results.map((product) => (
<li key={product.id}>
{product.name} — {product.category}
</li>
))}
</ul>
</section>
);
}Тип Product визначає форму кожного елемента:
type Product = {
id: number;
name: string;
category: string;
};Тому TypeScript перевіряє доступ до властивостей:
product.name;
product.category;
product.id;Тип результату хука описує його публічний API:
type UseProductSearchResult = {
query: string;
setQuery: Dispatch<SetStateAction<string>>;
results: Product[];
isWaiting: boolean;
};Якщо в об’єкті, який повертає хук, буде пропущена властивість або матиме неправильний тип, TypeScript повідомить про помилку.
Водночас тип внутрішньої змінної debouncedQuery визначається автоматично як string, оскільки хук отримує рядок:
const debouncedQuery = useDebouncedValue(
query,
300,
);Використовуйте об’єкт, коли хук повертає кілька значень із різним призначенням:
type UseFormResult = {
values: Record<string, string>;
isValid: boolean;
submit: () => void;
};Переваги:
зрозумілі імена властивостей;
порядок властивостей не має значення;
легше розширювати API хука.
Використовуйте кортеж, коли значення мають просту і стабільну послідовність:
type UseToggleResult = [
value: boolean,
toggle: () => void,
];Перевага кортежу — короткий виклик:
const [isOpen, toggle] = useToggle();Однак додавання нового елемента до кортежу може змінити порядок деструктуризації в усіх компонентах, які використовують хук.
TypeScript може автоматично вивести тип результату:
function useCounter() {
const [count, setCount] = useState(0);
return {
count,
increment: () => {
setCount((currentCount) => currentCount + 1);
},
};
}Цей варіант працює, але для публічних або повторно використовуваних хуків явний тип часто кращий:
type UseCounterResult = {
count: number;
increment: () => void;
};
function useCounter(): UseCounterResult {
const [count, setCount] = useState<number>(0);
return {
count,
increment: () => {
setCount((currentCount) => currentCount + 1);
},
};
}Явний тип:
документує очікуваний API;
допомагає помітити помилку прямо всередині хука;
спрощує рефакторинг;
показує користувачам хука доступні значення та функції.
Для маленького локального хука виведення типів може бути достатнім. Для загальнодоступного хука бажано описувати результат окремим типом.
const [items, setItems] = useState([]);У такому випадку TypeScript може вивести тип елементів як never, і додавання значень спричинить помилку.
Правильно:
const [items, setItems] = useState<string[]>([]);Або для об’єктів:
const [items, setItems] = useState<Product[]>([]);nullНеправильно:
const [user, setUser] = useState<User>(null);Правильно:
const [user, setUser] = useState<User | null>(null);Якщо значення може бути відсутнім, це потрібно відобразити в типі.
Неправильно:
function useToggle() {
const [value, setValue] = useState(false);
return [
value,
() => setValue((currentValue) => !currentValue),
];
}TypeScript може сприйняти результат як звичайний масив об’єднаного типу.
Правильно:
function useToggle(): [boolean, () => void] {
const [value, setValue] = useState(false);
return [
value,
() => setValue((currentValue) => !currentValue),
];
}anyfunction useData(value: any): any {
return value;
}any вимикає значну частину перевірок TypeScript. Якщо хук працює з різними типами, краще використати generic:
function useData<T>(value: T): T {
return value;
}Якщо функція очікує аргумент, це потрібно відобразити в типі:
type UseCounterResult = {
incrementBy: (amount: number) => void;
};Функція без аргументів або функція з аргументом іншого типу не відповідатиме такому контракту.
Аргументи Custom Hook типізуються як аргументи звичайної функції.
Для складного стану використовуйте явні типи в useState.
Для порожніх масивів вказуйте тип елементів: useState<Item[]>([]).
Якщо стан може бути відсутнім, додавайте null до union-типу.
Результат хука зручніше описувати окремим типом.
Для об’єкта результату використовуйте іменовані властивості.
Для кортежу явно вказуйте порядок і тип кожного елемента.
Для універсальних хуків використовуйте generic-параметри.
Не замінюйте точні типи значенням any.