ahooksАвтора меня следует считать очень и очень опытным пользователем React Hooks. В процессе использования React Hooks более двух лет я все чаще обнаруживал, что у всех (в том числе и у меня) возникает множество неверных представлений об использовании React Hooks. строгие и есть неправильные руководящие принципы.
1. Не все зависимости нужно помещать в массив зависимостей
Для всех пользователей React Hooks существует единое мнение: «Внешние переменные, используемые в useEffect, должны быть помещены во второй параметр массива», и мы установимeslint-plugin-react-hooksПлагин, чтобы напомнить себе, если вы забыли некоторые переменные.
Приведенный выше консенсус исходит из официального документа:
Я бы назвал это правило корнем всех зол, и оно подчеркнуто, и все новички воспринимают его всерьез, включая меня. Однако в реальной разработке обнаруживается, что это не так.
Ниже приведен относительно простой пример, требования таковы: когдаprops.countа такжеcountПри изменении сообщайте все текущие данные.
Этот пример относительно прост, сначала вставьте исходный код:
function Demo(props) {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const [a, setA] = useState('');
useEffect(() => {
monitor(props.count, count, text, a);
}, [props.count, count]);
return (
<div>
<button
onClick={() => setCount(c => c + 1)}
>
click
</button>
<input value={text} onChange={e => setText(e.target.value)} />
<input value={a} onChange={e => setA(e.target.value)} />
</div>
)
}
Мы видим, что в примере кода useEffect не соответствует официальным рекомендациям React.textа такжеaПеременная не помещается в зависимый массив, ESLint предупреждает следующее:
Что если по спецификации мы поместим все зависимости во второй параметр массива?
useEffect(() => {
monitor(props.count, count, text, a);
}, [props.count, count, text, a]);
Хотя приведенный выше код соответствует официальной спецификации React, он не отвечает потребностям нашего бизнеса.textа такжеaКогда изменения также вызывают выполнение функции.
На данный момент он стоит перед дилеммой: когда спецификация использования useEffect соблюдается, бизнес-требования не могут быть выполнены. Когда бизнес-потребность удовлетворена, useEffect нестандартен.
Мое предложение:
- Не используйте
eslint-plugin-react-hooksплагин или, при желании, игнорировать предупреждения плагина. - Есть только один случай, когда вам нужно поместить переменную в массив deps, то есть, когда переменная изменяется, вам нужно вызвать функцию useEffect для выполнения. Не потому, что переменная используется в useEffect!
2. Параметр deps не решает проблему замыкания
Если код написан полностью в соответствии со вторым предложением, многие беспокоятся, не вызовет ли это каких-то ненужных проблем с закрытием? Мой вывод таков:Проблема закрытия не имеет ничего общего с параметром deps для useEffect.
Например, у меня есть такое требование: при входе на страницу на 3с выводить текущий последний счет. код показывает, как показано ниже:
function Demo() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
console.log(count)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
return (
<button
onClick={() => setCount(c => c + 1)}
>
click
</button>
)
}
Приведенный выше код после реализации инициализации в течение 3 с выводит значение count. Но, к сожалению, здесь обязательно будет проблема с закрытием, даже если мы много раз нажмем кнопку после входа, счетчик вывода все равно будет равен 0.
Тогда, если мы положимcountПоместите это в депс, разве это не нормально?
useEffect(() => {
const timer = setTimeout(() => {
console.log(count)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [count])
Как и в приведенном выше коде, в настоящее время действительно нет проблемы с закрытием, но каждый разcountПри изменении таймер выгружается и запускается заново, не удовлетворяя нашим первоначальным потребностям.
Единственное решение этого:
const [count, setCount] = useState(0);
// 通过 ref 来记忆最新的 count
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setTimeout(() => {
console.log(countRef.current)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
Хотя приведенный выше код очень запутан, это правда, что есть только это решение. Запомните этот код, он действительно мощный.
const countRef = useRef(count);
countRef.current = count;
Из приведенного выше примера видно, что проблемы замыкания нельзя избежать, просто следуя правилам React. Мы должны четко знать, в каких сценариях возникнет проблема закрытия.
2.1 В нормальных условиях проблем с закрытием не возникает
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const c = a + b;
useEffect(()=>{
console.log(a, b, c)
}, [a]);
useEffect(()=>{
console.log(a, b, c)
}, [b]);
useEffect(()=>{
console.log(a, b, c)
}, [c]);
При обычном использовании проблем с замыканием не будет, в приведенном выше коде вообще не будет проблем с замыканием, и это не имеет никакого отношения к написанию deps.
2.2 Отложенные вызовы будут иметь проблемы с закрытием
В случае отложенного вызова должна быть проблема с закрытием.Что такое отложенный вызов?
- Используйте setTimeout, setInterval, Promise.then и т. д.
- Удалить функцию useEffect
const getUsername = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('John');
}, 3000);
})
}
function Demo() {
const [count, setCount] = useState(0);
// setTimeout 会造成闭包问题
useEffect(() => {
const timer = setTimeout(() => {
console.log(count);
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
// setInterval 会造成闭包问题
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 3000);
return () => {
clearInterval(timer);
}
}, [])
// Promise.then 会造成闭包问题
useEffect(() => {
getUsername().then(() => {
console.log(count);
});
}, [])
// useEffect 卸载函数会造成闭包问题
useEffect(() => {
return () => {
console.log(count);
}
}, []);
return (
<button
onClick={() => setCount(c => c + 1)}
>
click
</button>
)
}
В приведенном выше примере кода проблема закрытия будет возникать во всех четырех случаях, и вывод всегда будет0. Основная причина этих четырех случаев одна и та же, давайте посмотрим на порядок выполнения кода:
- Инициализация компонента, в настоящее время
count = 0 - Выполнить useEffect, в это время выполняется функция useEffect, и цепочка ссылок JS записывает
count=0отношения цитирования - Жмем кнопку, счетчик меняется, но с предыдущей ссылкой ничего не поделаешь
Видно, что проблема с закрытием возникает в сценарии отложенного вызова. Решение выглядит следующим образом:
const [count, setCount] = useState(0);
// 通过 ref 来记忆最新的 count
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setTimeout(() => {
console.log(countRef.current)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
......
пройти черезuseRefобеспечить доступ в любое времяcountRef.currentОба обновлены для решения проблем с закрытием.
На данный момент я повторяю свое предложение для useEffect:
- Только при его изменении переменные, которые нужно повторно выполнить useEffect, должны быть помещены в deps. Вместо переменных, используемых useEffect, они помещаются в deps.
- Когда есть сценарий отложенного вызова, проблема закрытия может быть решена исх.
3. Старайтесь не использовать useCallback
Я рекомендую вам стараться не использовать useCallback в вашем проекте, так как в большинстве случаев это не только не улучшает производительность, но и сильно ухудшает читаемость кода.
3.1 useCallback не улучшает производительность в большинстве сценариев
useCallback может запомнить функцию и избежать повторного создания функции, чтобы при передаче функции дочернему компоненту можно было избежать повторного рендеринга дочернего компонента и повысить производительность.
const someFunc = useCallback(()=> {
doSomething();
}, []);
return <ExpensiveComponent func={someFunc} />
Основываясь на приведенных выше знаниях, многие студенты (включая меня) добавляют useCallback при написании кода, если это функция, а вы? Во всяком случае, я был.
Но мы должны отметить, что должно быть еще одно условие для повышения производительности, подкомпонент должен использоватьshouldComponentUpdateилиReact.memoповторить рендеринг, игнорируя те же параметры.
еслиExpensiveComponentКомпонент — это обычный компонент, и он бесполезен. Например следующее:
const ExpensiveComponent = ({ func }) => {
return (
<div onClick={func}>
hello
</div>
)
}
должен пройтиReact.memoпакетExpensiveComponent, позволит избежать повторного рендеринга, когда параметры остаются неизменными, и улучшит производительность.
const ExpensiveComponent = React.memo(({ func }) => {
return (
<div onClick={func}>
hello
</div>
)
})
Таким образом, useCallback следует сочетать сshouldComponentUpdate/React.memoВы используете его правильно? Конечно, я предлагаю не рассматривать оптимизацию производительности в общих проектах, то есть не использовать useCallback, если нет отдельных очень сложных компонентов, которые можно использовать поодиночке.
3.2 useCallback делает код менее читаемым
Я видел такой код после использования useCallback:
const someFuncA = useCallback((d, g, x, y)=> {
doSomething(a, b, c, d, g, x, y);
}, [a, b, c]);
const someFuncB = useCallback(()=> {
someFuncA(d, g, x, y);
}, [someFuncA, d, g, x, y]);
useEffect(()=>{
someFuncB();
}, [someFuncB]);
В приведенном выше коде переменные передаются слой за слоем, и сложно определить, какие изменения переменных вызовут выполнение useEffect.
Я ожидаю, что не буду использовать useCallback, просто напишу функцию напрямую:
const someFuncA = (d, g, x, y)=> {
doSomething(a, b, c, d, g, x, y);
};
const someFuncB = ()=> {
someFuncA(d, g, x, y);
};
useEffect(()=>{
someFuncB();
}, [...]);
В сценарии, где useEffect имеет отложенные вызовы, это может вызвать проблемы с закрытием, которые можно решить с помощью нашего универсального метода:
const someFuncA = (d, g, x, y)=> {
doSomething(a, b, c, d, g, x, y);
};
const someFuncB = ()=> {
someFuncA(d, g, x, y);
};
+ const someFuncBRef = useRef(someFuncB);
+ someFuncBRef.current = someFuncB;
useEffect(()=>{
+ setTimeout(()=>{
+ someFuncBRef.current();
+ }, 1000)
}, [...]);
Предложение для useCallback состоит всего из одного предложения: не используйте useCallback, если вам нечего делать.
4. useMemo рекомендует правильное использование
По сравнению с useCallback преимущества useMemo очевидны.
// 没有使用 useMemo
const memoizedValue = computeExpensiveValue(a, b);
// 使用 useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Если useMemo не используется,computeExpensiveValueбудет выполняться каждый раз при рендеринге. если используетсяuseMemo,только вaа такжеbКогда изменение внесено, оно будет выполнено только один разcomputeExpensiveValue.
Этот счет должен быть рассчитан каждым.Поэтому я предлагаю использовать useMemo надлежащим образом.
Конечно, это не бесконтрольное использование, и useMemo может оказаться нерентабельным при расчете очень простых базовых типов.
const a = 1;
const b = 2;
const c = useMemo(()=> a + b, [a, b]);
Например, в приведенном выше примере, пожалуйста, рассчитайтеa+bпотребление? или записьa/bИ сравнитьa/bИзменится ли потребление?
очевидныйa+bПотребляйте меньше.
const a = 1;
const b = 2;
const c = a + b;
Вы можете рассчитать эту учетную запись самостоятельно, я предлагаю простой расчет базового типа, не используйте useMemo~
5. Правильная поза useState
useState следует считать одним из самых простых хуков, но при использовании есть много советов, которым нужно следовать, если строго следовать следующему коду, который удваивается.
5.1 Нет необходимости объявлять состояние отдельно, если оно может быть рассчитано по другим состояниям
Состояние не должно вычисляться напрямую из другого состояния/реквизита, иначе нет необходимости определять состояние.
const SomeComponent = (props) => {
const [source, setSource] = useState([
{type: 'done', value: 1},
{type: 'doing', value: 2},
])
const [doneSource, setDoneSource] = useState([])
const [doingSource, setDoingSource] = useState([])
useEffect(() => {
setDoingSource(source.filter(item => item.type === 'doing'))
setDoneSource(source.filter(item => item.type === 'done'))
}, [source])
return (
<div>
.....
</div>
)
}
В приведенном выше примере переменнаяdoneSourceа такжеdoingSourceчерез переменнуюsourceРассчитано, то не определяйтеdoneSourceа такжеdoingSource!
const SomeComponent = (props) => {
const [source, setSource] = useState([
{type: 'done', value: 1},
{type: 'doing', value: 2},
])
const doneSource = useMemo(()=> source.filter(item => item.type === 'done'), [source]);
const doingSource = useMemo(()=> source.filter(item => item.type === 'doing'), [source]);
return (
<div>
.....
</div>
)
}
Как правило, такие проблемы в проекте относительно малозаметны и передаются слой за слоем, что трудно сразу увидеть в Code Review. Если вы можете четко определить переменные, это полдела.
5.2 Убедитесь, что источник данных уникален
Одни и те же данные в проекте гарантированно хранятся только в одном месте.
Не существует одновременно в редуксе и не определяет хранилище состояний в компоненте.
Не иметь хранилища состояний, определенного как в родительском компоненте, так и в текущем компоненте.
Не существует одновременно и в URL-запросе, и в компоненте определяется хранилище состояний.
function SearchBox({ data }) {
const [searchKey, setSearchKey] = useState(getQuery('key'));
const handleSearchChange = e => {
const key = e.target.value;
setSearchKey(key);
history.push(`/movie-list?key=${key}`);
}
return (
<input
value={searchKey}
placeholder="Search..."
onChange={handleSearchChange}
/>
);
}
В приведенном выше примереsearchKeyХранится в двух местах, как в URL-запросе, так и в определении состояния. Его можно оптимизировать следующим образом:
function SearchBox({ data }) {
const searchKey = parse(localtion.search)?.key;
const handleSearchChange = e => {
const key = e.target.value;
history.push(`/movie-list?key=${key}`);
}
return (
<input
value={searchKey}
placeholder="Search..."
onChange={handleSearchChange}
/>
);
}
В реальной разработке проекта такие проблемы относительно неясны, и на них следует обращать внимание при написании кода.
5.3 правильное слияние useState
В проекте нет кода, написанного так:
const [firstName, setFirstName] = useState();
const [lastName, setLastName] = useState();
const [school, setSchool] = useState();
const [age, setAge] = useState();
const [address, setAddress] = useState();
const [weather, setWeather] = useState();
const [room, setRoom] = useState();
Во всяком случае, я написал это сначала, useState слишком мелко разбит, в результате чего в коде появляется большой кусок useState.
Я бы предложил, то есть одинаковые переменные могут быть объединены в одно состояние, читаемость кода будет много улучшаться:
const [userInfo, setUserInfo] = useState({
firstName,
lastName,
school,
age,
address
});
const [weather, setWeather] = useState();
const [room, setRoom] = useState();
Конечно, когда мы изменяем переменные таким образом, мы не должны забывать приносить старые поля, например, мы хотим изменить толькоfirstName:
setUserInfo(s=> ({
...s,
fristName,
}))
На самом деле, если это компонент класса React, состояние будет автоматически объединено:
this.setState({
firstName
})
Возможно ли такое использование в Hooks? На самом деле, возможно, мы сами можем инкапсулировать хуки, такие как ahooksuseSetState, который инкапсулирует аналогичную логику:
const [userInfo, setUserInfo] = useSetState({
firstName,
lastName,
school,
age,
address
});
// 自动合并
setUserInfo({
firstName
})
Я часто использую его в своих проектахuseSetStateзаменитьuseState, чтобы управлять сложными типами состояния, моя мама любит меня больше.
6. Резюме
Как опытный пользователь React Hooks, я ценю повышение эффективности, которое приносит React Hooks, поэтому в последние годы я полностью освоил хуки. В то же время я все больше чувствую, что React Hooks трудно контролировать, особенно с приходом concurrent режима React 18, я не знаю, какие ямы это принесет.
Наконец, вот три предложения:
- Вы можете использовать более продвинутые хуки, упакованные другими, для повышения эффективности, такие какahooksбиблиотека (хахаха
- Вы можете посмотреть исходный код хуков, упакованный другими, чтобы углубить свое понимание хуков React, таких какahooksбиблиотека (хахаха
- Вы можете обратить внимание на мой публичный аккаунт [Front-end Technology Bricks Home], я буду часто публиковать некоторые технические статьи, написанные мной, и пересылать некоторые статьи, которые я считаю лучшими, люблю вас (づ ̄3 ̄)づ╭❤~