Лучшие практики React Hooks

React.js
Лучшие практики React Hooks

написать впереди

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

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

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

функциональные компоненты

Короче говоря, верните элемент React в функцию.

const App = (props) => {
    const { title } = props;
    return (
        <h1>{title}</h1>
    );  
};

Обычно эта функция принимает один параметр: объект props. Из этого объекта мы можем считывать данные, генерировать новые данные посредством вычислений и, наконец, возвращать элементы React в React для рендеринга. Существует также возможность выполнять побочные эффекты в функции.

В этой статье мы даем функции функционального компонента более простое имя: функция рендеринга.

const appElement = App({ title: "XXX" });
ReactDOM.render(
    appElement,
    document.getElementById('app')
);

В приведенном выше коде мы сами вызываем функцию рендеринга для выполнения рендеринга. Однако это не нормальная работа в React.

Нормальная работа — это код, подобный следующему:

// React.createElement(App, {
//     title: "XXX"
// });
const appElement = <App title="XXX" />;
ReactDOM.render(
    appElement,
    document.getElementById('app')
);

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

Этот механизм передачи функции рендеринга в React для внутренней обработки позволяет вводить состояние.

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

Каждый кадр имеет независимые переменные

Прежде чем вводить состояние, нам нужно понять это.

мы проходимПример 1Сделайте наблюдение:

Edit 1. 每一帧拥有独立的变量

function Example(props) {
    const { count } = props;
    const handleClick = () => {
        setTimeout(() => {
            alert(count);
        }, 3000);
    };
    return (
        <div>
            <p>{count}</p>
            <button onClick={handleClick}>Alert Count</button>
        </div>
    );
}

Фокус<Example>Код функционального компонента, гдеcountСвойство передается родительским компонентом, начальное значение равно 0 и увеличивается на 1 каждую секунду. Нажатие кнопки «Счетчик предупреждений» задержит всплывающее окно на 3 секунды.countценность . После операции обнаруживается, что значение, отображаемое во всплывающем окне, отличается от значения, отображаемого в тексте на странице, но равно значению при нажатии кнопки «Количество предупреждений».countценность .

Если заменить компонент класса, его реализация<Example2>Такой:

class Example2 extends Component {
    handleClick = () => {
        setTimeout(() => {
            alert(this.props.count);
        }, 3000);
    };

    render() {
        return (
            <div>
                <h2>Example2</h2>
                <p>{this.props.count}</p>
                <button onClick={this.handleClick}>Alert Count</button>
            </div>
        );
    }
}

В этот момент нажмите кнопку «Счетчик предупреждений», и появится задержка на 3 секунды.countЗначение совпадает со значением, отображаемым текстом на странице.

В некоторых случаях,<Example>Поведение функционального компонента соответствует ожидаемому. еслиsetTimeoutПо аналогии с запросом на выборку, когда запрос успешен, я хочу получить соответствующие данные до того, как запрос на выборку будет инициирован, и изменить его.

Как понять разницу?

существует<Example2>компонент класса, мы изthisполучен изprops.count.thisФиксировано, чтобы указывать на один и тот же экземпляр компонента. После того, как вступает в силу 3-секундная задержка, компонент перерисовывается,this.propsтакже изменился. Когда функция отложенного обратного вызова выполняется, чтениеthis.propsявляется последним значением свойства текущего компонента.

пока в<Example>В функциональном компоненте каждый раз, когда выполняется функция рендеринга,propsПередается в качестве аргумента функции, это переменная в области видимости функции.

когда<Example>Компонент создан и запустит такой код для завершения первого кадра:

const props_0 = { count: 0 };

const handleClick_0 = () => {
    setTimeout(() => {
        alert(props_0.count);
    }, 3000);
};
return (
    <div>
        <h2>Example</h2>
        <p>{props_0.count}</p>
        <button onClick={handleClick_0}>alert Count</button>
    </div>
);

Когда счетчик, переданный родительским компонентом, становится равным 1, React снова вызываетExampleфункция, выполнить второй кадр, в это времяcountда1.

const props_1 = { count: 1 };

const handleClick_1 = () => {
    setTimeout(() => {
        alert(props_1.count);
    }, 3000);
};
return (
    <div>
        <h2>Example</h2>
        <p>{props_1.count}</p>
        <button onClick={handleClick_1}>alert Count</button>
    </div>
);

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

Другими словами, для любогоprops, значение которого определяется в момент объявления и не меняется с течением времени.handleClickТо же верно и для функций. Например, функция обратного вызова таймера происходит в будущем, ноprops.countЗначение объявлено вhandleClickФункция уже определена.

Если мы используем деструктурирующее присваивание в начале функции,const { count } = props, затем используйте напрямуюcount, что ничем не отличается от рассмотренного выше случая.

условие

Можно просто считать, что в компоненте для возвращаемой древовидной структуры React Elements тип и ключевой атрибут элемента в определенной позиции остаются неизменными, и React выберет повторное использование экземпляра компонента; в противном случае, например, из<A/>компонент переключился на<B/>компонент, A будет уничтожен, затем B будет перестроен, а B в это время выполнит первый кадр.

В примере можно пройтиuseStateи т.п. иметь местное гос. При повторном использовании эти состояния сохраняются. И если его нельзя использовать повторно, состояние уничтожается.

НапримерuseState, создает состояние для текущего функционального компонента, значение которого сохраняется независимо от функции.useStateвернет массив, в котором вы получите значение состояния и метод обновления состояния. При деструктуризации значение состояния присваивается константе в рамках текущей функции рендеринга.stateсередина.

const [state, setState] = useState(initialState);

Когда компонент создан и не используется повторно, т.е. в первом кадре компонента, состоянию будет присвоено начальное значениеinitialState, и в последующем процессе повторного использования ему не будет повторно присваиваться начальное значение.

позвонивsetState, значение состояния может быть обновлено.

Каждый кадр имеет независимое состояние

Чтобы было ясно,stateКак константа в функции, это обычные данные, и нет такой операции, как привязка данных, чтобы заставить DOM обновляться. вызовsetStateПосле этого React повторно выполнит функцию рендеринга и все.

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

Для того, чтобы углубить впечатление, давайте посмотримПример 2, что является усложнением примера с официального сайта React:

Edit 每一帧拥有独立的状态

function Example2() {
    const [count, setCount] = useState(0);

    const handleClick = () => {
        setTimeout(() => {
            setCount(count + 1);
        }, 3000);
    };

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>
                setCount
            </button>
            <button onClick={handleClick}>
                Delay setCount
            </button>
        </div>
    );
}

В первом кадре,pТекст на этикетке равен 0. Нажмите «Delay setCount», текст по-прежнему равен 0. Затем дважды нажмите «setCount» в течение 3 секунд, второй кадр и третий кадр будут выполнены соответственно. ты увидишьpТекст в метке меняется с 0 на 1, 2. Но после нажатия «Delay setCount» в течение 3 секунд текст возвращается к 1.

// 第一帧
const count_1 = 0;

const handleClick_1 = () => {
    const delayAction_1 = () => {
        setCount(count_1 + 1);
    };
    setTimeout(delayAction_1, 3000);
};

//...
<button onClick={handleClick_1}>
//...

// 点击 "setCount" 后第二帧
const count_2 = 1;

const handleClick_2 = () => {
    const delayAction_2 = () => {
        setCount(count_2 + 1);
    };
    setTimeout(delayAction_2, 3000);
};

//...
<button onClick={handleClick_2}>
//...

// 再次点击 "setCount" 后第三帧
const count_3 = 2;

const handleClick_3 = () => {
    const delayAction_3 = () => {
        setCount(count_3 + 1);
    };
    setTimeout(delayAction_3, 3000);
};

//...
<button onClick={handleClick_3}>
//...

count,handleClickобеExample2Константы в области видимости функции. При нажатии «Delay setCount» функция выполнения после того, как таймер установлен на истечение 3000 мс, активируется.delayAction_1, функция читаетcount_1Значение константы равно 0, что совпадает со вторым кадром.count_2Это не имеет значения.

Получить значения в прошлых или будущих кадрах

Для состояния, если вы хотите нажать «Delay setCount» в первом кадре, при выполнении асинхронной функции обратного вызова, получитеcountЗначение в последнем кадре, вы можетеsetCount Передать функцию в качестве параметра.

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

Если вы хотите сравнить с компонентом класса,useRefЭффект связан с тем, чтобы позволить вам в компоненте классаthisДобавьте к нему свойства.

const refContainer = useRef(initialValue);

В первом кадре компонентаrefContainer.currentбудет присвоено начальное значениеinitialValue, и после этого никаких изменений не происходит. Но вы можете установить его значение самостоятельно. Установка его значения не приведет к повторному запуску функции рендеринга.

Например, мы передаем реквизит или состояние n-го кадра черезuseRefДля сохранения в n + 1-м кадре можно прочитать значение в прошлом, n-м кадре. Мы также можем использовать ссылку, чтобы сохранить некоторые реквизиты или состояние в кадре n + 1, а затем прочитать их в асинхронной функции обратного вызова, объявленной в кадре n.

правильноПример 2изменить его, чтобы получитьПример 3, чтобы увидеть конкретный эффект:

Edit 获取过去或未来帧中的值

function Example() {
    const [count, setCount] = useState(0);

    const currentCount = useRef(count);

    currentCount.current = count;

    const handleClick = () => {
        setTimeout(() => {
            setCount(currentCount.current + 1);
        }, 3000);
    };

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>
                setCount
            </button>
            <button onClick={handleClick}>
                Delay setCount
            </button>
        </div>
    );
}

существуетsetCountЗатем будет выполнен следующий кадр, в начале функции,currentCountвсегда с последнимиcountсостояние синхронизировано. Таким образом, вsetTimeoutВы можете использовать этот метод для получения текущего значения счетчика при выполнении функции обратного вызова.

пройти дальшеПример 4Узнайте, как получить значения из прошлых кадров:

Edit 获取过去帧中的值

function Example4() {
    const [count, setCount] = useState(1);

    const prevCountRef = useRef(1);
    const prevCount = prevCountRef.current;
    prevCountRef.current = count;

    const handleClick = () => {
        setCount(prevCount + count);
    };

    return (
        <div>
            <p>{count}</p>
            <button onClick={handleClick}>SetCount</button>
        </div>
    );
}

Функция, реализованная этим кодом, заключается в том, что начальное значение счетчика равно 1, а после нажатия кнопки оно накапливается до 2, а затем нажимается кнопка, и значение текущего счетчика и значение предыдущего счетчика всегда накапливается для получения нового значения count.

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

Тот же метод мы можем использовать для сохранения любого значения: реквизита, переменной состояния, даже функции и т. д. Позже в разделе «Эффекты» мы продолжим использовать ссылки в наших интересах.

Каждый кадр может иметь независимые эффекты.

Если вы проясните концепцию «каждый кадр имеет независимую переменную», вы обнаружите, что если useEffect/useLayoutEffect имеет одну и только одну функцию в качестве параметра, то Эффекты также независимы каждый раз, когда выполняется функция рендеринга. Потому что он выполняется в нужное время в функции рендеринга.

заuseEffectНапример, время выполнения наступает после того, как были сделаны все изменения DOM и браузер отобразил страницу, иuseLayoutEffectи в компоненте классаcomponentDidMount, componentDidUpdateConsistent — вызывается синхронно сразу после того, как React завершает обновление DOM, блокируя отрисовку страницы.

Если useEffect не передает второй параметр, то функция эффекта, переданная в первом параметре, независима для каждого выполнения функции рендеринга. Реквизит или состояние, захваченные в каждой функции эффекта, поступают из этой функции рендеринга.

Мы можем наблюдать другой пример:

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

    useEffect(() => {
        setTimeout(() => {
            console.log(`You clicked ${count} times`);
        }, 3000);
    });

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
        </button>
        </div>
    );
}

В этом примере каждая параcountПосле внесения изменений, после повторного выполнения функции рендеринга, печать с задержкой в ​​3 секундыcountценность .

Если мы продолжим нажимать кнопку, каков будет напечатанный результат?

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

Если вы переключитесь на компонент класса, попробуйте использоватьcomponentDidUpdateПри реализации вы получите разные результаты:

componentDidUpdate() {
    setTimeout(() => {
        console.log(`You clicked ${this.state.count} times`);
    }, 3000);
}

this.state.countвсегда указывать на последнююcountзначение, а не значение, принадлежащее вызову функции рендеринга.

Поэтому при использовании useEffect следует отбросить мысли о жизненных циклах в компонентах класса. Они не то же самое. Преднамеренный поиск альтернатив этим функциям жизненного цикла в useEffect зайдет в тупик и не сможет в полной мере использовать возможности useEffect.

Выполнение эффектов в выравнивании

React сравнивает значения React Elements до и после и обновляет только те части DOM, которые действительно изменились. Есть ли аналогичная концепция для эффектов?

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

Смысл этой зависимости таков: от каких переменных зависит текущий Эффект.

Но иногда проблема не всегда решается. Например, на официальном сайтетакой пример:

const [count, setCount] = useState(0);

useEffect(() => {
    const id = setInterval(() => {
        setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
}, [count]);

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

Однако не следует использовать следующие методы:

Ищите какие-то переменные в функции Effect, чтобы добавить их в deps, и условия должны быть соблюдены: когда они изменяются, эффект должен запускаться повторно.

Согласно этой практике,countизменения, мы не хотим продлеватьsetInterval, поэтому deps — это пустой массив. Это означает, что хук запускается только один раз, когда компонент монтируется. Эффект, очевидно, зависит отcount, но мы наврали, что у него нет зависимостей, то когдаsetIntervalПри выполнении callback-функции полученныйcountЗначение всегда равно 0.

С этой проблемой невозможно удалить напрямую из deps. Успокойтесь и проанализируйте, почему это используется здесьcount? Можно ли избежать его прямого использования?

Видно, что вsetCountиспользуется вcount, чтобы поставитьcountПеревести вcount + 1, а затем вернулся в React. React на самом деле уже знает текущийcount, все, что нам нужно сказать React, — это увеличить состояние, независимо от того, какое значение оно имеет в настоящее время.

Таким образом, существует лучшая практика: при изменении состояния следует использовать функциональную форму setState вместо непосредственного получения текущего состояния.

setCount(c => c + 1);

Другой сценарий:

const [count, setCount] = useState(0);

useEffect(() => {
    const id = setInterval(() => {
        console.log(count);
    }, 1000);
    return () => clearInterval(id);
}, []);

Здесь тоже, когдаcountизменения, мы не хотим продлеватьsetInterval. но мы можем поставитьcountСохранено реф.

const [count, setCount] = useState(0);
const countRef = useRef();
countRef.current = count;

useEffect(() => {
    const id = setInterval(() => {
        console.log(countRef.current);
    }, 1000);
    return () => clearInterval(id);
}, []);

так,countДействительно, он больше не используется, вместо этого используется ссылка для хранения переменной, которая является общей для всех фреймов.

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

По этому вопросу,FAQ на официальном сайтеОтвет уже дан: для функций используйте useCallback, чтобы избежать повторного создания, для объектов или массивов можно использовать useMemo. Тем самым уменьшая смену зам.

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

Использование плагина ESLinteslint-plugin-react-hooks@>=2.4.0,необходимо.

Помимо помощи в проверке плагинаДва правила, которым нужно следовать при использовании HookКроме того, он также предложит вам содержимое, которое должны заполнить сотрудники при использовании useEffect или useMemo.

Если вы используете VSCode и у вас установлено расширение ESLint. Когда вы пишете useEffect или useMemo, а содержимое в deps неполное, строка, где находится deps, выдаст предупреждение или подсказку об ошибке, и будет функция быстрого исправления, которая автоматически заполнит отсутствующие для вас deps.

Для этих советов не используйте грубую силу черезeslint-disableНеполноценный. В будущем вы можете снова изменить useEffect или useMemo, и если вы используете новую зависимость и пропустите ее в deps, это вызовет новые проблемы. Есть несколько сценариев, таких как useEffect, который зависит от функции и заполняет deps. Но в этой функции используется useCallback, а deps отсутствует, в таком случае, если возникнет проблема, устранить ее будет очень сложно, так почему же ESLint должен молчать?

Попробуйте использовать метод из предыдущего раздела для анализа.Для некоторых переменных, которые не хотят вызывать повторное обновление эффекта, используйте ref для его решения. Чтобы получить состояние для вычисления нового состояния, попробуйте settingsState в качестве параметра функции или используйте useReducer для объединения нескольких типов состояний.

Используйте useMemo/useCallback

Смысл useMemo в том, чтобы получить новое значение путем вычисления некоторых переменных. Добавляя эти переменные в зависимые зависимости, этот расчет пропускается, когда ни одно из значений в зависимостях не изменилось. Функция, переданная в useMemo, будет вызываться синхронно во время вызова функции рендеринга.

Некоторые относительно трудоемкие вычисления можно кэшировать с помощью useMemo.

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

const data = useMemo(() => ({
    a,
    b,
    c,
    d: 'xxx'
}), [a, b, c]);

// 可以用 useCallback 代替
const fn = useMemo(() => () => {
    // do something
}, [a, b]);

const memoComponentsA = useMemo(() => (
    <ComponentsA {...someProps} />
), [someProps]);

В этих примерах цель useMemo — максимально использовать кэшированное значение.

Для функции, когда она используется в качестве зависимости от другого useEffect, уменьшение регенерации функции может уменьшить вызов Effect и даже избежать создания некоторых бесконечных циклов;

Для объектов и массивов, если подкомпонент использует его в качестве реквизита, уменьшение его регенерации может избежать ненужного повторного рендеринга подкомпонентов и повысить производительность.

Неоптимизированный код выглядит следующим образом:

const data = { id };

return <Child data={data}>;

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

const data = useMemo(() => ({ id }), [id]);

return <Child data={data}>;

При рендеринге родительского компонента, пока идентификатор остается неизменным, значение данных не изменится, а дочерний компонент также не будет рендериться.

Для React Elements, возвращаемых компонентом, мы можем выборочно извлечь часть элементов, кэшировать их через useMemo и избежать повторного рендеринга этой части.

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

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

А useMemo в функциональном компоненте действительно может заменить эту часть работы. Для простоты понимания рассмотримПример 5:

Edit 使用 useMemo 缓存 React Elements

function Example(props) {
    const [count, setCount] = useState(0);
    const [foo] = useState("foo");

    const main = (
        <div>
            <Item key={1} x={1} foo={foo} />
            <Item key={2} x={2} foo={foo} />
            <Item key={3} x={3} foo={foo} />
            <Item key={4} x={4} foo={foo} />
            <Item key={5} x={5} foo={foo} />
        </div>
    );

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>setCount</button>
            {main}
        </div>
    );
}

Предположение<Item>Компонент, собственный рендер занимает больше времени. По умолчанию каждый раз, когда setCount изменяет значение count,<Example>Сделайте рендер, который возвращает 3 элемента React.<Item>Кроме того, повторный рендеринг, его трудоемкая операция блокирует рендеринг пользовательского интерфейса. Это привело к заметному заиканию после нажатия кнопки «setCount».

Для оптимизации производительности мы можемmainЭта часть переменной является самостоятельной как компонент<Main>, разделенный, и<Main>используя что-то вродеReact.memo , shouldComponentUpdateспособ сделатьcountКогда свойства меняются,<Main>Не повторяйте рендер.

const Main = React.memo((props) => {
    const { foo }= props;
    return (
        <div>
            <Item key={1} x={1} foo={foo} />
                <Item key={2} x={2} foo={foo} />
                <Item key={3} x={3} foo={foo} />
                <Item key={4} x={4} foo={foo} />
                <Item key={5} x={5} foo={foo} />
        </div>
    );
});

И теперь мы можем использоватьuseMemo, чтобы избежать разделения компонентов, а код стал более лаконичным и понятным:

function Example(props) {
    const [count, setCount] = useState(0);
    const [foo] = useState("foo");

    const main = useMemo(() => (
        <div>
            <Item key={1} x={1} foo={foo} />
            <Item key={2} x={2} foo={foo} />
            <Item key={3} x={3} foo={foo} />
            <Item key={4} x={4} foo={foo} />
            <Item key={5} x={5} foo={foo} />
        </div>
    ), [foo]);

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>setCount</button>
            {main}
        </div>
    );
}

ленивый инициализатор

Для государства он имеетленивый метод инициализации. Возможно, кто-то не понимает, что он делает.

someExpensiveComputationявляется относительно трудоемкой операцией. Если мы непосредственно используем

const initialState = someExpensiveComputation(props);
const [state, setState] = useState(initialState);

Обратите внимание, что хотяinitialStateимеет значение только при инициализации, ноsomeExpensiveComputationназывается каждый кадр. Только при использовании ленивых методов инициализации:

const [state, setState] = useState(() => {
    const initialState = someExpensiveComputation(props);
    return initialState;
});

потому чтоsomeExpensiveComputationЗапускается под анонимной функцией, которая вызывается тогда и только тогда, когда она инициализирована, что оптимизирует производительность.

Мы можем даже выйти за пределы вычислительного состояния, чтобы выполнить любую дорогостоящую инициализацию.

useState(() => {
    someExpensiveComputation(props);
    return null;
});

Избегайте злоупотребления рефами

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

В итоге практику можно резюмировать так:

useEffect Для функциональных зависимостей попробуйте поместить функцию в эффект или используйте useCallback для ее обертывания; useEffect/useCallback/useMemo, для зависимостей состояния или других свойств заполните deps в соответствии с подсказками eslint; если не используете состояние напрямую, вы просто хотите изменить состояние, используйте метод ввода функции setState (setState(c => c + 1)) вместо этого; если процесс изменения состояния зависит от других свойств, попробуйте агрегировать состояние и свойства и переписать их в форме useReducer. Если ни один из этих методов не работает, используйте ref, но делайте это с осторожностью.

Не злоупотребляйте useMemo

Используйте useMemo, чтобы напрямую вернуть результат последнего вычисления, когда зависимости не изменились, чтобы дочерний компонент пропускал рендеринг.

Но при возврате примитивных типов данных (таких как строки, числа, логические значения). Даже если он участвует в вычислении, пока содержимое, от которого зависит deps, остается неизменным, возвращаемый результат, скорее всего, останется неизменным. На этом этапе необходимо сбалансировать временные затраты на этот расчет с дополнительными затратами пространства на useMemo (кэшировать последний результат).

Кроме того, если массив зависимостей deps в useMemo пуст, это означает, что вы просто хотите сохранить значение, которое никогда не изменится при повторном рендеринге.

Например:

const Comp = () => {
    const data = useMemo(() => ({ type: 'xxx' }), []);
    return <Child data={data}>;
}

можно заменить на:

const Comp = () => {
    const { current: data } = useRef({ type: 'xxx' });
    return <Child data={data}>;
}

четное:

const data = { type: 'xxx' };
const Comp = () => {
    return <Child data={data}>;
}

Кроме того, если deps часто меняется, нам также нужно подумать о том, нужно ли использовать useMemo. Поскольку useMemo занимает дополнительное место, ему также необходимо проверять, изменяется ли deps каждый раз при рендеринге, что обходится дороже, чем отсутствие использования useMemo.

контролируемые и неконтролируемые

В кастомных хуках у нас может быть такая логика:

useSomething = (inputCount) => {
    const [ count, setCount ] = setState(inputCount);
};

Здесь есть проблема, внешний входящийinputCountсвойство изменилось так, что оно соответствуетuseSomethingВнутри крючкаcountВы хотите обновить это, когда состояние несовместимоcount?

Он не будет обновляться по умолчанию, потому что параметр useState представляет начальное значение, только когдаuseSomethingПервоначально назначен наcountсостояние. следовать заcountСостояние будет таким жеinputCountЭто не имеет значения. Таким образом, внешнее не может напрямую контролировать состояние, мы называем его неуправляемым.

Если вы хотите, чтобы вами всегда управляли внешние реквизиты, например, в этом примере,useSomethingвнутренний,countЗначение этого состояния необходимо изменить сinputCountДля синхронизации нужно написать:

useSomething = (inputCount) => {
    const [ count, setCount ] = setState(inputCount);
    setCount(inputCount);
};

setCountReact немедленно выйдет из текущего рендеринга и перезапустит функцию рендеринга с обновленным состоянием. в этот момент,Официальная документация сайтаобъясняется.

В соответствии с этим механизмом, в то время как состояние синхронизируется внешним миром, можно изменить состояние внутри через setState, что может вызвать новые проблемы. НапримерuseSomethingПервоначально count равен 0, а последующие внутренние проходыsetCountотредактированоcount1. Когда функция рендеринга внешнего функционального компонента вызывается снова, она также будет вызываться снова.useSomething, входящийinputCountпо-прежнему 0, он поместитcountизменить обратно на 0. Это, скорее всего, не так, как ожидалось.

При возникновении такой проблемы рекомендуетсяinputCountТекущее значениеsetCount(inputCount).

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

Практика: используйте слайдер

Пользовательские крючки с помощью селектора смахиванияuserSliderМы можем ответить на поставленный выше вопрос и, кстати, подвести итоги этой статьи.

image

userSliderЛогика, которую необходимо реализовать, такова: нажмите и удерживайте круглую область ручки скользящего селектора и перетащите, чтобы настроить значение, а диапазон значений — от 0 до 1.

userSliderОн отвечает только за реализацию логики, а стиль интерфейса делает сам компонент. Для имитации реального бизнеса текущее значение также отображается в виде текста. И есть несколько кнопок для переключения начального значения значения, это для переключения классификации, текущий ползунковый селектор нужно сбросить на определенное значение.

Следуя обычной логике, реализуем следующий код:

Edit useSlider 问题

Текущая проблема заключается в том,useEffectОн включает в себя получение и расчет нескольких состояний. Из-за модификации состояния в нескольких операциях, которые заставляют мышь нажимать, двигаться и подпрыгивать,useEffectЧастые обновления включают в себя отмену и повторную привязку прослушивателей событий нажатия, перемещения и всплывающих окон мыши, что приводит к проблемам с производительностью и трудно наблюдаемым ошибкам.

и предыдущийsetIntervalПример похож, мы не хотим обновляться при изменении состоянияuseEffect. Поскольку здесь задействовано несколько состояний: скользит ли оно, положение мыши, проблема последней мыши, скользящая ширина селектора, если она объединена в одноstate, столкнется с проблемой непонятного кода и отсутствия связности, мы стараемся использоватьuseReducerДелай замену.

const reducer = (state, action) => {
    switch (action.type) {
        case "start":
            return {
                ...state,
                lastPos: action.x,
                slideRange: action.slideWidth,
                sliding: true
            };
        case "move": {
            if (!state.sliding) {
                return state;
            }
            const pos = action.x;
            const delta = pos - state.lastPos;
            return {
                ...state,
                lastPos: pos,
                ratio: fixRatio(state.ratio + delta / state.slideRange)
            };
        }
        case "end": {
            if (!state.sliding) {
                return state;
            }
            const pos = action.x;
            const delta = pos - state.lastPos;
            return {
                ...state,
                lastPos: pos,
                ratio: fixRatio(state.ratio + delta / state.slideRange),
                sliding: false
            };
        }
        default:
            return state;
    }
};

//...

const handleThumbMouseDown = useCallback(ev => {
    const hotArea = hotAreaRef.current;
    dispatch({
        type: "start",
        x: ev.pageX
      slideWidth: hotArea.clientWidth
    });
}, []);

useEffect(() => {
    const onSliding = ev => {
        dispatch({
            type: "move",
            x: ev.pageX
        });
    };
    const onSlideEnd = ev => {
        dispatch({
            type: "end",
            x: ev.pageX
        });
    };
    document.addEventListener("mousemove", onSliding);
    document.addEventListener("mouseup", onSlideEnd);

    return () => {
        document.removeEventListener("mousemove", onSliding);
        document.removeEventListener("mouseup", onSlideEnd);
    };
}, []);

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

Далее, есть еще одна проблема, которая не решена.initRatioпередается как начальное значение,useSliderВнутреннее соотношение не контролируется извне.

В качестве примера возьмем настройку музыкального эквалайзера: текущий скользящий селектор представляет значение усиления на низкочастотном конце (31), пользователь может установить значение этого значения, перетаскивая ползунок (диапазон от -12 до 12 дБ, мы установите его на 3 дБ). В то же время мы предоставляем некоторые предустановленные параметры, после выбора предустановленного параметра, такого как стиль «поп», текущий ползунок необходимо сбросить до определенного значения -1 дБ. с этой целью,useSliderНеобходимо предоставить способ управления государством.

image

Согласно введению в предыдущем разделе, вuseSliderв начале мы можем поставить свойствоinitRatioТекущее значение сравнивается с предыдущим значением, и если есть изменение, выполняетсяsetRatio. Но все еще есть сцены, которые не могут быть удовлетворены: пользователь выбирает «популярный» пресет, затем перетаскивает ползунок для настройки, а затем снова выбирает «популярный» пресет.initRatioНичего не изменилось, но мы ожидаем, что соотношение вернется кinitRatio.

Решение этой проблемы заключается вuseSliderдобавить один внутриsetRatioметод.

const setRatio = useCallback(
    ratio =>
        dispatch({
            type: "setRatio",
            ratio
        }),
    []
);

Выходные данные метода используются извне для управления соотношением.initRatioБольше не управляет состоянием ratio, используется только для установки начального значения.

Вы можете посмотреть окончательный план реализации:

Edit useSlider 最终版

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

заключительные замечания

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

Релевантная информация

A Complete Guide to useEffect

Эта статья была опубликована сКоманда внешнего интерфейса NetEase Cloud Music, Любое несанкционированное воспроизведение статьи запрещено. Мы всегда нанимаем, если вы готовы сменить работу и вам нравится облачная музыка, тоПрисоединяйтесь к нам!