Я использую 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
Разделение на две переменные состояния кажется излишним.
Прежде чем использовать состояние, нам нужно рассмотреть «зернистость» разделения состояния. Если степень детализации слишком мала, код становится избыточным. Если степень детализации слишком грубая, возможность повторного использования кода будет снижена. Итак, какие штаты следует объединить, а какие разделить? Я суммировал следующие два момента:
- Разделите совершенно не связанные между собой состояния на группы состояний. Например
size
а такжеposition
. - Если определенные состояния взаимосвязаны или должны изменяться вместе, их можно объединить в набор состояний. Например
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
, но предоставьте некоторые способы его изменения.
Сказав так много, все сводится к написанию более чистого и простого в обслуживании кода. Если мы обнаружим, что массив зависимостей слишком зависим, нам нужно пересмотреть наш код:
- Массивы зависимостей не должны иметь более 3-х значений, иначе код будет сложно поддерживать.
- Если мы обнаружим, что зависимый массив зависит от слишком большого количества значений, мы должны принять некоторые меры, чтобы уменьшить его.
- Удалите ненужные зависимости.
- Разделите хуки на более мелкие единицы, каждый хук зависит от своего собственного массива зависимостей.
- Объедините несколько зависимых значений в одно, объединив связанные состояния.
- пройти через
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
Перед этим мы должны подумать над двумя вопросами:
- Перейти к
useMemo
Функция дорого стоит? В приведенном выше примере рассмотримgetResolvedValue
Накладные расходы функции не велики. Большинство методов в JS оптимизированы, напримерArray.map
,Array.forEach
Ждать. Если вы выполняете недорогую операцию, вам не нужно запоминать возвращаемое значение. В противном случае используйтеuseMemo
Стоимость сама по себе может перевешивать стоимость пересчета этого значения. Поэтому для некоторых простых операций JS нам не нужно использоватьuseMemo
чтобы «запомнить» его возвращаемое значение. - Изменяется ли ссылка на «запомненное» значение, когда ввод тот же? В приведенном выше примере, когда
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
Перед этим давайте зададим себе несколько вопросов:
- Дорого стоит функция запомнить?
- Является ли возвращаемое значение исходным значением?
- Будет ли запомненное значение использоваться другими хуками или подкомпонентами?
Ответьте на приведенные выше вопросы и решите, следует ли использоватьuseMemo
Это уже не сложно. Однако в реальных проектах лучше определить единый набор спецификаций, чтобы облегчить совместную работу нескольких человек в команде. Например, в первом вопросе, как вы определяете много накладных расходов? Если не будет четких стандартов, их будет очень трудно обеспечить. Поэтому я резюмирую следующие правила:
1. Следует использоватьuseMemo
сцена
- сохранить ссылки равными
- Для объектов, массивов, функций и т. д., используемых внутри компонентов, если они используются в массивах зависимостей других хуков или передаются нижестоящим компонентам в качестве реквизита, вы должны использовать
useMemo
. - Следует использовать объекты, массивы, функции и т. д., представленные в пользовательских хуках.
useMemo
. чтобы гарантировать, что ссылка не изменится, когда значение будет тем же самым. - использовать
Context
когда, еслиProvider
Значение, определенное в значении (первого уровня), изменилось даже при использовании Pure Component илиReact.memo
, все равно вызовет повторный рендеринг дочернего компонента. В этом случае все же рекомендуется использоватьuseMemo
Сохраняйте цитаты последовательными.
- Для объектов, массивов, функций и т. д., используемых внутри компонентов, если они используются в массивах зависимостей других хуков или передаются нижестоящим компонентам в качестве реквизита, вы должны использовать
- Дорогой расчет
- Например
cloneDeep
очень большие и глубоко иерархические данные
- Например
2. Сценарии без использования Memo
- Если возвращаемое значение является исходным значением:
string
,boolean
,null
,undefined
,number
,symbol
(за исключением динамически объявленных символов), как правило, не нужно использоватьuseMemo
. - Объекты, массивы, функции и т. д. используются только внутри компонента (не передаются дочерним компонентам в качестве реквизита) и не используются в массивах зависимостей других хуков, как правило, их не нужно использовать.
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
Пока не поддерживается. - Извлечь логику мультиплексирования. Хуки можно использовать и в других сценариях, за исключением тех, которые имеют четкие отношения родитель-потомок.
- Заменяет Class для большинства случаев использования, за исключением
- Реквизиты рендеринга: он имеет более высокую степень свободы в рендеринге компонентов и может выполнять динамический рендеринг в соответствии с данными, предоставленными родительским компонентом. Это подходит для сценариев, где есть четкие отношения родитель-потомок.
- Компоненты более высокого порядка: подходят для инъекций и генерируют новый многоразовый компонент. Подходит для написания плагинов.
Тем не менее, сцены, которые могут использовать хуки, все равно должны сначала использовать хуки, а затем 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
Выполнено много раз, не может удовлетворить спрос.
Как решить эти проблемы?
- пройти через
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];
};
- пройти через
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];
};
наконец
Мы суммируем некоторые общие проблемы на практике и предлагаем некоторые решения. Наконец, давайте резюмируем:
- Разделите совершенно не связанные между собой состояния на группы состояний.
- Если определенные состояния взаимосвязаны или должны изменяться вместе, их можно объединить в набор состояний.
- Массивы зависимостей не должны иметь более 3-х значений, иначе код будет сложно поддерживать.
- Если мы обнаружим, что зависимый массив зависит от слишком большого количества значений, мы должны принять некоторые меры, чтобы уменьшить его.
- Удалите ненужные зависимости.
- Разделите хуки на более мелкие единицы, каждый хук зависит от своего собственного массива зависимостей.
- Объедините несколько зависимых значений в одно, объединив связанные состояния.
- пройти через
setState
Функция обратного вызова получает последнее состояние, чтобы уменьшить внешние зависимости. - пройти через
ref
чтобы прочитать значение изменяемой переменной, но будьте осторожны, чтобы контролировать, как оно изменяется.
- следует использовать
useMemo
Сценарий:- сохранить ссылки равными
- дорогой расчет
- Нет необходимости использовать
useMemo
Сценарий:- Если возвращаемое значение является исходным значением:
string
,boolean
,null
,undefined
,number
,symbol
(за исключением динамически объявленных символов), как правило, не нужно использоватьuseMemo
. - Объекты, массивы, функции и т. д. используются только внутри компонента (не передаются дочерним компонентам в качестве реквизита) и не используются в массивах зависимостей других хуков, как правило, их не нужно использовать.
useMemo
.
- Если возвращаемое значение является исходным значением:
- Хуки, Render Props и высокоуровневые компоненты имеют свои собственные сценарии использования, и какой из них использовать, зависит от реальной ситуации.
- Если типы хуков одинаковы и зависимые массивы одинаковы, их следует объединить в один хук.
- Возвращаемое значение пользовательских хуков может использовать тип Tuple, который легче переименовать извне. Не рекомендуется, если возвращается слишком много значений.
-
ref
Не подвергайте непосредственно внешнему использованию, но предоставьте метод для изменения значения. - В использовании
useMemo
илиuseCallback
, вы можете использоватьref
илиsetState
обратный вызов, который гарантирует, что возвращаемая функция создается только один раз. То есть функция не будет создаваться дважды на основе изменений в зависимом массиве.
Справочная статья: