Выбір правильнага інструмента для React: Derive, Handle, Fetch, Defer чыра Effect
Канал прыямоў для замены вызваў функциі useEffect з рэфлексіяй на вырахаваныя значэння, обрабоўчыкі змаганняў, шар дадзенаў, useTransition, measured useMemo і API-і React 19.
Крыніцы: Stop Using `useEffect` for Everything: 7 React Patterns That Make Your Code Simpler
Большынства компанентаў React скарыстоюцца функцыяй useEffect для запрашання дадзеных, реакціі на параметры, вычыслення значэнняў і падтрымкі супараднасці стану. Чырэх месцаў пазней, калі вы ачынаеце файл, з’являюцца чатыры вызовы Effect, тры значэнняя стану, два флагі загрузкі і баг, які ніхто не можа паспяшна адгукнуцца. Шырокі падчыненне дае вам аднае запитанне, якое трэба задаць пры стварэнні будзь-каго хука: «Чы рэальна тут патрэбны Effect?», а таксама семь конкрэтных патэранаў таго, калі адпаведны ўтварыць няма.
Вычысленыя значэнняя належаць у функцыю render, а не ў стан
Найчастэйшы неправільны спосаб — зберагаччаць тое, што можна вычысліць. Дапустім, вы адображаеце тавары, фільтраваныя за выразам пошуку. Частая першая спроба — стварыць зменную стану для рэзультата:
const [filteredProducts, setFilteredProducts] = useState([]);
а пасля — Effect, які перывычысляе ёй значэнне кожны раз, калі змянююцца вхідныя даны:
useEffect(() => {
setFilteredProducts(
products.filter(product =>
product.name.toLowerCase().includes(query.toLowerCase())
)
);
}, [products, query]);
Эта методыка працюе, але filteredProducts не ўтварае незалежнага стану. Це простая функцыя з значэнняў, якія вже ў вас є:
products
+
query
=
filteredProducts
Якщо яго зберагаць, React спачатку атрыбуе застарэлыя рэзультаты, пасля чаго запускае Effect і зноў атрыбуе. Заместа таго вычысціце яго безпасова ў самай тэле компонента:
const filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(query.toLowerCase())
);
У такім разе у вас не будзе дапаможнага зменнага стану, не будзе Effect, няма чаго синхронізаваць, і не будзе другог атрыбування толькі для отрымання рэзультата. Якщо фільтрацыя справды ўсконала, напрыклад для дужа большай лісты, загорніце тую ж выразу ў useMemo:
const filteredProducts = useMemo(() => {
return products.filter(product =>
product.name.toLowerCase().includes(query.toLowerCase())
);
}, [products, query]);
Мемацыя — це дробны прыем. Галоўнае — гэты прынцып: значэнне, якое вырашаецца з props або стану, не патрабуе сваёго месца ў стане.
Дзеяння корыстніка павінны быць у обробчыках змаганняў
Іншы патэрн — перадае клік через стан, каб Effect мог усё гэта адразу зробіць. Спачатку — флаг:
const [shouldSave, setShouldSave] = useState(false);
Потым – Effect, які стежыць за флагам, а таксама працоўнік, які яго задае:
useEffect(() => {
if (shouldSave) {
saveUser();
setShouldSave(false);
}
}, [shouldSave]);function handleSave() {
setShouldSave(true);
}
Няма прычыны для зайвых перапрыёмоў. Клік – гэта момент, калі павінна выйсці дзеянне, і у вас вже ёсць функцыя, якая запускаецца ў той момент:
function handleSave() {
saveUser();
}
Пораўняйце два падходы. У версіі з флагам трэба пройсці калькі крокаў, перш чым ўсё будзе захавана, а пасля – запусціць ўтрохтуючы рендэр, каб перазначыць стан:
Click
↓
State update
↓
Render
↓
Effect
↓
saveUser()
↓
Another state update
↓
Render
У прямой версіи є толькі адны крок:
Click
↓
saveUser()
Загальны прынцып такі: запуск дзеяноў адбываецца ў працоўніках падзей, а Effectы існуюць для сінхронізаціі з системамі за межамі React, а не для маскавання звычайной логіки падзей.
Копіюйце прынтары ў стан толькі тады, калі вам патрэбны два значэння
Гэты падход выглядае безнебяжным. Компонент зберагае сабой копію імені:
const [name, setName] = useState('');
а Effect перазапісвае гэту копію ўсякі раз, калі змяніцца прынтар:
useEffect(() => {
setName(user.name);
}, [user]);
Цякмо, існуе два адказы пра правду, і ў кожны момент трэба спытацца, які з іх правильны:
user.name
+
name
Якщо вы толькі адображаеце значэнне, чытаючы яго безпосередна з атрыбута:
return <h2>{user.name}</h2>;
Існуе легітымны выключэнне: форма для рэдагавання, якая намеравана зберагчы тымчасовую, можна рэдагаваць копію данных сервера, пакуль корыстнік не запішае іх чы не анулюе дзеянне:
Server data
↓
Edit form
↓
User changes fields
↓
Save / Cancel
Хутчэй, локальны стан ў правільнам стане, таму што праект насправды ёсць адзінэчны значэнне з сае власной жыцёвай ціклам. Урок не ў тым, «ніколі не копіюйце прапсы ў стан», а ў тым, «знаёце, чаму вы ствараеце другы источнік правды». У случае формы, скасаванне праекта праз задачу форме key, які прыяўляецца да ID запису, часта ёсць чыстэйшым спосабам, чым сінхронізацыя з Effect; наша стаття пра замену Effect-аў для сінхронізацыі стану детальна расказвае пра гэты метод.
Дазвольце слою дадзеных кераваць запрашэннямі
Загрузка дадзеных часта распадаецца на кантэксты Effect. Яна пачынаецца з аднаго запрашэння пад час стварэння компонента:
useEffect(() => {
fetchUser();
}, []);
і ператвараецца ў низ Effect-аў, кожны з якіх запрашоўвае дадзеныя для таго ж корыстніка:
useEffect(() => {
fetchOrders();
}, [userId]);useEffect(() => {
fetchNotifications();
}, [userId]);useEffect(() => {
fetchPermissions();
}, [userId]);
У такі моменты компанента стае шарам аранжавання API. У большых прыкладах програмнародзення тут саме практычныя рашэнні для запошуку дадзейна. Залежна ад вашага стака, гэта можа быць:
- TanStack Query
- SWR
- функцыі запошуку дадзейна вашай фрэймворк
- Server Components, якія падходзяць у такых ситуацыях
Інакш кажучы, вам даведзяць самостоятельна рашыдаць кожную з гэтых проблем для кожнага запытку:
loading
error
data
refetch
cache
retry
stale data
race conditions
Шар дадзейна адрабатвае ўсё гэта за вас. Канцэптуальна, запытка паказвае, калікі дадзейна ёй трэба і як іх запрашыць, а таксама вяртае текущы статус:
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
Ключ запыткі таксама служыць ключам кэшу, таму компаненты, якія запрашаюць адно і тое ж пользоўніка, дзелаюць адной запыткай, а змена userId запрашвае новага пользоўніка, калі жадныя застарэлыя адпаведзенні не будуць выкарыстоўваны. Компанента знову працюе над атрыбутамі UI, а не выступае як маленькая бібліятэка для сетевых задач.
Адзначыце неаспэчныя апдэты за дапамою useTransition
Разглядзім канцэлер для пошуку, який выкарыстоўвае дорогі таблы. Праз адну толькі змяну стану кожны натыск клавішы прыводзіць да пераробкі таблы, прычыму ўвод не можа паказаць наступны симвал:
function handleChange(e) {
setQuery(e.target.value);
}
Рашэнне — разлучыць значэння на два часткі: тое, што паказвае ўвод, і тое, па чым фільтруець таблы:
const [inputValue, setInputValue] = useState('');
const [query, setQuery] = useState('');
Значэнне ўводу апдэюецца звычайна, тады як апдэт запиту ўкладаецца ў startTransition:
const [isPending, startTransition] = useTransition();function handleChange(e) {
const value = e.target.value; setInputValue(value); startTransition(() => {
setQuery(value);
});
}
Тепер React ставіцца да гэтых двух апдэтаў розна. Натысканне клавіш є аспэчным:
Typing
↓
Urgent
тады як фільтрацыю і пераробку можна перарваць і адклаць:
Filtering/rendering
↓
Can wait
Пераходы не робяць медленны код швэдзейшым; яны зменяюць планаванне, каб уведамленні заставаліся адгукнымі, а новы натиск клавішы мог ўтрымаць процес адрасавання. isPending дазваляе вам падсказаць, што таблыця ўжо наздаганяе.
Змерыце перш чым кэшаваць
Апошнім нядачным падходам є оптымаўзаванне на інстынкты. Яно часта пачынаецца з адной кэшаванай вычысленні:
const result = useMemo(() => {
return calculateSomething(data);
}, [data]);
праз кэшаванае супарабоцтва:
const users = useMemo(() => {
return data.map(...);
}, [data]);
потым кэшаванае апісанне столбца:
const columns = useMemo(() => [...], []);
і кэшаваныя настройкі:
const options = useMemo(() => [...], []);
Шырока большасць значэнняў у компаненте стаюць кэшаванымі, пры тым ніхто ніколі не пераканаліваецца, чы не ўсё-такі якаясь частка працы є медленной. useMemo дапамагае з дорогімі вычысленнямі і стабільнымі канстантамі для кэшаванных дзецяў, але оптымаўзаванне павінна выконвацца пасля завершэння циклу:
Problem
↓
Measure
↓
Find bottleneck
↓
Optimize
↓
Measure again
а не як спрытнасць:
Component feels complicated
↓
Add useMemo
Для дешавых вычыслаў мемаізацыя толькі дадае надтэрагу з чытанням і масівы залежнасцяў, якія можу стаць застарэлымі. Профіляр у React DevTools паказвае, чы рэальна компонента ёсць вузкім месца.
Іспользуйце прымітывы, якія дадаў React 19
Некалькі прычынакаў з ранейшых версый React тепер маюць простыяя замены. Гадзінныя, калі прабываў прасылаць ref у свой сабе-компонент, было паказваць яго ў forwardRef. У React 19 функцыйныя компоненты прымоўляють ref як звычайны проп:
function CustomInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
Оптымістычны UI — ўсё тое ж прыклад. Уместо таго, каб ручна дадаць временны элемент у стэйт і памятаць пра тое, каб пазней яго адмахнуць чы заменіць:
setTodos(prev => [
...prev,
{
id: 'temp',
text,
pending: true
}
]);
можна выкарыстаць useOptimistic, які стварае временны відглед на аснове рэальнага стэйта:
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, todo) => [...state, todo]
);
Оптымістычныя значэння паказваюцца пад час выканання асінхронной дзеянні і заменяюцца на рэальныя todos як толькі дзеяння завершыцца, таму немае тымчасовых элементаў, якія трэба было б адчыстыць. Оіх павінны выклікаць усередзіні дзеяння або пераходу; крайнія случаі, связаныя з непрацэйнасцю запытоў, рассмотреныя ў нашай статыце пра способы адката з викорыстаннем оптымізма.
Для ID, якіе з’ўязваюць пазначкі з элементамі вводу для доступнасці, выкарыстоўваюць вбудованы хук:
const id = useId();
замест таго, каб стварыць яго адносна ўялкава:
const id = `input-${Math.random()}`;
Адносна ўялкавы ID змінюецца за кожны раз, калі відбываецца рендарынг, і разлічыцца межа серверам і кліентам, што наражае на пагубы процес гідратацыі і з’ўязок пазначак. React продовжае дадаваць прымітывы, якія безпасэродзе выражаюць намер, таму трэба ухілявацца ад рашэння текущых проблем за дапамою патэранаў, якія існавалі ранейш.
Спісак пунктав для прыемкі рашэння перад тым, як адагаць хук
Хораша разуменне React апавяраецца ў веданні таго, калі трэба не выкарыстоўваць хуки. Компонент не стане кращым, якшо будуць выкарыстоўваны ўсе яны:
useEffect()
useMemo()
useCallback()
useRef()
useState()
Старацца пра такі поток дадзеных, які лёгка ў розуменні. Перад тым, як адгукнуцца да хука, назваць проблему і падабралі ўсё да відпаведнага інструмента.
Для выведзеных дадзеных:
Calculate it.
Для дзеяння корыстніка:
Handle the event.
Для сінхронізацыі з чым-небудзь за межами React, такім як падпіска, таймер або не-React віджэт:
Consider useEffect.
Для чытання з зовнішняй базы дадзеных:
Consider useSyncExternalStore or a state library.
Для апдэйта, які мае значэнне, але не ўскорыты:
Consider useTransition.
Для вычыслення, якое можа быць рэзантным:
Measure first, then consider useMemo.
І калі React вже мае прыродны інструмент для выкаання такой задачы:
Use the primitive instead of rebuilding it yourself.
Галоўныя выводы
- Найздаровейшыя компаненты зазвычай маюць якомога менш непатрэбных хуків, а не якомога больш.
- Заменіце запитанне "Дзе я можу выкарыстоўваць
useEffect?" на запитанне "Чы рэальна тут патрэбен хук Effect?" У большасці случаў адпаведзь паказвае на вычысленні ў час рэндарування, обрабоцьку запускаючых падзеяў або бібліятэку дадзеных. - Перш чым вивачаць сінтаксі кожнага новага API React, выявіце проблему, якую ён рашае.
- Інодзе найэфектывнейшая оптымізацыя компаненту — это выдаленне хуків, а не ўжоўтворэнне новых.