Вы действительно используете React Hooks, верно?

React.js

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

Вопрос 1: Должен ли я использовать одну переменную состояния или несколько переменных состояния?

useStateВнешний вид позволяет использовать несколько переменных состояний для сохранения состояния, например:

const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);

Но в то же время нам может понравиться компонент Classthis.stateто же самое, сложить все состояния в одноobject, поэтому нужна только одна переменная состояния:

const [state, setState] = useState({
  width: 100,
  height: 100,
  left: 0,
  top: 0
});

Итак, вопрос в том, должны ли мы использовать одну переменную состояния или несколько переменных состояния?

Если вы используете одну переменную состояния, вам нужно объединять предыдущее состояние каждый раз, когда вы обновляете состояние. потому чтоuseStateвернутьsetStateзаменит исходное значение. Это и компонент классаthis.setStateразные.this.setStateавтоматически объединит обновленные поля вthis.stateв объекте.

const handleMouseMove = (e) => {
  setState((prevState) => ({
    ...prevState,
    left: e.pageX,
    top: e.pageY,
  }))
};

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

function usePosition() {
  const [left, setLeft] = useState(0);
  const [top, setTop] = useState(0);

  useEffect(() => {
    // ...
  }, []);

  return [left, top, setLeft, setTop];
}

Мы обнаружили, что каждое обновлениеleftВремяtopбудет соответствующим образом обновлен. Поэтому поставьтеtopа такжеleftРазделение на две переменные состояния кажется излишним.

Прежде чем использовать состояние, нам нужно рассмотреть «зернистость» разделения состояния. Если степень детализации слишком мала, код становится избыточным. Если степень детализации слишком грубая, возможность повторного использования кода будет снижена. Итак, какие штаты следует объединить, а какие разделить? Я суммировал следующие два момента:

  1. Разделите совершенно не связанные между собой состояния на группы состояний. Напримерsizeа такжеposition.
  2. Если определенные состояния взаимосвязаны или должны изменяться вместе, их можно объединить в набор состояний. Напримерleftа такжеtop.
function Box() {
  const [position, setPosition] = usePosition();
  const [size, setSize] = useState({width: 100, height: 100});
  // ...
}

function usePosition() {
  const [position, setPosition] = useState({left: 0, top: 0});

  useEffect(() => {
    // ...
  }, []);

  return [position, setPosition];
}

Вопрос 2: Слишком много зависимостей DEPS затрудняет обслуживание крючков?

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

function Example({id, name}) {
  useEffect(() => {
    console.log(id, name);
  }, [id, name]); 
}

В приведенном выше примере только тогда, когдаidилиnameЖурнал распечатывается только при наличии изменений. Массив зависимостей должен содержать все значения, задействованные в потоке данных React, используемом внутри обратного вызова, напримерstate,propsи их производные. Если есть упущения, это может привести к ошибкам. На самом деле это проблема замыкания JS.Студенты, которые не уверены в замыкании, могут сами погуглить, и я не буду здесь подробно об этом рассказывать.

function Example({id, name}) {
  useEffect(() => {
    // 由于依赖数组中不包含 name,所以当 name 发生变化时,无法打印日志
    console.log(id, name); 
  }, [id]);
}

В React, помимо useEffect, есть хуки, которые получают в качестве параметров массив зависимостей.useMemo,useCallbackа такжеuseImperativeHandle. Как мы только что упоминали, не пропускайте значение зависимости внутри функции обратного вызова в массиве зависимостей. Однако, если массив зависимостей зависит от слишком многих вещей, это может затруднить сопровождение кода. Я видел этот кусок кода в проекте:

const refresh = useCallback(() => {
  // ...
}, [name, searchState, address, status, personA, personB, progress, page, size]);

Не говорите о внутренней логике, просто видеть эту кучу зависимостей — ошеломительно! Если в проекте полно такого кода, то можно себе представить, как больно его поддерживать. Как я могу избежать написания такого кода?

Во-первых, нужно переосмыслить, действительно ли нужны эти депы? Рассмотрим следующий пример:

function Example({id}) {
  const requestParams = useRef({});
  requestParams.current = {page: 1, size: 20, id};

  const refresh = useCallback(() => {
    doRefresh(requestParams.current);
  }, []);


  useEffect(() => {
    id && refresh(); 
  }, [id, refresh]); // 思考这里的 deps list 是否合理?
}

несмотря на то чтоuseEffectФункция обратного вызова зависит отidа такжеrefreshметод, но наблюдайтеrefreshМетод может обнаружить, что он никогда не изменится после создания первого рендера. Поэтому сделайте так, какuseEffectДеп лишний.

Во-вторых, если эти зависимости действительно нужны, следует ли эту логику помещать в один и тот же хук?

function Example({id, name, address, status, personA, personB, progress}) {
  const [page, setPage] = useState();
  const [size, setSize] = useState();

  const doSearch = useCallback(() => {
    // ...
  }, []);

  const doRefresh = useCallback(() => {
    // ...
  }, []);


  useEffect(() => {
    id && doSearch({name, address, status, personA, personB, progress});
    page && doRefresh({name, page, size});
  }, [id, name, address, status, personA, personB, progress, page, size]);
}

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

useEffect(() => {
  id && doSearch({name, address, status, personA, personB, progress});
}, [id, name, address, status, personA, personB, progress]);

useEffect(() => {
  page && doRefresh({name, page, size});
}, [name,  page, size]);

Что делать, если логика не может быть разделена, но зависимый массив по-прежнему зависит от слишком многих вещей? Так же, как наш код выше:

useEffect(() => {
  id && doSearch({name, address, status, personA, personB, progress});
}, [id, name, address, status, personA, personB, progress]);

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

const [filters, setFilters] = useState({
  name: "",
  address: "",
  status: "",
  personA: "",
  personB: "",
  progress: ""
});

useEffect(() => {
  id && doSearch(filters);
}, [id, filters]);

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

const useValues = () => {
  const [values, setValues] = useState({
    data: {},
    count: 0
  });

  const [updateData] = useCallback(
      (nextData) => {
        setValues({
          data: nextData,
          count: values.count + 1 // 因为 callback 内部依赖了外部的 values 变量,所以必须在依赖数组中指定它
        });
      },
      [values], 
  );

  return [values, updateData];
};

В приведенном выше коде мы должныuseCallbackуказан в массиве зависимостейvalues, иначе мы не сможем получить последнюю информацию в обратном вызовеvaluesусловие. Однако, поsetStateФункция обратного вызова, нам больше не нужно полагаться на внешниеvaluesпеременная, поэтому ее также не нужно указывать в массиве зависимостей. Как следующее:

const useValues = () => {
  const [values, setValues] = useState({});

  const [updateData] = useCallback((nextData) => {
    setValues((prevValues) => ({
      data: nextData,
      count: prevValues.count + 1, // 通过 setState 回调函数获取最新的 values 状态,这时 callback 不再依赖于外部的 values 变量了,因此依赖数组中不需要指定任何值
    }));
  }, []); // 这个 callback 永远不会重新创建

  return [values, updateData];
};

Наконец, можно такжеrefдля хранения изменяемых переменных. Раньше мы толькоrefИспользуется как инструмент для хранения ссылок на узлы DOM, можетuseRefКрюки могут делать гораздо больше. Мы можем использовать его для хранения ссылки на некоторое значение, а также для чтения и записи в него. Например:

const useValues = () => {
  const [values, setValues] = useState({});
  const latestValues = useRef(values);
  latestValues.current = values;

  const [updateData] = useCallback((nextData) => {
    setValues({
      data: nextData,
      count: latestValues.current.count + 1,
    });
  }, []); 

  return [values, updateData];
};

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

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

  1. Массивы зависимостей не должны иметь более 3-х значений, иначе код будет сложно поддерживать.
  2. Если мы обнаружим, что зависимый массив зависит от слишком большого количества значений, мы должны принять некоторые меры, чтобы уменьшить его.
    • Удалите ненужные зависимости.
    • Разделите хуки на более мелкие единицы, каждый хук зависит от своего собственного массива зависимостей.
    • Объедините несколько зависимых значений в одно, объединив связанные состояния.
    • пройти черезsetStateФункция обратного вызова получает последнее состояние, чтобы уменьшить внешние зависимости.
    • пройти черезrefчтобы прочитать значение изменяемой переменной, но будьте осторожны, чтобы контролировать, как оно изменяется.

Вопрос 3: Следует ли его использовать?useMemo?

Следует использоватьuseMemo? Что касается этого вопроса, некоторые люди никогда не задумывались об этом, а некоторые люди даже не думают, что это проблема. В любом случае, просто используйтеuseMemoилиuseCallback「包裹一下」,似乎就能使应用远离性能的问题。但真的是这样吗? иногдаuseMemo

Почему ты это сказал?首先,我们需要知道useMemoТакже есть накладные расходы.useMemoОна будет "запоминать" некоторые значения. При этом при последующем рендере будет выниматься значение в зависимом массиве и сравниваться с последним записанным значением. Если оно не равно, функция обратного вызова будет выполняться заново В противном случае будет возвращено "запомненное" значение. Сам этот процесс будет потреблять определенное количество памяти и вычислительных ресурсов. Поэтому чрезмерное использованиеuseMemoМожет повлиять на работу программы.

для добросовестного использованияuseMemo, нам нужно выяснитьuseMemoПрименимые сценарии:

  • Некоторые вычисления имеют большую стоимость, и нам нужно «запоминать» возвращаемое значение, чтобы избежать повторного вычисления каждый раз при RENDER.
  • Нам также необходимо «запомнить» это значение, поскольку ссылка на значение изменяется, что приводит к повторному рендерингу нижестоящих компонентов.

Давайте посмотрим пример:

interface IExampleProps {
  page: number;
  type: string;
}

const Example = ({page, type}: IExampleProps) => {
  const resolvedValue = useMemo(() => {
    return getResolvedValue(page, type);
  }, [page, type]);

  return <ExpensiveComponent resolvedValue={resolvedValue}/>;
};

Примечание:ExpensiveComponentкомпонент завернутыйReact.memo.

В приведенном выше примере рендерингExpensiveComponentСтоимость огромна. Так когдаresolvedValueАвтор не хочет повторно отображать этот компонент, когда ссылка на файл . Поэтому автор использовалuseMemo, чтобы избежать пересчета при каждом рендереresolvedValue, вызывая изменение его ссылки, что приводит к повторному рендерингу нижестоящего компонента.

Это опасение справедливо, но использованиеuseMemoПеред этим мы должны подумать над двумя вопросами:

  1. Перейти кuseMemoФункция дорого стоит? В приведенном выше примере рассмотримgetResolvedValueНакладные расходы функции не велики. Большинство методов в JS оптимизированы, напримерArray.map,Array.forEachЖдать. Если вы выполняете недорогую операцию, вам не нужно запоминать возвращаемое значение. В противном случае используйтеuseMemoСтоимость сама по себе может перевешивать стоимость пересчета этого значения. Поэтому для некоторых простых операций JS нам не нужно использоватьuseMemoчтобы «запомнить» его возвращаемое значение.
  2. Изменяется ли ссылка на «запомненное» значение, когда ввод тот же? В приведенном выше примере, когдаpageа такжеtypeОдинаковый,resolvedValueСсылки изменятся? Здесь нам нужно рассмотретьresolvedValueтипа. еслиresolvedValueявляется объектом.Поскольку мы используем «функциональное программирование» в нашем проекте, каждый вызов функции будет генерировать новую ссылку. но еслиresolvedValueявляется примитивным значением (string, boolean, null, undefined, number, symbol), понятия «эталон» нет, и вычисляемое значение каждый раз должно быть равным. То есть,ExpensiveComponentКомпоненты не будут перерисовываться.

Следовательно, еслиgetResolvedValueне дорого иresolvedValueВозвращает примитивное значение, такое как строка, тогда мы можем полностью удалить его.useMemo, как показано ниже:

interface IExampleProps {
  page: number;
  type: string;
}

const Example = ({page, type}: IExampleProps) => {
  const resolvedValue = getResolvedValue(page, type);
  return <ExpensiveComponent resolvedValue={resolvedValue}/>;
};

Еще одно недоразумение — оценка стоимости создания функции. Некоторые люди думают, что создание функций в рендере может быть дорогим, чтобы избежать многократного создания функций, используйтеuseMemoилиuseCallback. Но для современных браузеров стоимость создания функции тривиальна. Поэтому нам не нужно использоватьuseMemoилиuseCallbackЧтобы сохранить эту часть накладных расходов на производительность. Конечно, если это необходимо для того, чтобы ссылка обратного вызова была одинаковой каждый раз, когда вы выполняете рендеринг, вы можете использовать ее с уверенностью.useMemoилиuseCallback.

const Example = () => {
  const onSubmit = useCallback(() => { // 考虑这里的 useCallback 是否必要?
    doSomething();
  }, []);

  return <form onSubmit={onSubmit}></form>;
};

Я видел статью раньше (ссылка в конце статьи), где упоминалось, что если вы просто хотите сохранить ссылку на значение при повторном рендеринге, лучше использоватьuseRef, вместоuseMemo. Я не согласен с этой точкой зрения. Давайте посмотрим пример:

// 使用 useMemo
function Example() {
  const users = useMemo(() => [1, 2, 3], []);

  return <ExpensiveComponent users={users} />
}
  
// 使用 useRef
function Example() {
  const {current: users} = useRef([1, 2, 3]);

  return <ExpensiveComponent users={users} />
}

В приведенном выше примере мы используемuseMemoпомнить"usersМассивы не потому, что сам массив дорог, а потому, чтоusersссылка меняется при каждом рендеринге, в результате чего дочерний компонентExpensiveComponentПовторный рендеринг (может быть дороже).

Автор считает, что семантически его не следует употреблятьuseMemo, вместо этого вы должны использоватьuseRef, иначе он будет потреблять больше памяти и вычислительных ресурсов. Хотя в РеактеuseRefа такжеuseMemoРеализация немного отличается, но когдаuseMemoКогда зависимый массив является пустым массивом, он иuseRefСтоимость можно сказать почти одинаковая.useRefможно даже использовать напрямуюuseMemoсделать это так:

const useRef = (v) => {
  return useMemo(() => ({current: v}), []);
};

Поэтому я думаю использоватьuseMemoСохранение ссылки на значение согласованной не является большой проблемой.

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

function Example() {
  const data = useData();
  const [dataChanged, setDataChanged] = useState(false);

  useEffect(() => {
    setDataChanged((prevDataChanged) => !prevDataChanged); // 当 data 发生变化时,调用 setState。如果 data 值相同而引用不同,就可能会产生非预期的结果。
  }, [data]);

  console.log(dataChanged);

  return <ExpensiveComponent data={data} />;
}

const useData = () => {
  // 获取异步数据
  const resp = getAsyncData([]);

  // 处理获取到的异步数据,这里使用了 Array.map。因此,即使 data 相同,每次调用得到的引用也是不同的。
  const mapper = (data) => data.map((item) => ({...item, selected: false}));

  return resp ? mapper(resp) : resp;
};

В приведенном выше примере мы передаемuseDataКрюк получилdata. каждый рендерdataЗначение не изменилось, но ссылка несовместима. если поставитьdataиспользоватьuseEffectМассив зависимостей , может привести к неожиданным результатам. Кроме того, из-за различных ссылок это также приведет кExpensiveComponentПовторный рендеринг компонента, вызывающий проблемы с производительностью.

Если ссылка отличается из-за того, что значение реквизита одинаково, что приводит к повторному рендерингу дочернего компонента, это не обязательно проблема производительности. Потому что повторный рендеринг Virtual DOM ≠ повторный рендеринг DOM. Но когда дочерние компоненты особенно велики, Diff в Virtual DOM также обходится дорого. Следовательно, вы должны стараться избегать повторного рендеринга подкомпонентов.

Поэтому, используяuseMemoПеред этим давайте зададим себе несколько вопросов:

  1. Дорого стоит функция запомнить?
  2. Является ли возвращаемое значение исходным значением?
  3. Будет ли запомненное значение использоваться другими хуками или подкомпонентами?

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

1. Следует использоватьuseMemoсцена

  1. сохранить ссылки равными
    • Для объектов, массивов, функций и т. д., используемых внутри компонентов, если они используются в массивах зависимостей других хуков или передаются нижестоящим компонентам в качестве реквизита, вы должны использоватьuseMemo.
    • Следует использовать объекты, массивы, функции и т. д., представленные в пользовательских хуках.useMemo. чтобы гарантировать, что ссылка не изменится, когда значение будет тем же самым.
    • использоватьContextкогда, еслиProviderЗначение, определенное в значении (первого уровня), изменилось даже при использовании Pure Component илиReact.memo, все равно вызовет повторный рендеринг дочернего компонента. В этом случае все же рекомендуется использоватьuseMemoСохраняйте цитаты последовательными.
  2. Дорогой расчет
    • НапримерcloneDeepочень большие и глубоко иерархические данные

2. Сценарии без использования Memo

  1. Если возвращаемое значение является исходным значением:string, boolean, null, undefined, number, symbol(за исключением динамически объявленных символов), как правило, не нужно использоватьuseMemo.
  2. Объекты, массивы, функции и т. д. используются только внутри компонента (не передаются дочерним компонентам в качестве реквизита) и не используются в массивах зависимостей других хуков, как правило, их не нужно использовать.useMemo.

Вопрос 4: Могут ли хуки заменить компоненты более высокого порядка и реквизиты рендеринга?

До хуков у нас было два способа повторного использования логики компонентов:Render Propsа такжекомпоненты более высокого порядка. Но оба эти метода могут вызвать проблему «вложенного ада» JSX. Появление хуков упрощает повторное использование логики компонентов и решает проблему «вложенного ада». Хуки для React — это то же самое, что async/await для промисов.

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

Компонента высшего порядка HOC

Компонент более высокого порядка — это функция, которая принимает компонент в качестве параметра и возвращает новый компонент.

function enhance(Comp) {
  // 增加一些其他的功能
  return class extends Component {
    // ...
    render() {
      return <Comp />;
    }
  };
}

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

const RedButton = withStyles({
  root: {
    background: "red",
  },
})(Button);

В приведенном выше коде мы хотим сохранитьButtonЛогика компонента, но при этом мы хотим использовать его оригинальный стиль. Поэтому мы проходимwithStylesЭтот компонент более высокого порядка внедряет пользовательские стили и создает новый компонент.RedButton.

Render Props

Render Props инкапсулирует повторно используемую логику через родительские компоненты и предоставляет данные дочерним компонентам. Что касается того, как отображать данные после того, как подкомпонент получит данные, это полностью зависит от подкомпонента, и гибкость очень высока. В компонентах более высокого порядка результат рендеринга определяется родительским компонентом. Реквизиты рендеринга не создают новые компоненты и более интуитивно отражают «отношения родитель-потомок».

<Parent>
  {(data) => {
    // 你父亲已经把江山给你打好了,并给你留下了一堆金币,至于怎么花就看你自己了
    return <Child data={data} />;
  }}
</Parent>

Render Props, как часть JSX, может легко использовать жизненный цикл React и Props, State для рендеринга и имеет очень высокую степень свободы в рендеринге. В то же время, в отличие от хуков, ему нужно соблюдать некоторые правила, и вы можете с уверенностью использовать if/else, map и другие операции в нем.

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

Изменение приведенного выше примера HOC на Render Props действительно немного «хлопотно» в использовании:

<RedButton>
  {(styles)=>(
    <Button styles={styles}/>
  )}
</RedButton>

резюме

До хуков как компоненты более высокого порядка, так и Render Props, по сути, переносили логику повторного использования в родительские компоненты. После появления хуков мы вынесли логику повторного использования на верхний уровень компонента вместо того, чтобы принудительно поднимать ее в родительский компонент. Это позволяет избежать «ада гнездования», вызванного HOC и Render Props. Однако, как и Context<Provider/>а также<Consumer/>У этого есть отношения отца и дочернего уровня (отношения древесной структуры) или только рендеринг реквизиты или HOC.

Для хуков, Render Props и компонентов более высокого порядка все они имеют свои собственные сценарии использования:

  • Крючки:
    • Заменяет Class для большинства случаев использования, за исключениемgetSnapshotBeforeUpdateа такжеcomponentDidCatchПока не поддерживается.
    • Извлечь логику мультиплексирования. Хуки можно использовать и в других сценариях, за исключением тех, которые имеют четкие отношения родитель-потомок.
  • Реквизиты рендеринга: он имеет более высокую степень свободы в рендеринге компонентов и может выполнять динамический рендеринг в соответствии с данными, предоставленными родительским компонентом. Это подходит для сценариев, где есть четкие отношения родитель-потомок.
  • Компоненты более высокого порядка: подходят для инъекций и генерируют новый многоразовый компонент. Подходит для написания плагинов.

Тем не менее, сцены, которые могут использовать хуки, все равно должны сначала использовать хуки, а затем Render Props и HOC. Конечно, Hooks, Render Props и HOC не являются противоположностями. Мы можем либо использовать Hook для написания Render Props и HOC, либо использовать Render Props и Hooks в HOC.

Вопрос 5: Какие еще есть хорошие практики использования хуков?

1. Если типы хуков одинаковы и зависимые массивы непротиворечивы, их следует объединить в один хук. В противном случае будет больше накладных расходов.

const dataA = useMemo(() => {
  return getDataA();
}, [A, B]);

const dataB = useMemo(() => {
  return getDataB();
}, [A, B]);

// 应该合并为
  
const [dataA, dataB] = useMemo(() => {
  return [getDataA(), getDataB()]
}, [A, B]);

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

export const useToggle = (defaultVisible: boolean = false) => {
  const [visible, setVisible] = useState(defaultVisible);
  const show = () => setVisible(true);
  const hide = () => setVisible(false);

  return [visible, show, hide] as [typeof visible, typeof show, typeof hide];
};

const [isOpen, open, close] = useToggle(); // 在外部可以更方便地修改名字
const [visible, show, hide] = useToggle();

3.refНе подвергайте непосредственно внешнему использованию, но предоставьте метод для изменения значения. 4. В использованииuseMemoилиuseCallback, убедитесь, что возвращаемая функция создается только один раз. То есть функция не будет создаваться дважды на основе изменений в зависимом массиве. Например:

export const useCount = () => {
  const [count, setCount] = useState(0);

  const [increase, decrease] = useMemo(() => {
    const increase = () => {
      setCount(count + 1);
    };

    const decrease = () => {
      setCount(count - 1);
    };
    return [increase, decrease];
  }, [count]);

  return [count, increase, decrease];
};

существуетuseCountВ Хуке,countИзменение состояния сделаетuseMemoсерединаincreaseа такжеdecrease函数被重新创建。由于闭包特性,如果这两个函数被其他 Hook 用到了,我们应该将这两个函数也添加到相应 Hook 的依赖数组中,否则就会产生 bug。 Например:

function Counter() {
  const [count, increase] = useCount();

  useEffect(() => {
    const handleClick = () => {
      increase(); // 执行后 count 的值永远都是 1
    };

    document.body.addEventListener("click", handleClick);
    return () => {
      document.body.removeEventListener("click", handleClick);
    };
  }, []); 

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

существуетuseCountсередина,increaseпоследуетcountизменения воссоздаются. ноincreaseПосле воссоздания,useEffectне будет выполняться снова, поэтомуuseEffectвзято изincreaseвсегда при первом созданииincrease. и при первом созданииcountравно 0, поэтому независимо от того, сколько раз вы нажимаете,countвсегда 1.

этоincreaseфункционировать вuseEffectБыло бы неплохо иметь зависимости в массиве? На самом деле, это создает больше проблем:

  • increaseИзменения приведут к частому связыванию прослушивателей событий и выпуску прослушивателей событий.
  • Требуется выполнить только один раз, когда компонент смонтирован.useEffect,ноincreaseизменения приведут кuseEffectВыполнено много раз, не может удовлетворить спрос.

Как решить эти проблемы?

  1. пройти черезsetStateОбратные вызовы, чтобы функции не зависели от внешних переменных. Например:
export const useCount = () => {
  const [count, setCount] = useState(0);

  const [increase, decrease] = useMemo(() => {
    const increase = () => {
      setCount((latestCount) => latestCount + 1);
    };

    const decrease = () => {
      setCount((latestCount) => latestCount - 1);
    };
    return [increase, decrease];
  }, []); // 保持依赖数组为空,这样 increase 和 decrease 方法都只会被创建一次

  return [count, increase, decrease];
};
  1. пройти черезrefдля хранения изменяемых переменных. Например:
export const useCount = () => {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  countRef.current = count;

  const [increase, decrease] = useMemo(() => {
    const increase = () => {
      setCount(countRef.current + 1);
    };

    const decrease = () => {
      setCount(countRef.current - 1);
    };
    return [increase, decrease];
  }, []); // 保持依赖数组为空,这样 increase 和 decrease 方法都只会被创建一次

  return [count, increase, decrease];
};

наконец

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

  1. Разделите совершенно не связанные между собой состояния на группы состояний.
  2. Если определенные состояния взаимосвязаны или должны изменяться вместе, их можно объединить в набор состояний.
  3. Массивы зависимостей не должны иметь более 3-х значений, иначе код будет сложно поддерживать.
  4. Если мы обнаружим, что зависимый массив зависит от слишком большого количества значений, мы должны принять некоторые меры, чтобы уменьшить его.
    • Удалите ненужные зависимости.
    • Разделите хуки на более мелкие единицы, каждый хук зависит от своего собственного массива зависимостей.
    • Объедините несколько зависимых значений в одно, объединив связанные состояния.
    • пройти черезsetStateФункция обратного вызова получает последнее состояние, чтобы уменьшить внешние зависимости.
    • пройти черезrefчтобы прочитать значение изменяемой переменной, но будьте осторожны, чтобы контролировать, как оно изменяется.
  5. следует использоватьuseMemoСценарий:
    • сохранить ссылки равными
    • дорогой расчет
  6. Нет необходимости использоватьuseMemoСценарий:
    • Если возвращаемое значение является исходным значением:string, boolean, null, undefined, number, symbol(за исключением динамически объявленных символов), как правило, не нужно использоватьuseMemo.
    • Объекты, массивы, функции и т. д. используются только внутри компонента (не передаются дочерним компонентам в качестве реквизита) и не используются в массивах зависимостей других хуков, как правило, их не нужно использовать.useMemo.
  7. Хуки, Render Props и высокоуровневые компоненты имеют свои собственные сценарии использования, и какой из них использовать, зависит от реальной ситуации.
  8. Если типы хуков одинаковы и зависимые массивы одинаковы, их следует объединить в один хук.
  9. Возвращаемое значение пользовательских хуков может использовать тип Tuple, который легче переименовать извне. Не рекомендуется, если возвращается слишком много значений.
  10. refНе подвергайте непосредственно внешнему использованию, но предоставьте метод для изменения значения.
  11. В использованииuseMemoилиuseCallback, вы можете использоватьrefилиsetStateобратный вызов, который гарантирует, что возвращаемая функция создается только один раз. То есть функция не будет создаваться дважды на основе изменений в зависимом массиве.

Справочная статья:

Вы злоупотребляете useMemo: переосмысление мемоизации хуков