От автора: в этом посте давайте рассмотрим несколько простых советов, которые помогут вам писать более чистые компоненты React и лучше масштабировать проект.
Избегайте передачи свойств c помощью оператора разделения
Давайте сначала начнем с антипаттерна, которого вам следует избегать. Если нет конкретной и обоснованной причины, чтобы сделать это, вы должны избегать прохождения свойств вниз дерева компонентов, используя оператор разделения, например, так: {…props}.
Такая передача свойств действительно ускоряет написание компонентов. Однако она также затрудняет выявление ошибок в коде. Вы теряете уверенность в своих компонентах, что затрудняет их рефакторинг, и в результате ошибки начнут появляться намного раньше.
Оберните параметры функций в объект
Если ваша функция принимает несколько параметров, рекомендуется заключить их в объект. Вот пример:
1 2 3 |
export const sampleFunction = ({ param1, param2, param3 }) => { console.log({ param1, param2, param3 }); } |
Такой способ написания функции дает несколько заметных преимуществ:
Вам больше не нужно беспокоиться о порядке передачи аргументов. Я делал такую ошибку несколько раз, когда передавал аргументы функции в неправильном порядке.
Для редакторов с настроенным IntelliSense вы получите автозаполнение для аргументов функции.
Для обработчиков событий используйте функции, возвращающие функции обработчика
Если вы знакомы с функциональным программированием, этот метод напоминает каррирование, поскольку вы заранее устанавливаете некоторые параметры. Давайте рассмотрим пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export default function SampleComponent({ onValueChange }) { const handleChange = (key) => { return (e) => onValueChange(key, e.target.value) } return ( <form> <input onChange={handleChange('name')} /> <input onChange={handleChange('email')} /> <input onChange={handleChange('phone')} /> </form> ) } |
Как видите, написав таким образом функции-обработчики, вы можете сохранить дерево компонентов более чистым.
Использйте map вместо if / else
Когда вам нужно отобразить различные элементы на основе пользовательской логики, я предлагаю использовать map вместо операторов if / else. Вот пример использования if / else:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const Student = ({ name }) => <p>Student name: {name}</p> const Teacher = ({ name }) => <p>Teacher name: {name}</p> const Guardian = ({ name }) => <p>Guardian name: {name}</p> export default function SampleComponent({ user }) { let Component = Student; if (user.type === 'teacher') { Component = Teacher } else if (user.type === 'guardian') { Component = Guardian } return ( <div> <Component name={user.name} /> </div> ) } |
А вот пример использования map:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import React from 'react' const Student = ({ name }) => <p>Student name: {name}</p> const Teacher = ({ name }) => <p>Teacher name: {name}</p> const Guardian = ({ name }) => <p>Guardian name: {name}</p> const COMPONENT_MAP = { student: Student, teacher: Teacher, guardian: Guardian } export default function SampleComponent({ user }) { const Component = COMPONENT_MAP[user.type] return ( <div> <Component name={user.name} /> </div> ) } |
Используя эту простую стратегию, ваши компоненты станут более декларативными и более простыми для понимания. Это также упрощает расширение логики и добавление в нее дополнительных элементов.
Хуки компонентов
Считаю этот паттерн полезным, если вы не злоупотребляете им. Вы можете обнаружить, что используете некоторые компоненты во всем своем приложении. Если им для работы требуется состояние, вы можете обернуть их хуком, обеспечивающим это состояние. Хорошими примерами таких компонентов являются всплывающие окна, всплывающие уведомления или простые модальные окна. Например, вот хук компонента для простого модального окна подтверждения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import ConfirmationDialog from 'components/global/ConfirmationDialog'; export default function useConfirmationDialog({ headerText, bodyText, confirmationButtonText, onConfirmClick, }) { const [isOpen, setIsOpen] = useState(false); const onOpen = () => { setIsOpen(true); }; const Dialog = useCallback( () => ( <ConfirmationDialog headerText={headerText} bodyText={bodyText} isOpen={isOpen} onConfirmClick={onConfirmClick} onCancelClick={() => setIsOpen(false)} confirmationButtonText={confirmationButtonText} /> ), [isOpen] ); return { Dialog, onOpen, }; } |
Затем вы можете использовать свой компонентный хук так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import React from "react"; import { useConfirmationDialog } from './useConfirmationDialog' function Client() { const { Dialog, onOpen } = useConfirmationDialog({ headerText: "Delete this record?", bodyText: "Are you sure you want delete this record? This cannot be undone.", confirmationButtonText: "Delete", onConfirmClick: handleDeleteConfirm, }); function handleDeleteConfirm() { //TODO: delete } const handleDeleteClick = () => { onOpen(); }; return ( <div> <Dialog /> <button onClick={handleDeleteClick} /> </div> ); } export default Client; |
Такое абстрагирование компонента избавляет вас от написания большого количества шаблонного кода управления состоянием.
Разделение компонентов
Следующие три совета относятся к разумному разделению компонентов. По моему опыту, сохранение небольших размеров компонентов — лучший способ сохранить управляемость проекта.
Используйте обертки
Если вы пытаетесь найти способ разделить свой большой компонент, посмотрите на функциональность, которую предоставляет каждый элемент вашего компонента. Некоторые элементы предназначены для обеспечения особой функциональности, например, обработчики перетаскивания.
Вот пример компонента, который реализует перетаскивание с помощью response-beautiful-dnd:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import React from 'react' import { DragDropContext, Droppable } from 'react-beautiful-dnd'; export default function DraggableSample() { function handleDragStart(result) { console.log({ result }); } function handleDragUpdate({ destination }) console.log({ destination }); } const handleDragEnd = ({ source, destination }) => { console.log({ source, destination }); }; return ( <div> <DragDropContext onDragEnd={handleDragEnd} onDragStart={handleDragStart} onDragUpdate={handleDragUpdate} > <Droppable droppableId="droppable" direction="horizontal"> {(provided) => ( <div {...provided.droppableProps} ref={provided.innerRef}> {columns.map((column, index) => { return ( <ColumnComponent key={index} column={column} /> ); })} </div> )} </Droppable> </DragDropContext> </div> ) } |
Теперь проверьте компонент после того, как мы переместили всю логику перетаскивания в компонент-оболочку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import React from 'react' export default function DraggableSample() { return ( <div> <DragWrapper> {columns.map((column, index) => { return ( <ColumnComponent key={index} column={column} /> ); })} </DragWrapper> </div> ) } |
А вот код оболочки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import React from 'react' import { DragDropContext, Droppable } from 'react-beautiful-dnd'; export default function DragWrapper({children}) { function handleDragStart(result) { console.log({ result }); } function handleDragUpdate({ destination }) { console.log({ destination }); } const handleDragEnd = ({ source, destination }) => { console.log({ source, destination }); }; return ( <DragDropContext onDragEnd={handleDragEnd} onDragStart={handleDragStart} onDragUpdate={handleDragUpdate} > <Droppable droppableId="droppable" direction="horizontal"> {(provided) => ( <div {...provided.droppableProps} ref={provided.innerRef}> {children} </div> )} </Droppable> </DragDropContext> ) } |
В результате легче взглянуть на компонент и понять, что он делает на высоком уровне. Вся функциональность перетаскивания находится в оболочке, и её легче понять.
Разделение задач
Это мой любимый метод разделения больших компонентов. В контексте React разделение задач означает разделение частей компонентов, отвечающих за выборку и изменение данных, и частей, отвечающих исключительно за отображение дерева элементов.
Этот метод разделения задач является основной причиной появления шаблона хуков. Вы можете и должны обернуть всю логику, которая управляет API-интерфейсами или подключениями к глобальному состоянию, с помощью пользовательского хука.
Например, рассмотрим компонент:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import React from 'react' import { someAPICall } from './API' import ItemDisplay from './ItemDisplay' export default function SampleComponent() { const [data, setData] = useState([]) useEffect(() => { someAPICall().then((result) => { setData(result) }) }, []) function handleDelete() { console.log('Delete!'); } function handleAdd() { console.log('Add!'); } const handleEdit = () => { console.log('Edit!'); }; return ( <div> <div> {data.map(item => <ItemDisplay item={item} />)} </div> <div> <button onClick={handleDelete} /> <button onClick={handleAdd} /> <button onClick={handleEdit} /> </div> </div> ) } |
Теперь вот его реорганизованная версия с разделением кода с помощью пользовательских хуков:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import React from 'react' import ItemDisplay from './ItemDisplay' export default function SampleComponent() { const { data, handleDelete, handleEdit, handleAdd } = useCustomHook() return ( <div> <div> {data.map(item => <ItemDisplay item={item} />)} </div> <div> <button onClick={handleDelete} /> <button onClick={handleAdd} /> <button onClick={handleEdit} /> </div> </div> ) } |
А вот и сам хук:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import { someAPICall } from './API' export const useCustomHook = () => { const [data, setData] = useState([]) useEffect(() => { someAPICall().then((result) => { setData(result) }) }, []) function handleDelete() { console.log('Delete!'); } function handleAdd() { console.log('Add!'); } const handleEdit = () => { console.log('Edit!'); }; return { handleEdit, handleAdd, handleDelete, data } } |
Отдельный файл для каждого компонента
Часто люди пишут такой код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import React from 'react' export default function SampleComponent({ data }) { export const ItemDisplay = ({ name, date }) => ( <div> <h3>{name}</h3> <p>{date}</p> </div> ) return ( <div> <div> {data.map(item => <ItemDisplay item={item} />)} </div> </div> ) } |
Хотя в написании такого кода нет ничего плохого, следовать ему не рекомендуется. У перевода ItemDisplay в отдельный файл нет недостатков, а плюсы заключаются в том, что ваши компоненты слабо связаны и их легче расширять.
Написание чистого кода по большей части сводится к тому, чтобы быть внимательным и находить время, чтобы следовать рекомендациям и избегать антипаттернов. Так что если вы потратите время на то, чтобы следовать этим шаблонам, это поможет вам писать более чистые компоненты React. Я считаю эти шаблоны очень полезными в моем проекте, и надеюсь, что вы тоже!
Автор: Iskander Samatov
Источник: isamatov.com
Редакция: Команда webformyself.
Читайте нас в Telegram, VK, Яндекс.Дзен