26 апреля 2021

Простые советы по написанию чистых компонентов React

Перевод. Источник – https://isamatov.com

Давайте в этом посте рассмотрим несколько простых советов, которые помогут вам писать более чистые компоненты React и лучше масштабировать ваш проект.

Избегайте оператора spread при передаче props.

Давайте начнем с антипаттерна, которого вам следует избегать. Если нет конкретной и обоснованной причины, вам следует избегать передачи свойств по дереву компонентов с помощью оператора spread, например: {...props}.

Такая передача props, конечно, ускоряет написание компонентов. Но в то же время затрудняет выявление ошибок в коде. Вы теряете уверенность в своих компонентах, что затрудняет их рефакторинг, и в результате ошибки начнут появляться намного раньше.

Оберните параметры функции в объект

Если ваша функция принимает несколько параметров, рекомендуется заключить их в объект. Вот пример:

export const sampleFunction = ({ param1, param2, param3 }) => {
    console.log({ param1, param2, param3 });
}

Такой способ написания сигнатуры функции дает несколько заметных преимуществ:

  • Вам больше не нужно беспокоиться о порядке передачи аргументов функции. Я совершал эту ошибку несколько раз, когда допускал баг и передавал аргументы функции в неправильном порядке.
  • В редакторах кода с настроенным IntelliSense (в настоящее время - в большинстве из них), вы получите удобную функцию автозаполнения для аргументов функции.

Для обработчиков событий используйте функции, возвращающие функции обработчика.

Если вы знакомы с функциональным программированием, этот метод напоминает каррирование, поскольку вы заранее устанавливаете некоторые параметры.

Давайте посмотрим на пример:

import React from 'react'

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:

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>


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>
    )
}

С использованием этой простой стратегии ваши компоненты станут более декларативными и более простыми для понимания. Это также упрощает расширение логики и добавление в нее дополнительных элементов.

Нужен сайт на JAMstack?
Наймите нас

Компонентные хуки


Я считаю этот паттерн полезным, если им не злоупотребляют.

Вы можете обнаружить, что используете некоторые компоненты во всем своем приложении. Если им для работы требуется состояние, можно обернуть их хуком, обеспечивающим это состояние. Хорошими примерами таких компонентов являются всплывающие окна, всплывающие уведомления или простые модальные окна. Например, вот хук компонента для простого модального окна подтверждения:

import React, { useCallback, useState } from 'react';
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 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;

Такое абстрагирование компонента избавляет вас от написания большого количества шаблонного (boilerplate) кода управления состоянием. Если вы хотите узнать больше о полезных хуках React, прочтите мой пост здесь.

Разделение компонентов


Следующие три совета относятся к разумному разделению компонентов. По моему опыту, сохранение небольших размеров компонентов - лучший способ сохранить управляемость проекта.

Используйте обертки (wrappers)


Если не получается найти способ разделить большой компонент, посмотрите на функциональность, которую предоставляет каждый элемент этого компонента. Некоторые элементы предназначены для обеспечения особой функциональности, например, обработчики перетаскивания.

Вот пример компонента, который реализует drag-and-drop с помощью response-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>
    )
}

Теперь посмотрите, каким стал компонент после того, как мы переместили всю логику перетаскивания в компонент-обёртку:

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>
    )
}

А вот его refactored версия с разделением кода с помощью настраиваемых хуков:

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>{name}</h3>
            <p>{date}</p>
        </div>
    )

    return (
        <div>
            <div>
                {data.map(item => <ItemDisplay item={item} />)}
            </div>
        </div>
    )
}

Хотя в написании такого кода нет ничего ужасно плохого, делать так не рекомендуется. Перемещение ItemDisplay в отдельный файл не имеет каких-то минусов, а плюсы заключаются в том, что компоненты слабо связаны и их легче расширять.

Заключение

Написание чистого кода по большей части сводится к тому, чтобы быть внимательным и находить время, чтобы следовать паттернам и избегать антипаттернов. Так что если вы потратите время на это, то сможете писать более чистые компоненты React. Я считаю эти паттерны очень полезными для моих проектов, и надеюсь, что вы тоже их оцените!

Создаем быстрые сайты на JAMstack.
Наймите нас

Не получилось найти то, что искали?

Напишите нам