Перейти к содержанию

useEffect

useEffect - это хук React, который позволяет вам синхронизировать компонент с внешней системой.

1
useEffect(setup, dependencies?)

Описание

useEffect(setup, dependencies?)

Вызовите useEffect на верхнем уровне вашего компонента, чтобы объявить Эффект:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => {
            connection.disconnect();
        };
    }, [serverUrl, roomId]);
    // ...
}

Параметры

  • setup: Функция с логикой вашего Эффекта. Ваша функция настройки может также по желанию возвращать функцию cleanup. Когда ваш компонент будет добавлен в DOM, React запустит вашу функцию настройки. После каждого повторного рендеринга с измененными зависимостями React будет сначала запускать функцию очистки (если вы ее предоставили) со старыми значениями, а затем запускать вашу функцию настройки с новыми значениями. После удаления вашего компонента из DOM React запустит вашу функцию очистки.
  • опциональные dependencies: Список всех реактивных значений, на которые ссылается код setup. Реактивные значения включают props, state, а также все переменные и функции, объявленные непосредственно в теле вашего компонента. Если ваш линтер настроен на React, он проверит, что каждое реактивное значение правильно указано в качестве зависимости. Список зависимостей должен иметь постоянное количество элементов и быть написан inline по типу [dep1, dep2, dep3]. React будет сравнивать каждую зависимость с предыдущим значением, используя сравнение Object.is. Если вы опустите этот аргумент, ваш Effect будет запускаться заново после каждого повторного рендеринга компонента.

Возврат

useEffect возвращает undefined.

Ограничения

  • useEffect - это хук, поэтому вы можете вызывать его только на верхнем уровне вашего компонента или ваших собственных хуков. Вы не можете вызывать его внутри циклов или условий. Если вам это нужно, создайте новый компонент и переместите состояние в него.

  • Если вы не пытаетесь синхронизироваться с какой-то внешней системой, вам, вероятно, не нужен Эффект.

  • Когда включен строгий режим, React будет проводить один дополнительный цикл настройки+очистки только для разработки перед первой реальной настройкой. Это стресс-тест, который гарантирует, что ваша логика очистки "отражает" вашу логику настройки и что она останавливает или отменяет все, что делает настройка. Если это вызывает проблему, реализуйте функцию очистки.

  • Если некоторые из ваших зависимостей являются объектами или функциями, определенными внутри компонента, есть риск, что они приведут к тому, что Эффект будет перезапускаться чаще, чем нужно. Чтобы исправить это, удалите ненужные зависимости object и function. Вы также можете извлекать обновления состояния и нереактивную логику вне вашего Эффекта.

  • Если ваш Эффект не был вызван взаимодействием (например, щелчком мыши), React позволит браузеру сначала нарисовать обновленный экран, прежде чем запустить ваш Эффект. Если ваш Эффект делает что-то визуальное (например, позиционирует всплывающую подсказку), и задержка заметна (например, она мерцает), замените useEffect на useLayoutEffect.

  • Даже если ваш Эффект был вызван взаимодействием (например, щелчком), браузер может перерисовать экран до обработки обновлений состояния внутри вашего Эффекта. Обычно это то, что вам нужно. Однако, если вы должны запретить браузеру перерисовывать экран, вам нужно заменить useEffect на useLayoutEffect.

  • Эффекты работают только на клиенте. Они не работают во время серверного рендеринга.

Использование

Подключение к внешней системе

Некоторые компоненты должны оставаться подключенными к сети, API браузера или сторонней библиотеке, пока они отображаются на странице. Эти системы не контролируются React, поэтому их называют внешними.

Чтобы подключить ваш компонент к какой-либо внешней системе, вызовите useEffect на верхнем уровне вашего компонента:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => {
            connection.disconnect();
        };
    }, [serverUrl, roomId]);
    // ...
}

Вам необходимо передать два аргумента в useEffect:

  1. Функция setup с установочным кодом, которая подключается к этой системе.
    • Она должна возвращать функцию очистки с кодом очистки, которая отсоединяется от этой системы.
  2. Список зависимостей, включающий каждое значение из вашего компонента, используемое внутри этих функций.

React вызывает ваши функции установки и очистки всякий раз, когда это необходимо, что может происходить несколько раз:.

  1. Ваш код установки выполняется, когда ваш компонент добавляется на страницу (mounts).
  2. После каждого повторного рендеринга вашего компонента, когда зависимости изменились:
    • Сначала ваш очищающий код запускается со старыми пропсами и состоянием.
    • Затем, ваш установочный код запускается с новыми пропсами и состоянием.
  3. Ваш очищающий код запускается в последний раз после того, как ваш компонент удаляется со страницы (размонтируется).

Давайте проиллюстрируем эту последовательность на примере выше.

Когда вышеуказанный компонент ChatRoom добавляется на страницу, он подключается к чату с начальными serverUrl и roomId. Если serverUrl или roomId изменятся в результате повторного рендеринга (например, если пользователь выберет другую комнату в выпадающем списке), ваш Эффект отключится от предыдущей комнаты и подключится к следующей. Когда компонент ChatRoom будет удален со страницы, ваш Эффект отключится в последний раз.

Чтобы помочь вам найти ошибки, при разработке React запускает setup и cleanup один дополнительный раз перед setup. Это стресс-тест, который проверяет правильность реализации логики вашего Эффекта. Если это вызывает видимые проблемы, значит, вашей функции очистки не хватает логики. Функция очистки должна остановить или отменить все, что делала функция настройки. Эмпирическое правило гласит, что пользователь не должен различать между однократным вызовом функции setup (как в продакшене) и последовательностью setupcleanupsetup (как в разработке). См. общие решения.

Попробуйте писать каждый эффект как независимый процесс и думать об одном цикле установки/очистки за раз. Не должно иметь значения, монтируется, обновляется или размонтируется ваш компонент. Когда ваша логика очистки правильно "зеркалит" логику установки, ваш Эффект устойчив к запуску установки и очистки так часто, как это необходимо.

Подключение к внешней системе

Эффект позволяет синхронизировать ваш компонент с какой-либо внешней системой (например, с чат-службой). Здесь под внешней системой подразумевается любой кусок кода, не контролируемый React, например:

Если вы не подключаетесь к какой-либо внешней системе, вам, вероятно, не нужен эффект.

Примеры подключения к внешней системе

1. Подключение к чат-серверу

В этом примере компонент ChatRoom использует эффект, чтобы оставаться подключенным к внешней системе, определенной в chat.js. Нажмите "Открыть чат", чтобы появился компонент ChatRoom. Эта песочница работает в режиме разработки, поэтому существует дополнительный цикл подключения и отключения, как объясняется здесь Попробуйте изменить roomId и erverUrl с помощью выпадающего списка и ввода, и посмотрите, как Эффект снова подключается к чату. Нажмите "Закрыть чат", чтобы увидеть, как Эффект отключится в последний раз.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => {
            connection.disconnect();
        };
    }, [roomId, serverUrl]);

    return (
        <>
            <label>
                Server URL:{' '}
                <input
                    value={serverUrl}
                    onChange={(e) =>
                        setServerUrl(e.target.value)
                    }
                />
            </label>
            <h1>Welcome to the {roomId} room!</h1>
        </>
    );
}

export default function App() {
    const [roomId, setRoomId] = useState('general');
    const [show, setShow] = useState(false);
    return (
        <>
            <label>
                Choose the chat room:{' '}
                <select
                    value={roomId}
                    onChange={(e) =>
                        setRoomId(e.target.value)
                    }
                >
                    <option value="general">general</option>
                    <option value="travel">travel</option>
                    <option value="music">music</option>
                </select>
            </label>
            <button onClick={() => setShow(!show)}>
                {show ? 'Close chat' : 'Open chat'}
            </button>
            {show && <hr />}
            {show && <ChatRoom roomId={roomId} />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection(serverUrl, roomId) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

2. Прослушивание глобального события браузера

В этом примере внешней системой является сам DOM браузера. Обычно вы указываете слушателей событий с помощью JSX, но вы не можете прослушивать глобальный объект window таким образом. Эффект позволяет вам подключиться к объекту window и прослушивать его события. Прослушивание события pointermove позволяет отслеживать положение курсора (или пальца) и обновлять красную точку, чтобы она двигалась вместе с ним.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { useState, useEffect } from 'react';

export default function App() {
    const [position, setPosition] = useState({
        x: 0,
        y: 0,
    });

    useEffect(() => {
        function handleMove(e) {
            setPosition({ x: e.clientX, y: e.clientY });
        }
        window.addEventListener('pointermove', handleMove);
        return () => {
            window.removeEventListener(
                'pointermove',
                handleMove
            );
        };
    }, []);

    return (
        <div
            style={{
                position: 'absolute',
                backgroundColor: 'pink',
                borderRadius: '50%',
                opacity: 0.6,
                transform: `translate(${position.x}px, ${position.y}px)`,
                pointerEvents: 'none',
                left: -20,
                top: -20,
                width: 40,
                height: 40,
            }}
        />
    );
}

3. Запуск анимации

В этом примере внешней системой является библиотека анимации в animation.js. Она предоставляет класс JavaScript под названием FadeInAnimation, который принимает узел DOM в качестве аргумента и раскрывает методы start() и stop() для управления анимацией. Этот компонент использует ссылку для доступа к базовому узлу DOM. Эффект считывает узел DOM из ссылки и автоматически запускает анимацию для этого узла при появлении компонента.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { useState, useEffect, useRef } from 'react';
import { FadeInAnimation } from './animation.js';

function Welcome() {
    const ref = useRef(null);

    useEffect(() => {
        const animation = new FadeInAnimation(ref.current);
        animation.start(1000);
        return () => {
            animation.stop();
        };
    }, []);

    return (
        <h1
            ref={ref}
            style={{
                opacity: 0,
                color: 'white',
                padding: 50,
                textAlign: 'center',
                fontSize: 50,
                backgroundImage:
                    'radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%)',
            }}
        >
            Welcome
        </h1>
    );
}

export default function App() {
    const [show, setShow] = useState(false);
    return (
        <>
            <button onClick={() => setShow(!show)}>
                {show ? 'Remove' : 'Show'}
            </button>
            <hr />
            {show && <Welcome />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
export class FadeInAnimation {
    constructor(node) {
        this.node = node;
    }
    start(duration) {
        this.duration = duration;
        if (this.duration === 0) {
            // Jump to end immediately
            this.onProgress(1);
        } else {
            this.onProgress(0);
            // Start animating
            this.startTime = performance.now();
            this.frameId = requestAnimationFrame(() =>
                this.onFrame()
            );
        }
    }
    onFrame() {
        const timePassed =
            performance.now() - this.startTime;
        const progress = Math.min(
            timePassed / this.duration,
            1
        );
        this.onProgress(progress);
        if (progress < 1) {
            // We still have more frames to paint
            this.frameId = requestAnimationFrame(() =>
                this.onFrame()
            );
        }
    }
    onProgress(progress) {
        this.node.style.opacity = progress;
    }
    stop() {
        cancelAnimationFrame(this.frameId);
        this.startTime = null;
        this.frameId = null;
        this.duration = 0;
    }
}

4. Управление модальным диалогом

В этом примере внешней системой является DOM браузера. Компонент ModalDialog отображает элемент <dialog>. Он использует Effect для синхронизации свойства isOpen с вызовами методов showModal() и close().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useState } from 'react';
import ModalDialog from './ModalDialog.js';

export default function App() {
    const [show, setShow] = useState(false);
    return (
        <>
            <button onClick={() => setShow(true)}>
                Open dialog
            </button>
            <ModalDialog isOpen={show}>
                Hello there!
                <br />
                <button
                    onClick={() => {
                        setShow(false);
                    }}
                >
                    Close
                </button>
            </ModalDialog>
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { useEffect, useRef } from 'react';

export default function ModalDialog({ isOpen, children }) {
    const ref = useRef();

    useEffect(() => {
        if (!isOpen) {
            return;
        }
        const dialog = ref.current;
        dialog.showModal();
        return () => {
            dialog.close();
        };
    }, [isOpen]);

    return <dialog ref={ref}>{children}</dialog>;
}

5. Отслеживание видимости элемента

В этом примере внешней системой снова является DOM браузера. Компонент App отображает длинный список, затем компонент Box, а затем еще один длинный список. Прокрутите список вниз. Обратите внимание, что когда компонент Box появляется в области просмотра, цвет фона меняется на черный. Чтобы реализовать это, компонент Box использует Effect для управления IntersectionObserver. Этот API браузера уведомляет вас, когда элемент DOM становится видимым в области просмотра.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Box from './Box.js';

export default function App() {
    return (
        <>
            <LongSection />
            <Box />
            <LongSection />
            <Box />
            <LongSection />
        </>
    );
}

function LongSection() {
    const items = [];
    for (let i = 0; i < 50; i++) {
        items.push(
            <li key={i}>Item #{i} (keep scrolling)</li>
        );
    }
    return <ul>{items}</ul>;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { useRef, useEffect } from 'react';

export default function Box() {
    const ref = useRef(null);

    useEffect(() => {
        const div = ref.current;
        const observer = new IntersectionObserver(
            (entries) => {
                const entry = entries[0];
                if (entry.isIntersecting) {
                    document.body.style.backgroundColor =
                        'black';
                    document.body.style.color = 'white';
                } else {
                    document.body.style.backgroundColor =
                        'white';
                    document.body.style.color = 'black';
                }
            }
        );
        observer.observe(div, {
            threshold: 1.0,
        });
        return () => {
            observer.disconnect();
        };
    }, []);

    return (
        <div
            ref={ref}
            style={{
                margin: 20,
                height: 100,
                width: 100,
                border: '2px solid black',
                backgroundColor: 'blue',
            }}
        />
    );
}

Обертывание эффектов в пользовательские хуки

Эффекты - это "аварийный люк": Вы используете их, когда вам нужно "выйти за пределы React" и когда нет лучшего встроенного решения для вашего случая использования. Если вы часто сталкиваетесь с необходимостью вручную писать Effects, это обычно признак того, что вам нужно извлечь некоторые пользовательские хуки для общего поведения, на которое полагаются ваши компоненты.

Например, этот пользовательский хук useChatRoom "прячет" логику вашего Эффекта за более декларативным API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function useChatRoom({ serverUrl, roomId }) {
    useEffect(() => {
        const options = {
            serverUrl: serverUrl,
            roomId: roomId,
        };
        const connection = createConnection(options);
        connection.connect();
        return () => connection.disconnect();
    }, [roomId, serverUrl]);
}

Затем вы можете использовать его из любого компонента, как это сделано здесь:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );

    useChatRoom({
        roomId: roomId,
        serverUrl: serverUrl,
    });
    // ...
}

В экосистеме React также существует множество отличных пользовательских хуков для любых целей.

Подробнее об обертывании эффектов в пользовательские хуки

Примеры обертывания эффектов в пользовательские хуки

1. Пользовательский хук useChatRoom

Этот пример идентичен одному из предыдущих примеров, но логика вынесена в пользовательский хук.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );

    useChatRoom({
        roomId: roomId,
        serverUrl: serverUrl,
    });

    return (
        <>
            <label>
                Server URL:{' '}
                <input
                    value={serverUrl}
                    onChange={(e) =>
                        setServerUrl(e.target.value)
                    }
                />
            </label>
            <h1>Welcome to the {roomId} room!</h1>
        </>
    );
}

export default function App() {
    const [roomId, setRoomId] = useState('general');
    const [show, setShow] = useState(false);
    return (
        <>
            <label>
                Choose the chat room:{' '}
                <select
                    value={roomId}
                    onChange={(e) =>
                        setRoomId(e.target.value)
                    }
                >
                    <option value="general">general</option>
                    <option value="travel">travel</option>
                    <option value="music">music</option>
                </select>
            </label>
            <button onClick={() => setShow(!show)}>
                {show ? 'Close chat' : 'Open chat'}
            </button>
            {show && <hr />}
            {show && <ChatRoom roomId={roomId} />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { useEffect } from 'react';
import { createConnection } from './chat.js';

export function useChatRoom({ serverUrl, roomId }) {
    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => {
            connection.disconnect();
        };
    }, [roomId, serverUrl]);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection(serverUrl, roomId) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

2. Пользовательский хук useWindowListener

Этот пример идентичен одному из предыдущих примеров, но логика вынесена в пользовательский хук.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useState } from 'react';
import { useWindowListener } from './useWindowListener.js';

export default function App() {
    const [position, setPosition] = useState({
        x: 0,
        y: 0,
    });

    useWindowListener('pointermove', (e) => {
        setPosition({ x: e.clientX, y: e.clientY });
    });

    return (
        <div
            style={{
                position: 'absolute',
                backgroundColor: 'pink',
                borderRadius: '50%',
                opacity: 0.6,
                transform: `translate(${position.x}px, ${position.y}px)`,
                pointerEvents: 'none',
                left: -20,
                top: -20,
                width: 40,
                height: 40,
            }}
        />
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { useState, useEffect } from 'react';

export function useWindowListener(eventType, listener) {
    useEffect(() => {
        window.addEventListener(eventType, listener);
        return () => {
            window.removeEventListener(eventType, listener);
        };
    }, [eventType, listener]);
}

3. Пользовательский хук useIntersectionObserver

Этот пример идентичен одному из предыдущих примеров, но логика частично вынесена в пользовательский хук.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Box from './Box.js';

export default function App() {
    return (
        <>
            <LongSection />
            <Box />
            <LongSection />
            <Box />
            <LongSection />
        </>
    );
}

function LongSection() {
    const items = [];
    for (let i = 0; i < 50; i++) {
        items.push(
            <li key={i}>Item #{i} (keep scrolling)</li>
        );
    }
    return <ul>{items}</ul>;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useRef, useEffect } from 'react';
import { useIntersectionObserver } from './useIntersectionObserver.js';

export default function Box() {
    const ref = useRef(null);
    const isIntersecting = useIntersectionObserver(ref);

    useEffect(() => {
        if (isIntersecting) {
            document.body.style.backgroundColor = 'black';
            document.body.style.color = 'white';
        } else {
            document.body.style.backgroundColor = 'white';
            document.body.style.color = 'black';
        }
    }, [isIntersecting]);

    return (
        <div
            ref={ref}
            style={{
                margin: 20,
                height: 100,
                width: 100,
                border: '2px solid black',
                backgroundColor: 'blue',
            }}
        />
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState, useEffect } from 'react';

export function useIntersectionObserver(ref) {
    const [isIntersecting, setIsIntersecting] = useState(
        false
    );

    useEffect(() => {
        const div = ref.current;
        const observer = new IntersectionObserver(
            (entries) => {
                const entry = entries[0];
                setIsIntersecting(entry.isIntersecting);
            }
        );
        observer.observe(div, {
            threshold: 1.0,
        });
        return () => {
            observer.disconnect();
        };
    }, [ref]);

    return isIntersecting;
}

Управление виджетом без React

Иногда вы хотите, чтобы внешняя система синхронизировалась с каким-либо параметром или состоянием вашего компонента.

Например, если у вас есть сторонний виджет карты или компонент видеоплеера, написанный без React, вы можете использовать эффект для вызова методов, которые заставят его состояние соответствовать текущему состоянию вашего компонента React. Этот Эффект создает экземпляр класса MapWidget, определенного в map-widget.js. Когда вы изменяете параметр zoomLevel компонента Map, Эффект вызывает setZoom() для экземпляра класса, чтобы сохранить его синхронизацию:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useState } from 'react';
import Map from './Map.js';

export default function App() {
    const [zoomLevel, setZoomLevel] = useState(0);
    return (
        <>
            Zoom level: {zoomLevel}x
            <button
                onClick={() => setZoomLevel(zoomLevel + 1)}
            >
                +
            </button>
            <button
                onClick={() => setZoomLevel(zoomLevel - 1)}
            >
                -
            </button>
            <hr />
            <Map zoomLevel={zoomLevel} />
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';

export default function Map({ zoomLevel }) {
    const containerRef = useRef(null);
    const mapRef = useRef(null);

    useEffect(() => {
        if (mapRef.current === null) {
            mapRef.current = new MapWidget(
                containerRef.current
            );
        }

        const map = mapRef.current;
        map.setZoom(zoomLevel);
    }, [zoomLevel]);

    return (
        <div
            style={{ width: 200, height: 200 }}
            ref={containerRef}
        />
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'leaflet/dist/leaflet.css';
import * as L from 'leaflet';

export class MapWidget {
    constructor(domNode) {
        this.map = L.map(domNode, {
            zoomControl: false,
            doubleClickZoom: false,
            boxZoom: false,
            keyboard: false,
            scrollWheelZoom: false,
            zoomAnimation: false,
            touchZoom: false,
            zoomSnap: 0.1,
        });
        L.tileLayer(
            'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
            {
                maxZoom: 19,
                attribution: '© OpenStreetMap',
            }
        ).addTo(this.map);
        this.map.setView([0, 0], 0);
    }
    setZoom(level) {
        this.map.setZoom(level);
    }
}

В данном примере функция очистки не нужна, поскольку класс MapWidget управляет только узлом DOM, который был ему передан. После удаления React-компонента Map из дерева, и DOM-узел, и экземпляр класса MapWidget будут автоматически очищены от мусора JavaScript-движком браузера.

Получение данных с помощью эффектов

Вы можете использовать Эффект для получения данных для вашего компонента. Обратите внимание, что если вы используете фреймворк, использование механизма получения данных вашего фреймворка будет намного эффективнее, чем написание эффектов вручную.

Если вы хотите получить данные из Эффекта вручную, ваш код может выглядеть следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
    const [person, setPerson] = useState('Alice');
    const [bio, setBio] = useState(null);

    useEffect(() => {
        let ignore = false;
        setBio(null);
        fetchBio(person).then((result) => {
            if (!ignore) {
                setBio(result);
            }
        });
        return () => {
            ignore = true;
        };
    }, [person]);
    // ...
}

Обратите внимание на переменную ignore, которая инициализируется в false и устанавливается в true во время очистки. Это гарантирует, что ваш код не пострадает от "условий гонки": ответы сети могут приходить не в том порядке, в котором вы их отправили.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
    const [person, setPerson] = useState('Alice');
    const [bio, setBio] = useState(null);
    useEffect(() => {
        let ignore = false;
        setBio(null);
        fetchBio(person).then((result) => {
            if (!ignore) {
                setBio(result);
            }
        });
        return () => {
            ignore = true;
        };
    }, [person]);

    return (
        <>
            <select
                value={person}
                onChange={(e) => {
                    setPerson(e.target.value);
                }}
            >
                <option value="Alice">Alice</option>
                <option value="Bob">Bob</option>
                <option value="Taylor">Taylor</option>
            </select>
            <hr />
            <p>
                <i>{bio ?? 'Loading...'}</i>
            </p>
        </>
    );
}

Вы также можете переписать, используя синтаксис async / await, но вам все равно придется предоставить функцию очистки:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
    const [person, setPerson] = useState('Alice');
    const [bio, setBio] = useState(null);
    useEffect(() => {
        async function startFetching() {
            setBio(null);
            const result = await fetchBio(person);
            if (!ignore) {
                setBio(result);
            }
        }

        let ignore = false;
        startFetching();
        return () => {
            ignore = true;
        };
    }, [person]);

    return (
        <>
            <select
                value={person}
                onChange={(e) => {
                    setPerson(e.target.value);
                }}
            >
                <option value="Alice">Alice</option>
                <option value="Bob">Bob</option>
                <option value="Taylor">Taylor</option>
            </select>
            <hr />
            <p>
                <i>{bio ?? 'Loading...'}</i>
            </p>
        </>
    );
}

Запись получения данных непосредственно в Effects становится повторяющейся и затрудняет добавление таких оптимизаций, как кэширование и серверный рендеринг. Проще использовать пользовательский хук - либо свой собственный, либо поддерживаемый сообществом.

Какие есть хорошие альтернативы для получения данных в Effects?

Написание вызовов fetch внутри Effects является популярным способом получения данных, особенно в полностью клиентских приложениях. Однако это очень ручной подход, и у него есть существенные недостатки:

  • Эффекты не запускаются на сервере. Это означает, что первоначальный HTML, отрисованный на сервере, будет содержать только состояние загрузки без каких-либо данных. Клиентский компьютер должен будет загрузить весь JavaScript и отобразить ваше приложение только для того, чтобы обнаружить, что теперь ему нужно загрузить данные. Это не очень эффективно.
  • Получение данных непосредственно в Effects позволяет легко создавать "сетевые водопады". Вы рендерите родительский компонент, он получает некоторые данные, рендерит дочерние компоненты, а затем они начинают получать свои данные. Если сеть не очень быстрая, это значительно медленнее, чем параллельная выборка всех данных.
  • Например, если компонент размонтируется, а затем снова монтируется, ему придется снова получать данные.
  • Это не очень эргономично. Существует довольно много кода, связанного с написанием вызовов fetch таким образом, чтобы не страдать от ошибок типа race conditions.

Этот список недостатков не является специфическим для React. Он применим к выборке данных при подключении с помощью любой библиотеки. Как и в случае с маршрутизацией, выборка данных не является тривиальной задачей, поэтому мы рекомендуем следующие подходы:

  • Если вы используете фреймворк, используйте его встроенный механизм выборки данных. Современные фреймворки React имеют встроенные механизмы выборки данных, которые эффективны и не страдают от описанных выше подводных камней.
  • В противном случае, рассмотрите возможность использования или создания кэша на стороне клиента. Популярные решения с открытым исходным кодом включают React Query, useSWR и React Router 6.4+. Вы также можете создать собственное решение, в этом случае вы будете использовать Effects под капотом, но также добавите логику для дедупликации запросов, кэширования ответов и избежания сетевых водопадов (путем предварительной загрузки данных или поднятия требований к данным в маршрутах).

Вы можете продолжать получать данные непосредственно в Effects, если ни один из этих подходов вам не подходит.

Указание реактивных зависимостей

Обратите внимание, что вы не можете "выбрать" зависимости вашего Эффекта. Каждый реактивное значение, используемое вашим Эффектом, может быть использован в качестве зависимого.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function ChatRoom({ roomId }) {
    // This is a reactive value
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    ); // This is a reactive value too

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        ); // This Effect reads these reactive values
        connection.connect();
        return () => connection.disconnect();
    }, [serverUrl, roomId]);
    // ✅ So you must specify them as dependencies of your Effect
    // ...
}

Если serverUrl или roomId изменятся, ваш Эффект переподключится к чату, используя новые значения.

Реактивные значения включают пропсы и все переменные и функции, объявленные непосредственно внутри вашего компонента. Поскольку roomId и erverUrl являются реактивными значениями, вы не можете удалить их из зависимостей. Если вы попытаетесь опустить их и ваш линтер правильно настроен для React, линтер отметит это как ошибку, которую нужно исправить:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => connection.disconnect();
    }, []); // 🔴 React Hook useEffect has missing dependencies:
    //           'roomId' and 'serverUrl'
    // ...
}

Чтобы удалить зависимость, вам нужно "доказать" линтеру, что она не должна быть зависимостью. Например, вы можете убрать serverUrl из вашего компонента, чтобы доказать, что он не реактивный и не будет меняться при повторных рендерах:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Not a reactive value anymore
const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => connection.disconnect();
    }, [roomId]); // ✅ All dependencies declared
    // ...
}

Теперь, когда serverUrl не является реактивным значением (и не может меняться при повторном рендере), ему не нужно быть зависимостью. Если код вашего Эффекта не использует никаких реактивных значений, его список зависимостей должен быть пустым ([]):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore

function ChatRoom() {
    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => connection.disconnect();
    }, []); // ✅ All dependencies declared
    // ...
}

Эффект с пустыми зависимостями не перезапускается при изменении пропсов или состояния вашего компонента.

Подавление линтера

Если у вас есть существующая кодовая база, у вас могут быть некоторые Эффекты, которые подавляют линтер подобным образом:

1
2
3
4
5
useEffect(() => {
    // ...
    // 🔴 Avoid suppressing the linter like this:
    // eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

Когда зависимости не соответствуют коду, существует высокий риск появления ошибок. Подавляя линтер, вы "лжете" React о значениях, от которых зависит ваш Эффект. Вместо этого докажите, что они не нужны.

Примеры передачи реактивных зависимостей

1. Передача массива зависимостей

Если вы укажете зависимости, ваш Эффект запустится после первоначального рендера и после повторных рендеров с измененными зависимостями.

1
2
3
useEffect(() => {
    // ...
}, [a, b]); // Runs again if a or b are different

В приведенном ниже примере serverUrl и roomId являются реактивными значениями, поэтому они оба должны быть указаны как зависимости. В результате выбор другой комнаты в выпадающем списке или редактирование URL сервера приводит к повторному подключению чата. Однако, поскольку message не используется в эффекте (и поэтому не является зависимостью), редактирование сообщения не приводит к повторному подключению к чату.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );
    const [message, setMessage] = useState('');

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => {
            connection.disconnect();
        };
    }, [serverUrl, roomId]);

    return (
        <>
            <label>
                Server URL:{' '}
                <input
                    value={serverUrl}
                    onChange={(e) =>
                        setServerUrl(e.target.value)
                    }
                />
            </label>
            <h1>Welcome to the {roomId} room!</h1>
            <label>
                Your message:{' '}
                <input
                    value={message}
                    onChange={(e) =>
                        setMessage(e.target.value)
                    }
                />
            </label>
        </>
    );
}

export default function App() {
    const [show, setShow] = useState(false);
    const [roomId, setRoomId] = useState('general');
    return (
        <>
            <label>
                Choose the chat room:{' '}
                <select
                    value={roomId}
                    onChange={(e) =>
                        setRoomId(e.target.value)
                    }
                >
                    <option value="general">general</option>
                    <option value="travel">travel</option>
                    <option value="music">music</option>
                </select>
                <button onClick={() => setShow(!show)}>{show ? 'Close chat' : 'Open chat'}</button>
            </label>
            {show && <hr />}
            {show && <ChatRoom roomId={roomId} />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection(serverUrl, roomId) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

2. Передача пустого массива зависимостей

Если ваш Эффект действительно не использует никаких реактивных значений, он будет запущен только после начального рендеринга.

1
2
3
useEffect(() => {
    // ...
}, []); // Does not run again (except once in development)

Даже при пустых зависимостях, setup и cleanup будут выполняться один дополнительный раз в разработке, чтобы помочь вам найти ошибки.

В этом примере и serverUrl и roomId жестко закодированы. Поскольку они объявлены вне компонента, они не являются реактивными значениями, а значит, не являются зависимостями. Список зависимостей пуст, поэтому Effect не запускается при повторном рендере.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';
const roomId = 'music';

function ChatRoom() {
    const [message, setMessage] = useState('');

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => connection.disconnect();
    }, []);

    return (
        <>
            <h1>Welcome to the {roomId} room!</h1>
            <label>
                Your message:{' '}
                <input
                    value={message}
                    onChange={(e) =>
                        setMessage(e.target.value)
                    }
                />
            </label>
        </>
    );
}

export default function App() {
    const [show, setShow] = useState(false);
    return (
        <>
            <button onClick={() => setShow(!show)}>
                {show ? 'Close chat' : 'Open chat'}
            </button>
            {show && <hr />}
            {show && <ChatRoom />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection(serverUrl, roomId) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

3. Непередача массива зависимостей вообще

Если вы не передаете массив зависимостей вообще, ваш Effect запускается после каждого рендера (и повторного рендера) вашего компонента.

1
2
3
useEffect(() => {
    // ...
}); // Always runs again

В этом примере Effect повторно запускается при изменении serverUrl и roomId, что вполне разумно. Однако он также запускается повторно, когда вы изменяете message, что, вероятно, нежелательно. Вот почему обычно указывается массив зависимостей.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );
    const [message, setMessage] = useState('');

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => {
            connection.disconnect();
        };
    }); // No dependency array at all

    return (
        <>
            <label>
                Server URL:{' '}
                <input
                    value={serverUrl}
                    onChange={(e) =>
                        setServerUrl(e.target.value)
                    }
                />
            </label>
            <h1>Welcome to the {roomId} room!</h1>
            <label>
                Your message:{' '}
                <input
                    value={message}
                    onChange={(e) =>
                        setMessage(e.target.value)
                    }
                />
            </label>
        </>
    );
}

export default function App() {
    const [show, setShow] = useState(false);
    const [roomId, setRoomId] = useState('general');
    return (
        <>
            <label>
                Choose the chat room:{' '}
                <select
                    value={roomId}
                    onChange={(e) =>
                        setRoomId(e.target.value)
                    }
                >
                    <option value="general">general</option>
                    <option value="travel">travel</option>
                    <option value="music">music</option>
                </select>
                <button onClick={() => setShow(!show)}>{show ? 'Close chat' : 'Open chat'}</button>
            </label>
            {show && <hr />}
            {show && <ChatRoom roomId={roomId} />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection(serverUrl, roomId) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

Обновление состояния на основе предыдущего состояния от Эффекта

Когда вы хотите обновить состояние на основе предыдущего состояния Эффекта, вы можете столкнуться с проблемой:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function Counter() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const intervalId = setInterval(() => {
            setCount(count + 1); // You want to increment the counter every second...
        }, 1000);
        return () => clearInterval(intervalId);
    }, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
    // ...
}

Поскольку count является реактивным значением, оно должно быть указано в списке зависимостей. Однако это заставляет Effect очищать и устанавливать заново каждый раз, когда изменяется count. Это не идеально.

Чтобы исправить это, передайте функцию обновления состояния c => c + 1 в setCount:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { useState, useEffect } from 'react';

export default function Counter() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const intervalId = setInterval(() => {
            setCount((c) => c + 1); // ✅ Pass a state updater
        }, 1000);
        return () => clearInterval(intervalId);
    }, []); // ✅ Now count is not a dependency

    return <h1>{count}</h1>;
}

Теперь, когда вы передаете c => c + 1 вместо count + 1, вашему Эффекту больше не нужно зависеть от count. В результате этого исправления, ему не нужно будет очищать и устанавливать интервал заново каждый раз, когда count меняется.

Удаление ненужных объектных зависимостей

Если ваш Эффект зависит от объекта или функции, созданной во время рендеринга, он может запускаться слишком часто. Например, этот Эффект переподключается после каждого рендеринга, потому что объект options разный для каждого рендеринга:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
    const [message, setMessage] = useState('');

    const options = {
        // 🚩 This object is created from scratch on every re-render
        serverUrl: serverUrl,
        roomId: roomId,
    };

    useEffect(() => {
        const connection = createConnection(options); // It's used inside the Effect
        connection.connect();
        return () => connection.disconnect();
    }, [options]); // 🚩 As a result, these dependencies are always different on a re-render
    // ...
}

Избегайте использования объекта, созданного во время рендеринга, в качестве зависимого. Вместо этого создайте объект внутри Effect:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
    const [message, setMessage] = useState('');

    useEffect(() => {
        const options = {
            serverUrl: serverUrl,
            roomId: roomId,
        };
        const connection = createConnection(options);
        connection.connect();
        return () => connection.disconnect();
    }, [roomId]);

    return (
        <>
            <h1>Welcome to the {roomId} room!</h1>
            <input
                value={message}
                onChange={(e) => setMessage(e.target.value)}
            />
        </>
    );
}

export default function App() {
    const [roomId, setRoomId] = useState('general');
    return (
        <>
            <label>
                Choose the chat room:{' '}
                <select
                    value={roomId}
                    onChange={(e) =>
                        setRoomId(e.target.value)
                    }
                >
                    <option value="general">general</option>
                    <option value="travel">travel</option>
                    <option value="music">music</option>
                </select>
            </label>
            <hr />
            <ChatRoom roomId={roomId} />
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection({ serverUrl, roomId }) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

Теперь, когда вы создаете объект options внутри эффекта, сам эффект зависит только от строки roomId.

С этим исправлением ввод ввода не переподключает чат. В отличие от объекта, который создается заново, строка типа roomId не меняется, пока вы не зададите ей другое значение. Подробнее об удалении зависимостей .

Удаление ненужных зависимостей от функций

Если ваш Эффект зависит от объекта или функции, созданной во время рендеринга, он может запускаться слишком часто. Например, этот Эффект переподключается после каждого рендера, потому что функция createOptions разная для каждого рендера:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function ChatRoom({ roomId }) {
    const [message, setMessage] = useState('');

    function createOptions() {
        // 🚩 This function is created from scratch on every re-render
        return {
            serverUrl: serverUrl,
            roomId: roomId,
        };
    }

    useEffect(() => {
        const options = createOptions(); // It's used inside the Effect
        const connection = createConnection();
        connection.connect();
        return () => connection.disconnect();
    }, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
    // ...
}

Само по себе создание функции с нуля при каждом повторном рендере не является проблемой. Вам не нужно ее оптимизировать. Однако если вы используете ее в качестве зависимости от вашего Эффекта, это приведет к тому, что ваш Эффект будет запускаться заново после каждого повторного рендеринга.

Избегайте использования функции, созданной во время рендеринга, в качестве зависимости. Вместо этого объявите ее внутри Эффекта:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
    const [message, setMessage] = useState('');

    useEffect(() => {
        function createOptions() {
            return {
                serverUrl: serverUrl,
                roomId: roomId,
            };
        }

        const options = createOptions();
        const connection = createConnection(options);
        connection.connect();
        return () => connection.disconnect();
    }, [roomId]);

    return (
        <>
            <h1>Welcome to the {roomId} room!</h1>
            <input
                value={message}
                onChange={(e) => setMessage(e.target.value)}
            />
        </>
    );
}

export default function App() {
    const [roomId, setRoomId] = useState('general');
    return (
        <>
            <label>
                Choose the chat room:{' '}
                <select
                    value={roomId}
                    onChange={(e) =>
                        setRoomId(e.target.value)
                    }
                >
                    <option value="general">general</option>
                    <option value="travel">travel</option>
                    <option value="music">music</option>
                </select>
            </label>
            <hr />
            <ChatRoom roomId={roomId} />
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection({ serverUrl, roomId }) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

Теперь, когда вы определяете функцию createOptions внутри эффекта, сам эффект зависит только от строки roomId. С этим исправлением ввод в input не переподключает чат. В отличие от функции, которая создается заново, такая строка, как roomId, не меняется, пока вы не установите для нее другое значение. Подробнее об удалении зависимостей .

Чтение последних пропсов и состояния из эффекта

Экспериментальный API

Этот раздел описывает экспериментальный API, который еще не был выпущен в стабильной версии React.

По умолчанию, когда вы читаете реактивное значение из Эффекта, вы должны добавить его как зависимость. Это гарантирует, что ваш Эффект "реагирует" на каждое изменение этого значения. Для большинства зависимостей это именно то поведение, которое вам нужно.

Однако иногда вы хотите читать последние пропсы и состояния из Эффекта без "реакции" на них. Например, представьте, что вы хотите регистрировать количество товаров в корзине при каждом посещении страницы:

1
2
3
4
5
6
function Page({ url, shoppingCart }) {
    useEffect(() => {
        logVisit(url, shoppingCart.length);
    }, [url, shoppingCart]); // ✅ All dependencies declared
    // ...
}

Что если вы хотите регистрировать посещение новой страницы после каждого изменения url, но не, если меняется только shoppingCart? Вы не можете исключить shoppingCart из зависимостей, не нарушая правила реактивности. Однако вы можете выразить, что вы не хотите, чтобы часть кода "реагировала" на изменения, даже если она вызывается из Эффекта. Объявите событие Эффекта с помощью хука useEffectEvent и переместите код, читающий shoppingCart, внутрь него:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function Page({ url, shoppingCart }) {
    const onVisit = useEffectEvent((visitedUrl) => {
        logVisit(visitedUrl, shoppingCart.length);
    });

    useEffect(() => {
        onVisit(url);
    }, [url]); // ✅ All dependencies declared
    // ...
}

События Эффекта не являются реактивными и всегда должны быть исключены из зависимостей вашего Эффекта. Именно это позволяет вам поместить нереактивный код (где вы можете прочитать последнее значение некоторых пропсов и состояния) внутрь них. Читая shoppingCart внутри onVisit, вы гарантируете, что shoppingCart не будет повторно запускать ваш Эффект.

Подробнее о том, как события Effect Events позволяют разделить реактивный и нереактивный код .

Отображение разного содержимого на сервере и клиенте

Если ваше приложение использует серверный рендеринг (либо напрямую, либо через фреймворк), ваш компонент будет отображаться в двух разных средах. На сервере он будет рендериться для создания исходного HTML. На клиенте React снова запустит код рендеринга, чтобы прикрепить обработчики событий к этому HTML. Вот почему, чтобы hydration работал, ваш первоначальный вывод рендеринга должен быть идентичным на клиенте и на сервере.

В редких случаях вам может понадобиться отобразить разное содержимое на клиенте. Например, если ваше приложение считывает некоторые данные из localStorage, оно не может сделать это на сервере. Вот как это можно реализовать:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function MyComponent() {
    const [didMount, setDidMount] = useState(false);

    useEffect(() => {
        setDidMount(true);
    }, []);

    if (didMount) {
        // ... return client-only JSX ...
    } else {
        // ... return initial JSX ...
    }
}

Пока приложение загружается, пользователь будет видеть начальный вывод рендера. Затем, после загрузки и гидратации, ваш Effect запустится и установит didMount в true, вызывая повторный рендеринг. При этом произойдет переключение на вывод рендера только для клиента. Эффекты не запускаются на сервере, поэтому didMount было false во время первоначального серверного рендеринга.

Используйте этот паттерн экономно. Помните, что пользователи с медленным соединением будут видеть начальное содержимое довольно долго - потенциально много секунд - поэтому вы не захотите вносить резкие изменения во внешний вид вашего компонента. Во многих случаях вы можете избежать этого, условно показывая различные вещи с помощью CSS.

Устранение неполадок

Мой эффект запускается дважды, когда компонент монтируется

Когда включен строгий режим, в процессе разработки React запускает установку и очистку один дополнительный раз перед фактической установкой.

Это стресс-тест, который проверяет правильность реализации логики вашего Эффекта. Если это вызывает видимые проблемы, значит, вашей функции очистки не хватает логики. Функция очистки должна остановить или отменить все, что делала функция настройки. Эмпирическое правило гласит, что пользователь не должен различать между однократным вызовом функции setup (как в производстве) и последовательностью setup → cleanup → setup (как в разработке).

Подробнее о как это помогает найти ошибки и как исправить логику

Мой эффект запускается после каждого рендера

Во-первых, проверьте, не забыли ли вы указать массив зависимостей:

1
2
3
useEffect(() => {
    // ...
}); // 🚩 No dependency array: re-runs after every render!

Если вы указали массив зависимостей, но ваш Effect все равно перезапускается в цикле, это связано с тем, что одна из ваших зависимостей отличается при каждом повторном рендере.

Вы можете отладить эту проблему, вручную записав журнал зависимостей в консоль:

1
2
3
4
5
useEffect(() => {
    // ..
}, [serverUrl, roomId]);

console.log([serverUrl, roomId]);

Затем вы можете щелкнуть правой кнопкой мыши на массивах из разных рендеров в консоли и выбрать "Store as a global variable" для обоих. Предположив, что первый массив был сохранен как temp1, а второй - как temp2, вы можете использовать консоль браузера, чтобы проверить, является ли каждая зависимость в обоих массивах одинаковой:

1
2
3
4
5
6
7
8
// Is the first dependency the same between the arrays?
Object.is(temp1[0], temp2[0]);

// Is the second dependency the same between the arrays?
Object.is(temp1[1], temp2[1]);

// ... and so on for every dependency ...
Object.is(temp1[2], temp2[2]);

Когда вы обнаружите зависимость, которая отличается при каждом повторном рендере, вы обычно можете исправить ее одним из этих способов:

В крайнем случае (если эти методы не помогли), оберните его создание с помощью useMemo или useCallback (для функций).

Мой Эффект повторяется в бесконечном цикле

Если ваш Эффект запускается в бесконечном цикле, эти две вещи должны быть верны:

  • Ваш Эффект обновляет некоторое состояние.
  • Это состояние приводит к повторному рендерингу, что вызывает изменение зависимостей Эффекта.

Прежде чем приступить к устранению проблемы, спросите себя, подключается ли ваш Эффект к какой-либо внешней системе (например, DOM, сеть, сторонний виджет и так далее). Зачем вашему Эффекту нужно устанавливать состояние? Синхронизируется ли он с этой внешней системой? Или вы пытаетесь управлять потоком данных вашего приложения вместе с ней?

Если внешней системы нет, подумайте, не упростит ли вашу логику полное удаление эффекта.

Если вы действительно синхронизируетесь с какой-то внешней системой, подумайте, почему и при каких условиях ваш Эффект должен обновлять состояние. Изменилось ли что-то, что влияет на визуальный вывод вашего компонента? Если вам нужно отслеживать какие-то данные, которые не используются при визуализации, то более подходящим вариантом может быть ref (который не вызывает повторного рендеринга). Убедитесь, что ваш эффект не обновляет состояние (и не запускает повторные рендеринги) чаще, чем это необходимо.

Наконец, если ваш Эффект обновляет состояние в нужное время, но цикл все еще существует, это связано с тем, что обновление состояния приводит к изменению одной из зависимостей Эффекта.

Моя логика очистки выполняется, даже если компонент не был размонтирован

Функция очистки запускается не только во время размонтирования, но и перед каждым повторным рендерингом с измененными зависимостями. Кроме того, в процессе разработки React запускает setup+cleanup один дополнительный раз сразу после монтирования компонента.

Если у вас есть код очистки без соответствующего кода установки, это обычно запах кода:

1
2
3
4
5
6
useEffect(() => {
    // 🔴 Avoid: Cleanup logic without corresponding setup logic
    return () => {
        doSomething();
    };
}, []);

Ваша логика очистки должна быть "симметрична" логике настройки и должна останавливать или отменять все, что сделала настройка:

1
2
3
4
5
6
7
useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
        connection.disconnect();
    };
}, [serverUrl, roomId]);

Узнайте, чем жизненный цикл эффекта отличается от жизненного цикла компонента .

Мой эффект делает что-то визуальное, и я вижу мерцание перед его запуском

Если ваш Эффект должен блокировать браузер от рисования экрана, замените useEffect на useLayoutEffect. Обратите внимание, что это не нужно для подавляющего большинства Эффектов. Это понадобится только в том случае, если очень важно запустить ваш Эффект до рисования браузера: например, для измерения и позиционирования всплывающей подсказки до того, как пользователь ее увидит.

Источник — https://react.dev/reference/react/useEffect

Комментарии