Избегайте использования оператора spread при передаче свойств
Начнём с анти-паттерна, с приёма работы, которым лучше не пользоваться в тех случаях, когда для этого нет конкретных, обоснованных причин. Речь идёт о том, что следует избегать использования оператора spread ({...props}) при передаче свойств от родительских компонентов дочерним.
Передача свойств таким образом может, на самом деле, ускорить разработку. Но этот приём значительно усложняет обнаружение ошибок в коде. Разработчик начинает меньше доверять своим компонентам, это усложняет их рефакторинг, и, как результат, ошибки появляются в них раньше, чем в случаях, когда оператор spread для передачи свойств не используется.
Объявляйте функции, использующие несколько параметров, применяя объекты с параметрами
Если функция принимает несколько параметров — хорошо будет поместить их все в объект. Вот как это может выглядеть:
export const sampleFunction = ({ param1, param2, param3 }) => { console.log({ param1, param2, param3 }); }
Если подходить к созданию сигнатур своих функций именно так — можно получить несколько заметных преимуществ перед обычным способом объявления функций:
- Вам больше не придётся беспокоиться о порядке, в котором функции передаются аргументы. Я несколько раз сталкивался с проблемой, когда передача аргументов функции в неправильном порядке приводила к возникновению ошибки.
- При работе с редакторами, в которых используется IntelliSense (в наши дни это — практически все редакторы), в нашем распоряжении окажется приятная возможность автодополнения при вводе аргументов функции.
При работе с обработчиками событий используйте функции, возвращающие функции-обработчики
Если вы знакомы с функциональным программированием — то знайте, что то, о чём я хочу сейчас рассказать, напоминает каррирование. Суть в том, что тут применяется заблаговременная установка некоторых параметров функции.
Взгляните на этот пример:
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> ) }
Как видите, такое оформление функции-обработчика событий помогает поддерживать дерево компонентов в чистоте.
Используйте объекты, хранящие пары ключ-значение вместо условного оператора
Если нужно выводить различные элементы, основываясь на некоей логике, я посоветовал бы пользоваться объектами, хранящими данные в формате ключ-значение (коллекциями записей), а не выражениями if/else.
Вот пример, в котором используется if/else:
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> ) }
А вот — пример использования объекта, хранящего соответствующие значения:
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> ) }
Использование этой простой стратегии делает компоненты более декларативными и упрощает понимание их кода. Это, кроме того, облегчает расширение функционала программы через добавление новых элементов в коллекцию записей.
Оформляйте в виде хуков компоненты, рассчитанные на многократное использование в проекте
Я считаю весьма полезным паттерн, о котором хочу рассказать в этом разделе.
Возможно, вы сталкивались или столкнётесь с ситуацией, когда окажется, что какие-то компоненты постоянно используются в разных частях приложения. Если им для работы нужно хранить что-то в состоянии приложения, это значит, что их можно обернуть в хуки, предоставляющие им это состояние. Удачными примерами подобных компонентов можно назвать всплывающие окна, тост-уведомления, простые модальные окна. Например — вот компонент, в котором, с использованием хука, реализовано простое модальное окно для подтверждения выполнения некоей операции:
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, }; }
Пользоваться этим хуком можно так:
import React from "react"; import { useConfirmationDialog } from './useConfirmationDialog' function Client() { const { Dialog, onOpen } = useConfirmationDialog({ headerText: "Delete this record?", bodyText: "Are you sure you want to delete this record? This cannot be undone.", confirmationButtonText: "Delete", onConfirmClick: handleDeleteConfirm, }); function handleDeleteConfirm() { //TODO: удалить } const handleDeleteClick = () => { onOpen(); }; return ( <div> <Dialog /> <button onClick={handleDeleteClick} /> </div> ); } export default Client;
Такой подход к абстрагированию компонентов избавляет программиста от необходимости написания больших объёмов шаблонного кода для управления состоянием приложения. Если вы хотите узнать о нескольких полезных хуках React — взгляните на этот мой материал.
Разделяйте код компонентов на части
Следующие три совета посвящены грамотному разделению компонентов на части. Опыт подсказывает мне, что лучший способ поддержания проекта в таком состоянии, чтобы проектом было бы удобно управлять, заключается в том, чтобы стремиться делать компоненты как можно компактнее.
▍Использование обёрток
Если вам сложно найти способ разбиения большого компонента на части — взгляните на функционал, предоставляемый каждым из его элементов. Некоторые элементы, например, существуют ради решения узкоспециализированных задач, например, для поддержки механизма drag-and-drop.
Вот пример компонента, реализующего drag-and-drop с использованием react-beautiful-dnd:
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> ) }
А теперь взгляните на тот же компонент после того, как мы переместили drag-and-drop-логику в компонент-обёртку:
import React from 'react' export default function DraggableSample() { return ( <div> <DragWrapper> {columns.map((column, index) => { return ( <ColumnComponent key={index} column={column} /> ); })} </DragWrapper> </div> ) }
Вот — код обёртки:
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> ) }
В результате код компонента можно очень быстро прочесть и в общих чертах разобраться с тем, какую именно задачу решает этот компонент. А весь функционал, имеющий отношение к механизму drag-and-drop, размещён в компоненте-обёртке. С кодом, реализующим этот механизм, теперь тоже гораздо проще и удобнее работать.
▍Разделение обязанностей
Тут я хочу рассказать о моём любимом методе разделения больших компонентов на части.
В контексте React «разделение обязанностей» означает отделение тех частей компонента, которые ответственны за загрузку и преобразование данных, от тех частей, которые отвечают исключительно за отображения дерева элементов.
Паттерн «хук» появился именно благодаря идее разделения обязанностей. Можно и даже нужно оборачивать в хуки всю логику, ответственную за взаимодействие с некими API или с глобальным состоянием.
Например — давайте посмотрим на следующий компонент:
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> ) }
Ниже показано то, что получилось после его рефакторинга, в ходе которого его код разделён на части с применением пользовательских хуков. А именно — вот сам компонент:
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> ) }
Вот — код хука:
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 } }
▍Хранение кода каждого компонента в отдельном файле
Часто программисты пишут код компонентов примерно так:
import React from 'react' export default function SampleComponent({ data }) { export const ItemDisplay = ({ name, date }) => ( <div> <h3><font color="#3AC1EF">▍{name}</font></h3> <p>{date}</p> </div> ) return ( <div> <div> {data.map(item => <ItemDisplay item={item} />)} </div> </div> ) }
И, хотя тут нет ничего ужасно неправильного, нельзя рекомендовать писать код именно так. У перемещения ItemDisplay в отдельный файл нет никаких минусов. При этом плюсом такого подхода можно назвать тот факт, что, после переноса кода компонента в отдельный файл, компоненты проекта оказываются слабее связаны друг с другом. Их, кроме того, легче будет расширять.
Итоги
Написание чистого кода, в основном, сводится к тому, чтобы внимательно и вдумчиво относиться к своему делу и находить время на изучение удачных и неудачных паттернов. Хорошие решения стоит включать в свои проекты, а анти-паттерны лучше обходить стороной. Собственно говоря, применение удачных паттернов помогает писать более чистый код React-компонентов. Паттерны, которыми я тут поделился, очень помогают мне в работе. Надеюсь, они пригодятся и вам.
Что вы посоветовали бы тем, кто хочет сделать код своих React-компонентов чище?
ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/553068/


Добавить комментарий