Якщо ви використовуєте useEffect
з рефами, щоб обійти правила масиву залежностей в React, з новим компілятором React, який наближається, ви могли б натрапити на цю знайому помилку:
Значення рефів (властивість current) не можна доступати під час рендеру.
Або, можливо, ви вимкнули правила хуків для цього випадку, і натрапили на таке повідомлення:
Компілятор React пропустив оптимізацію цього компонента, оскільки одне або кілька правил React ESLint були вимкнені. Компілятор React працює тільки тоді, коли ваші компоненти дотримуються всіх правил React, вимкнення їх може призвести до несподіваної або неправильній поведінки react-compiler/react-compiler
💡Ідея: Що, якщо ми могли б виконувати ефект за потребою та одночасно передавати його залежності як аргумент для деструктуризації?
Представляємо usePureEffect
Хук usePureEffect
є альтернативою useEffect
з чистим зворотним викликом та точною реактивністю, яка поважає правила React без необхідності використання обхідних шляхів, таких як рефи, що оновлюються під час рендеру.
Термін «чистий» у usePureEffect
підкреслює відповідність принципам функціонального програмування. На відміну від традиційного useEffect
, usePureEffect
передає об'єкт залежностей функції як аргумент. Це дозволяє функції ефекту оголошуватися поза межами рендеру компонента без додаткової мемоізації.
Ось реалізація:
import { useEffect, useRef, useState } from "react"
type Destructor = () => void
type PureEffect = (dependencies: T) => void | Destructor
type Trigger = (objA: T, objB: T) => boolean
type Shape = Record
function bothAreFunctions(a: unknown, b: unknown): boolean {
return typeof a === "function" && typeof b === "function"
}
/**
* Порівнює два об'єкти залежностей на рівність.
*
* Порівняння є поверхневим, що означає, що вкладені об'єкти не порівнюються.
*
* Винятки:
* - функції завжди вважаються рівними
* - `NaN` вважається рівним `NaN`
*
* @param objA - Перший об'єкт для порівняння.
* @param objB - Другий об'єкт для порівняння.
* @returns `true`, якщо об'єкти рівні, `false` в іншому випадку.
*/
export function equal(objA: T, objB: T): boolean {
if (Object.is(objA, objB)) {
return true
}
if (objA === null || objB === null) {
throw new TypeError("Несподіване значення null")
}
if (typeof objA !== "object" || typeof objB !== "object") {
throw new TypeError("Очікуються об'єкти")
}
const keysA = Object.keys(objA) as Array
const keysB = Object.keys(objB) as Array
if (keysA.length !== keysB.length) {
return false
}
for (let index = 0; index < keysA.length; index++) {
const valueA = objA[keysA[index]]
const valueB = objB[keysA[index]]
if (!(keysA[index] in objB)) {
return false
}
if (bothAreFunctions(valueA, valueB)) {
continue
}
if (!Object.is(valueA, valueB)) {
return false
}
}
return true
}
/**
* usePureEffect — це кастомний хук, який виконує чистий ефект
* на основі наданих залежностей.
*
* Якщо наданий третій аргумент, ефект буде виконуватися
* тільки коли залежності зміняться. Залежності порівнюються поверхнево,
* значення функцій завжди ігноруються.
*
* @param effect - Функція ефекту, яку потрібно виконати при зміні залежностей.
* @param dependencies - Об'єкт, що містить залежності для ефекту.
* @param when - Опційна функція тригера, яка визначає, коли запускати ефект.
*/
export function usePureEffect(
effect: PureEffect,
dependencies: T,
when?: Trigger
): void {
const [state, setState] = useState({ dependencies, effect })
if (
(!when || when(state.dependencies, dependencies))
&& !equal(state.dependencies, dependencies)
) {
setState({ dependencies, effect })
}
useEffect(() => state.effect(state.dependencies), [state])
}
І найкраща частина полягає в тому, що React Compiler може оптимізувати цей хук без проблем.
Як працює usePureEffect
-
Передбачуване відстеження залежностей: Функція
equal
гарантує точні порівняння, ігноруючи функції. -
Користувацькі тригери з аргументом when: Опційна функція when дозволяє точно контролювати, коли виконувати ефект, без використання ненадійних обхідних шляхів — ефект виконується лише тоді, коли умова when також виконана.
3.
Впровадження залежностей як аргумент: Об’єкт передається безпосередньо у функцію ефекту, що робить її чистою.
Цей підхід усуває необхідність мемоізації зворотних викликів (callbacks) або відстеження контексту JavaScript у масиві залежностей.
Практичний випадок використання
Припустимо, вам потрібно виконувати ефект щоразу, коли змінюються певні властивості, але ви хочете ігнорувати зміни інших:
import { usePureEffect } from '@/hooks/usePureEffect';
interface AppProps {
propA: string;
propB: number;
}
// Зворотний виклик ефекту чистий та стабільний за посиланням:
function callback(dependencies: AppProps) {
console.log("Ефект запущено з:", dependencies);
}
// Точний контроль над тим, чи має виконуватись ефект:
function whenPropAChanges(prev: AppProps, next: AppProps) {
return prev.propA !== next.propA;
}
const App: React.FC = ({ propA, propB }) => {
usePureEffect(callback, { propA, propB }, whenPropAChanges);
return
...
; }; export default App; ``` > Ефект виконується лише тоді, коли змінюється propA, навіть якщо оновлюється propB. _Спробуйте usePureEffect у вашому наступному проекті. Це чистий, передбачуваний і комбінований спосіб керування побічними ефектами. Дайте знати, як це працює для вас!_ Насолоджуйтесь!
Перекладено з: [Skipping the effect in React](https://martinadamko.medium.com/skipping-the-effect-in-react-4c9c5038d64c)