предисловие
Оригинальная ссылка:7 code smells in your React components
Информация об авторе:Anton Gunnarsson
Разрешение на перевод:
текст
с момента использованияReact
Позже я увидел все больше и больше моментов, которые стоит оптимизировать, например:
- Много
props
-
props
несовместимость -
props
копировать какstate
- вернуть
JSX
Функция -
state
несколько состояний -
useState
излишний - сложный
useEffect
В этой статье я хочу поделиться несколькими советами, которые улучшат ваш код React.
много реквизита
Если вам нужно положить многоprops
в компонент, то есть большая вероятность, что компонент можно будет разделить дальше.
Вопрос в том, сколько это "много"? Ответ - это зависит.
Предположим, вы разрабатываете программу, содержащую 20 или болееprops
компонент, когда вы хотите добавить еще несколькоprops
Чтобы улучшить другие функции, в настоящее время есть два момента, на которые следует указать, следует ли разделять компоненты:
Компонент делает больше, чем одну вещь?
Как и функции, компонент должен хорошо выполнять только одну функцию, поэтому подумайте, не лучше ли разделить компонент на несколько виджетов.
Например, компонент существуетprops
несовместимостьиливернутьJSX
Функция.
Можно ли синтезировать компонент?
В разработке композиция — это отличный образец, который часто упускают из виду.
Если у вас есть компоненты, в которых несвязанная логика переполнена, пришло время подумать об использовании композиции.
Предположим, у нас есть компонент формы, который обрабатывает информацию о пользователях для организации:
<ApplicationForm
user={userData}
organization={organizationData}
categories={categoriesData}
locations={locationsData}
onSubmit={handleSubmit}
onCancel={handleCancel}
...
/>
Через этот компонентprops
, мы видим, что все они тесно связаны с функциональностью, предоставляемой компонентом.
Компонент выглядит хорошо, но если некоторые изprops
Поделившись им с подкомпонентами, поток данных станет более четким.
<ApplicationForm onSubmit={handleSubmit} onCancel={handleCancel}>
<ApplicationUserForm user={userData} />
<ApplicationOrganizationForm organization={organizationData} />
<ApplicationCategoryForm categories={categoriesData} />
<ApplicationLocationsForm locations={locationsData} />
</ApplicationForm>
Теперь мы увидели, что компонент формы обрабатывает только действия отправки и отмены, а другие вещи в области действия передаются соответствующим подкомпонентам.
Есть ли что рассказать о конфигурацииprops
В некоторых случаях несколько связанных конфигурацийprops
объединены в одинoptions
это хорошая практика.
Предположим, у нас есть компонент, который отображает какую-то таблицу:
<Grid
data={gridData}
pagination={false}
autoSize={true}
enableSort={true}
sortOrder="desc"
disableSelection={true}
infiniteScroll={true}
...
/>
Хорошо видно, что эта компонента в дополнение кdata
остальныеprops
Все дело в конфигурации.
Если вы настраиваете несколькоprops
синтезируется вoptions
, вы можете лучше контролировать параметры компонента, а также улучшена спецификация.
const options = {
pagination: false,
autoSize: true,
enableSort: true,
sortOrder: 'desc',
disableSelection: true,
infiniteScroll: true,
...
}
<Grid
data={gridData}
options={options}
/>
Несовместимость реквизита
Избегайте передачи несовместимых компонентов между компонентамиprops
.
Предположим, у вас есть библиотека компонентов в<Input />
компонент, который начинается только для текста, но через некоторое время вы используете его для телефонных номеров.
Ваша реализация может выглядеть так:
function Input({ value, isPhoneNumberInput, autoCapitalize }) {
if (autoCapitalize) capitalize(value)
return <input value={value} type={isPhoneNumberInput ? 'tel' : 'text'} />
}
Проблема в,isPhoneNumberInput
а такжеautoCapitalize
Между ними нет никакой связи, и делать первую букву номера телефона заглавной не имеет никакого смысла.
В этом случае мы можем разделить его на несколько небольших компонентов, чтобы прояснить конкретные обязанности, и, если есть общая логика, его можно поместить вhooks
середина.
function TextInput({ value, autoCapitalize }) {
if (autoCapitalize) capitalize(value)
useSharedInputLogic()
return <input value={value} type="text" />
}
function PhoneNumberInput({ value }) {
useSharedInputLogic()
return <input value={value} type="tel" />
}
Хотя приведенный выше пример немного неохотный, когда вы обнаружите, что компонентprops
Когда существуют несовместимости, пришло время подумать о разделении компонентов.
реквизит копируется как состояние
как лучшеprops
так какstate
начальное значение .
Имеются следующие компоненты:
function Button({ text }) {
const [buttonText] = useState(text)
return <button>{buttonText}</button>
}
Этот компонент будетtext
так какuseState
Начальное значение , может привести к неожиданному поведению.
На самом деле компонент выключенprops
обновить уведомление, еслиtext
обновляется в верхнем слое, и он по-прежнему будет отображать полученныеtext
Первое значение , которое более подвержено ошибкам в компоненте.
Более практичный сценарий заключается в том, что мы хотимprops
получить новыйstate
.
В приведенном ниже примереslowlyFormatText
функция для форматированияtext
, обратите внимание, что это занимает много времени.
function Button({ text }) {
const [formattedText] = useState(() => slowlyFormatText(text))
return <button>{formattedText}</button>
}
Лучшим решением этой проблемы является использованиеuseMemo
заменятьuseState
.
function Button({ text }) {
const formattedText = useMemo(() => slowlyFormatText(text), [text])
return <button>{formattedText}</button>
}
СейчасslowFormatFormat
только приtext
Запускается при внесении изменений и не блокирует обновления компонентов верхнего уровня.
Дальнейшее чтение:Writing resilient components by Dan Abramov.
Функции, возвращающие JSX
Не возвращайтесь из функции внутри компонентаJSX
.
Этот паттерн встречается редко, но я сталкиваюсь с ним время от времени.
Всего один пример для иллюстрации:
function Component() {
const topSection = () => {
return (
<header>
<h1>Component header</h1>
</header>
)
}
const middleSection = () => {
return (
<main>
<p>Some text</p>
</main>
)
}
const bottomSection = () => {
return (
<footer>
<p>Some footer text</p>
</footer>
)
}
return (
<div>
{topSection()}
{middleSection()}
{bottomSection()}
</div>
)
}
Хотя этот пример выглядит нормально, на самом деле он нарушает целостность кода и затрудняет обслуживание.
или вернуть функциюJSX
Встроить непосредственно в компонент или разделить его на компонент.
Следует отметить одну вещь: если вы создаете новый компонент, вам не нужно перемещать его в новый файл.
Если несколько компонентов тесно связаны, имеет смысл хранить их в одном файле.
несколько состояний состояния
Избегайте использования нескольких логических значений для представления состояния компонента.
При написании компонента и многократных итерациях легко получить ситуацию, когда внутри есть несколько логических значений, указывающих, в каком состоянии находится компонент.
Например следующий пример:
function Component() {
const [isLoading, setIsLoading] = useState(false)
const [isFinished, setIsFinished] = useState(false)
const [hasError, setHasError] = useState(false)
const fetchSomething = () => {
setIsLoading(true)
fetch(url)
.then(() => {
setIsLoading(false)
setIsFinished(true)
})
.catch(() => {
setHasError(true)
})
}
if (isLoading) return <Loader />
if (hasError) return <Error />
if (isFinished) return <Success />
return <button onClick={fetchSomething} />
}
При нажатии на кнопку мыisLoading
Установить какtrue
, и пройтиfetch
Выполнять сетевые запросы.
Если запрос будет успешным, мыisLoading
Установить какfalse
,isFinished
Установить какtrue
, если есть ошибка, тоhasError
Установить какtrue
.
Хотя это технически возможно, трудно сделать вывод о том, в каком состоянии находится компонент, и непросто в обслуживании.
И можно оказаться в «невозможных состояниях», например, когда мы случайно поместилиisLoading
а такжеisFinished
Установить какtrue
.
Решение этой проблемы раз и навсегда заключается в использовании перечислений для управления состоянием.
В других языках перечисление — это способ определения переменной, которая может быть установлена только в предопределенный набор постоянных значений, хотя вJavaScript
Перечисления не существуют в , но мы можем использовать строки как перечисления:
function Component() {
const [state, setState] = useState('idle')
const fetchSomething = () => {
setState('loading')
fetch(url)
.then(() => {
setState('finished')
})
.catch(() => {
setState('error')
})
}
if (state === 'loading') return <Loader />
if (state === 'error') return <Error />
if (state === 'finished') return <Success />
return <button onClick={fetchSomething} />
}
Таким образом, невозможные состояния полностью исключаются, а расширения используются шире.
если вы используетеTypeScript
Для разработки перечисление может быть реализовано с момента определения:
const [state, setState] = useState<'idle' | 'loading' | 'error' | 'finished'>('idle')
слишком много useState
Избегайте использования слишком большого количества компонентов в одном компоненте.useState
.
один содержит многоuseState
Компонент может выполнять более одной функции, рассмотрите возможность его разделения.
Конечно, есть и сложные сценарии, и нам нужно управлять некоторыми сложными состояниями в компоненте.
Ниже приведен пример компонента автоматического ввода:
function AutocompleteInput() {
const [isOpen, setIsOpen] = useState(false)
const [inputValue, setInputValue] = useState('')
const [items, setItems] = useState([])
const [selectedItem, setSelectedItem] = useState(null)
const [activeIndex, setActiveIndex] = useState(-1)
const reset = () => {
setIsOpen(false)
setInputValue('')
setItems([])
setSelectedItem(null)
setActiveIndex(-1)
}
const selectItem = (item) => {
setIsOpen(false)
setInputValue(item.name)
setSelectedItem(item)
}
...
}
у нас естьreset
функция, которая сбрасывает все состояния, иselectItem
функция для обновления некоторого состояния.
Эти функции неотделимыuseState
определенное состояние. Если функция продолжает итерацию, будет все больше и больше функций, состояние увеличится с ним, и поток данных станет размытым.
В этом случае используйтеuseReducer
вместо слишком многоuseState
хороший выбор.
const initialState = {
isOpen: false,
inputValue: "",
items: [],
selectedItem: null,
activeIndex: -1
}
function reducer(state, action) {
switch (action.type) {
case "reset":
return {
...initialState
}
case "selectItem":
return {
...state,
isOpen: false,
inputValue: action.payload.name,
selectedItem: action.payload
}
default:
throw Error()
}
}
function AutocompleteInput() {
const [state, dispatch] = useReducer(reducer, initialState)
const reset = () => {
dispatch({ type: 'reset' })
}
const selectItem = (item) => {
dispatch({ type: 'selectItem', payload: item })
}
...
}
используяreducer
, мы инкапсулируем логику управления состоянием и выносим сложную логику из компонента, что упрощает его обслуживание.
Дальнейшее чтение:state reducer pattern by Kent C. Dodds.
Комплексное использованиеЭффект
избегать вuseEffect
Они делают код подверженным ошибкам и трудным для понимания.
Следующий пример делает большую ошибку:
function Post({ id, unlisted }) {
...
useEffect(() => {
fetch(`/posts/${id}`).then(/* do something */)
setVisibility(unlisted)
}, [id, unlisted])
...
}
когдаunlisted
меняться, даже еслиid
Без изменений, тоже позвонюfetch
.
Правильный способ написать это должен состоять в том, чтобы разделить несколько зависимостей:
function Post({ id, unlisted }) {
...
useEffect(() => { // when id changes fetch the post
fetch(`/posts/${id}`).then(/* ... */)
}, [id])
useEffect(() => { // when unlisted changes update visibility
setVisibility(unlisted)
}, [unlisted])
...
}
заключительные замечания
Это все, что я разделяю. Помните, это ни в коем случае не правила, а скорее указание на то, что что-то может быть «не так».
Если вы обнаружите другие проблемные шаблоны, не стесняйтесь комментировать или наTwitterСвяжитесь со мной по.