6 главных ошибок React Hook, которые допускают новички
Самая сложная часть изучения React — это не научиться использовать React, а научиться писать хороший и чистый код React.
В этой статье я расскажу о 6 ошибках, которые, как я вижу, допускают почти все, используя хуки useState
и useEffect
.
Ошибка 1. Использование состояния, когда оно вам не нужно.
Самая первая ошибка, о которой я хочу поговорить, — это использование состояния, когда оно вам на самом деле не нужно.
Давайте посмотрим на этот пример.
import {useState} from "react";
const App = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
function onSubmit(e) {
e.preventDefault();
console.log({ email, password });
}
return (
<form onSubmit={onSubmit}>
<label htmlFor="email">Email</label>
<input
value={email}
onChange={e => setEmail(e.target.value)}
type="email"
id="email"
/>
<label htmlFor="password">Password</label>
<input
value={password}
onChange={e => setPassword(e.target.value)}
type="password"
id="password"
/>
<button type="submit">Submit</button>
</form>
)
};
export default App;
Здесь мы используем состояние электронной почты и пароля, но проблема в том, что при отправке формы нам важны только значения адреса электронной почты и пароля, нам не требуется повторная отрисовка при обновлении электронной почты и пароля, поэтому вместо отслеживания состояния и повторной отрисовки каждый раз, когда я печатаю символ, я собираюсь сохранить его внутри ссылки.
import { useRef } from "react";
const App = () => {
const emailRef = useRef();
const passwordRef = useRef();
function onSubmit(e) {
e.preventDefault();
console.log({
email: emailRef.current.value,
password: passwordRef.current.value
});
}
return (
<form onSubmit={onSubmit}>
<label htmlFor="email">Email</label>
<input
ref={emailRef}
type="email"
id="email"
/>
<label htmlFor="password">Password</label>
<input
ref={passwordRef}
type="password"
id="password"
/>
<button type="submit">Submit</button>
</form>
)
};
export default App;
Если вы нажмете кнопку «Отправить» и снова посмотрите на консоль, она напечатает значения. Для этого вообще не нужно никакого государства.
Итак, первый совет для вас — подумать, действительно ли вам нужно использовать состояния и перерисовывать компоненты каждый раз, когда состояние изменяется, или вы можете использовать ссылку, если вам не нужно перерисовывать компоненты.
Также вы можете получить доступ к данным в форме напрямую, поэтому вам не нужны ссылки.
import { useRef } from "react";
const App = () => {
function onSubmit(event) {
event.preventDefault();
const data = new FormData(event.target);
console.log(data.get('email'));
console.log(data.get('password'));
fetch('/api/form-submit-url', {
method: 'POST',
body: data,
});
}
return (
<form onSubmit={onSubmit}>
<label htmlFor="email">Email</label>
<input
type="email"
name="email"
id="email"
/>
<label htmlFor="password">Password</label>
<input
type="password"
name="password"
id="password"
/>
<button type="submit">Submit</button>
</form>
)
};
export default App;
Ошибка 2. Неиспользование функциональной версии useState.
Давайте посмотрим на этот пример.
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
function adjustCount(amount) {
setCount(count + amount);
setCount(count + amount);
}
return (
<>
<button onClick={ () => adjustCount(-1) }> - </button>
<span> {count} </span>
<button onClick={ () => adjustCount(1) }> + </button>
</>
)
}
Если вы нажмете кнопку, счетчик обновится только один раз, даже если вы установитеCount дважды. Это связано с тем, что когда второй триггер setCount
, а первый setCount
не завершен, значение счетчика не обновляется.
Чтобы решить эту проблему, вам следует использовать версию функции.
https://legacy.reactjs.org/docs/hooks-reference.html#functional-updates
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
function adjustCount(amount) {
setCount(prevCount => prevCount + amount);
setCount(prevCount => prevCount + amount);
}
return (
<>
<button onClick={ () => adjustCount(-1) }> - </button>
<span> {count} </span>
<button onClick={ () => adjustCount(1) }> + </button>
</>
)
}
Ошибка 3. Государственная доза не обновляется немедленно.
Давайте посмотрим на этот пример.
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
function adjustCount(amount) {
setCount(prevCount => prevCount + amount);
// count is the value before setCount
console.log(count);
}
return (
<>
<button onClick={ () => adjustCount(-1) }> - </button>
<span> {count} </span>
<button onClick={ () => adjustCount(1) }> + </button>
</>
)
}
Когда вы обновляете свою переменную состояния, она на самом деле не меняется сразу, она не меняется до следующего рендеринга, поэтому вместо размещения кода после установщика состояния вам следует использовать useEffect.
import {useEffect, useState} from "react";
export function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
}, [count])
function adjustCount(amount) {
setCount(prevCount => prevCount + amount);
}
return (
<>
<button onClick={ () => adjustCount(-1) }> - </button>
<span> {count} </span>
<button onClick={ () => adjustCount(1) }> + </button>
</>
)
}
Ошибка 4. Ненужное использование. useEffect
Давайте посмотрим на этот пример.
import { useEffect, useState } from "react";
const App = () => {
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName])
return (
<>
<input value={firstName} onChange={ e => setFirstName(e.target.value) } />
<input value={lastName} onChange={ e => setLastName(e.target.value) } />
{fullName}
</>
)
};
export default App;
Проблема в том, что когда мы обновляем firstName
или LastName
, состояние FullName
будет обновляться и повторно визуализировать компонент снова, он будет перерисовываться дважды.
Как сделать так, чтобы было оптимально.
import { useState } from "react";
const App = () => {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
return (
<>
<input value={firstName} onChange={ e => setFirstName(e.target.value) } />
<input value={lastName} onChange={ e => setLastName(e.target.value) } />
{fullName}
</>
)
};
export default App;
Ошибка 5. Ошибки ссылочного равенства
Давайте посмотрим на этот пример.
import {useEffect, useState} from "react";
const App = () => {
const [age, setAge] = useState(0);
const [name, setName] = useState('');
const [darkMode, setDarkMode] = useState(false);
const person = { age, name };
useEffect(() => {
console.log(person)
}, [person])
return (
<div style={{ background: darkMode ? "#333" : "#fff" }}>
Age: {" "}
<input value={age} type="number" onChange={ e => setAge(e.target.value)} />
<br/>
Name: <input value={name} onChange={ e => setName(e.target.value) } />
<br/>
Dark Mode: {" "}
<input type="checkbox" value={darkMode} onChange={e => setDarkMode(e.target.checked)} />
</div>
)
};
export default App;
Если вы измените возраст или имя, функция useEffect
будет активирована, но если вы переключите darkMode
, функция useEffect
также сработает, это не то, что мы ожидали.
Есть два ключевых момента, чтобы это произошло.
Во-первых, каждый рендер имеет свои собственные реквизиты и состояние, поэтому переменная person будет инициализирована как новая. Ознакомьтесь с этой статьей, если хотите изучить это подробно, https://overreacted.io/a-complete-guide-to-useeffect/
Второй момент — ссылочное равенство, https://barker.codes/blog/referential-equality-in-javascript/.
поэтому переменная person не равна при обновлении darkMode
.
Мы можем использовать useMemo, чтобы решить эту проблему.
import {useEffect, useMemo, useState} from "react";
const App = () => {
const [age, setAge] = useState(0);
const [name, setName] = useState('');
const [darkMode, setDarkMode] = useState(false);
const person = useMemo(() => {
return { age, name }
}, [age, name])
useEffect(() => {
console.log(person)
}, [person])
return (
<div style={{ background: darkMode ? "#333" : "#fff" }}>
Age: {" "}
<input value={age} type="number" onChange={ e => setAge(e.target.value)} />
<br/>
Name: <input value={name} onChange={ e => setName(e.target.value) } />
<br/>
Dark Mode: {" "}
<input type="checkbox" value={darkMode} onChange={e => setDarkMode(e.target.checked)} />
</div>
)
};
export default App;
Ошибка 6. Не прерывание запросов на выборку
Давайте посмотрим на этот пример.
import {useEffect, useState} from "react";
export function useFetch(url) {
const [loading, setLoading] = useState(true);
const [data, setData] = useState();
const [error, setError] = useState();
useEffect(() => {
setLoading(true)
fetch(url)
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [url])
}
Проблема в этом примере заключается в том, что когда компонент размонтирован или URL-адрес изменен, предыдущая выборка все еще работает в фоновом режиме, во многих ситуациях она будет работать не так, как ожидалось, подробно ознакомьтесь с этой статьей, https://plainenglish.io/community/how-to-cancel-fetch-and-axios-requests-in-react-useeffect-hook
Мы можем решить эту проблему с помощью AbortController
.
import {useEffect, useState} from "react";
export function useFetch(url) {
const [loading, setLoading] = useState(true);
const [data, setData] = useState();
const [error, setError] = useState();
useEffect(() => {
const controller = new AbortController();
setLoading(true)
fetch(url, { signal: controller.signal })
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
return () => {
controller.abort();
}
}, [url])
}