Как уменьшить размер компонента React: 3 профессиональных приема

В процессе работы многие фрагменты кода увеличиваются настолько, что над ними легко потерять контроль. Речь идет о компонентах React. Обрастание компонента множеством функций, JSX и конфигураций становится одной из главных проблем для тех, кто начинает изучать React. Рассмотрим, как ее избежать.

Возьмем в качестве примера один из компонентов пользовательского интерфейса  —  table. Попробуем оптимизировать его объем, используя для этого 3 приема.

Компонент table будет иметь несколько функций, которые можно расширить в дальнейшем.

  • Получение заголовков и строк в качестве параметров и рендеринг таблицы.
  • Получение сортировщиков в качестве параметра, определяющего, какой заголовок подлежит сортировке.
  • Переключение заголовка таблицы будет вызывать сортировку по возрастанию и убыванию.

Приложение будет выглядеть так:

import React from 'react'
import Table from './components/Table'

function App() {
const headers = {
name: 'Name',
origin: 'Origin',
largestCountry: 'Largest Exporter',
productionInBillions: 'Pruduction (BLN)',
}

const rows = [
{
name: 'Apple',
origin: 'Spain',
largestCountry: 'India',
productionInBillions: '1.5',
},
{
name: 'Mango',
origin: 'India',
largestCountry: 'India',
productionInBillions: '1.9',
},
{
name: 'Avocados',
origin: 'America',
largestCountry: 'America',
productionInBillions: '1.9',
},
{
name: 'PassionFruit',
origin: 'America',
largestCountry: 'America',
productionInBillions: '1.7',
},
]

const sorters = {
name: true,
origin: true,
productionInBillions: true,
}
return <Table headers={headers} rows={rows} sorters={sorters} />
}

export default App

Изначально табличный компонент будет выглядеть вот так. Позже мы его оптимизируем.

table {
font-family: arial, sans-serif;
border-collapse: collapse;
width: 100%;
}
td,
th {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
}

tr:nth-child(even) {
background-color: #dddddd;
}

.sort-icon {
display: inline;
}

import React, { FunctionComponent, useEffect, useState } from 'react'
import './Table.css'

interface TableProps {
headers: Record<string, string>
sorters?: Record<string, boolean>
rows: Record<string, string>[]
}

const Table: FunctionComponent<TableProps> = ({ headers, rows, sorters }) => {
const isSortable = Boolean(sorters)
const [displayedRows, setDisplayedRows] = useState(rows)
const [sortersData, setSortersData] = useState(sorters)
const [currentSort, setCurrentSort] = useState('')

useEffect(() => {
if (!isSortable || currentSort === '') {
return
}

const sortedRows = rows.sort(
(a: Record<string, string>, b: Record<string, string>) => {
if (sortersData![currentSort]) {
return a[currentSort] < b[currentSort] ? 1 : -1
} else if (!sortersData![currentSort]) {
return a[currentSort] > b[currentSort] ? 1 : -1
}

return 0
},
)
setDisplayedRows([...sortedRows])
}, [sortersData])

const handleSortToggled = (headerKey: string, isAsc: boolean) => {
if (!isSortable) {
return
}

const newIsAsc = !isAsc
sortersData![headerKey] = newIsAsc
setCurrentSort(headerKey)
setSortersData({ ...sortersData })
}

return (
<>
<table>
<thead>
<tr>
{Object.keys(headers).map((headerKey: string, index: number) => (
<th key={'col' + index}>
{headers[headerKey]}
{isSortable && sortersData![headerKey] !== undefined && (
<div
className="sort-icon"
onClick={() =>
handleSortToggled(headerKey, sortersData![headerKey])
}
>
{sortersData![headerKey] ? <>&and;</> : <>&or;</>}
</div>
)}
</th>
))}
</tr>
</thead>
<tbody>
{displayedRows.map((row: Record<string, string>, index: number) => (
<tr key={'row' + index}>
{Object.values(row).map((cell: string, cellIndex: number) => (
<td key={'cell' + cellIndex}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</>
)
}

export default Table

Возможности пространства имен

В компонентах React часто увеличивается количество типов (интерфейсов, перечислений) и констант. В первом методе мы будем использовать пространства имен. Файл конфигурации может хранить константы, типы и даже чистые вспомогательные функции, поэтому их не нужно оставлять внутри компонента. Рассмотрим подробнее:

export namespace TableConfig {
export type Row = Record<string, string>
export type Header = Record<string, string>
export type Sorter = Record<string, boolean>

export interface TableProps {
headers: Header
rows: Row[]
sorters?: Sorter
}
}

Затем можно будет обновить тип реквизита (props) компонента table:

...
import { TableConfig } from './TableConfig'

const Table: FunctionComponent<TableConfig.TableProps> = ({
...

2. Разделение на субкомпоненты и совместное использование состояния

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

  1. Всегда создавайте дочерние компоненты вне тела основного компонента: либо в том же файле, либо в других файлах. В противном случае возникнет проблема с производительностью, и при каждом обновлении состояния будут создаваться дочерние компоненты.
  2. Используйте context, если хотите поделиться состоянием основного компонента, чтобы избежать перегрузки свойств.

Рассмотрим это на примере:

import React, {
createContext,
FunctionComponent,
useContext,
useEffect,
useState,
} from 'react'
import './Table.css'
import { TableConfig } from './TableConfig'

const TableContext = createContext<Record<string, any>>({})

const Header: FunctionComponent<{ headerKey: string }> = ({ headerKey }) => {
const { headers, isSortable, sortersData, handleSortToggled } = useContext(
TableContext,
)

return (
<th>
{headers[headerKey]}
{isSortable && sortersData![headerKey] !== undefined && (
<div
className="sort-icon"
onClick={() => handleSortToggled(headerKey, sortersData![headerKey])}
>
{sortersData![headerKey] ? <>&and;</> : <>&or;</>}
</div>
)}
</th>
)
}

const Row: FunctionComponent<{ row: TableConfig.Row }> = ({ row }) => {
return (
<tr>
{Object.values(row).map((cell: string, cellIndex: number) => (
<td key={'cell' + cellIndex}>{cell}</td>
))}
</tr>
)
}

const Table: FunctionComponent<TableConfig.TableProps> = ({
headers,
rows,
sorters,
}) => {
const isSortable = Boolean(sorters)
const [displayedRows, setDisplayedRows] = useState(rows)
const [sortersData, setSortersData] = useState(sorters)
const [currentSort, setCurrentSort] = useState('')

useEffect(() => {
if (!isSortable || currentSort === '') {
return
}

const sortedRows = rows.sort(
(a: Record<string, string>, b: Record<string, string>) => {
if (sortersData![currentSort]) {
return a[currentSort] < b[currentSort] ? 1 : -1
} else if (!sortersData![currentSort]) {
return a[currentSort] > b[currentSort] ? 1 : -1
}

return 0
},
)
setDisplayedRows([...sortedRows])
}, [sortersData])

const handleSortToggled = (headerKey: string, isAsc: boolean) => {
if (!isSortable) {
return
}

const newIsAsc = !isAsc
sortersData![headerKey] = newIsAsc
setCurrentSort(headerKey)
setSortersData({ ...sortersData })
}

return (
<>
<TableContext.Provider
value={{ headers, isSortable, sortersData, handleSortToggled }}
>
<table>
<thead>
<tr>
{Object.keys(headers).map((headerKey: string, index: number) => (
<Header headerKey={headerKey} key={'col' + index} />
))}
</tr>
</thead>
<tbody>
{displayedRows.map((row: Record<string, string>, index: number) => (
<Row key={'row' + index} row={row} />
))}
</tbody>
</table>
</TableContext.Provider>
</>
)
}

export default Table

3. Пользовательские хуки для масштабирования и читаемости

Итак, мы можем масштабировать типы и JSX. Остался последний элемент, который нужно переместить. Это состояние и методы, работающие с состоянием. Лучше всего просто переместить их в пользовательский хук. Посмотрим, как это выглядит:

import React, {
createContext,
FunctionComponent,
useContext,
useEffect,
useState,
} from 'react'
import './Table.css'
import { TableConfig } from './TableConfig'
import { useSorter } from './useSorter'

const TableContext = createContext<Record<string, any>>({})

const Header: FunctionComponent<{ headerKey: string }> = ({ headerKey }) => {
const { headers, isSortable, sortersData, handleSortToggled } = useContext(
TableContext,
)

return (
<th>
{headers[headerKey]}
{isSortable && sortersData![headerKey] !== undefined && (
<div
className="sort-icon"
onClick={() => handleSortToggled(headerKey, sortersData![headerKey])}
>
{sortersData![headerKey] ? <>&and;</> : <>&or;</>}
</div>
)}
</th>
)
}

const Row: FunctionComponent<{ row: TableConfig.Row }> = ({ row }) => {
return (
<tr>
{Object.values(row).map((cell: string, cellIndex: number) => (
<td key={'cell' + cellIndex}>{cell}</td>
))}
</tr>
)
}

const Table: FunctionComponent<TableConfig.TableProps> = ({
headers,
rows,
sorters,
}) => {
const [displayedRows, setDisplayedRows] = useState(rows)
const [sortedRows, isSortable, sortersData, sortToggled] = useSorter(
rows,
sorters,
)

useEffect(() => {
setDisplayedRows([...sortedRows])
}, [sortedRows])

const handleSortToggled = (headerKey: string, isAsc: boolean) => {
sortToggled(headerKey, isAsc)
}

return (
<>
<TableContext.Provider
value={{ headers, isSortable, sortersData, handleSortToggled }}
>
<table>
<thead>
<tr>
{Object.keys(headers).map((headerKey: string, index: number) => (
<Header headerKey={headerKey} key={'col' + index} />
))}
</tr>
</thead>
<tbody>
{displayedRows.map((row: Record<string, string>, index: number) => (
<Row key={'row' + index} row={row} />
))}
</tbody>
</table>
</TableContext.Provider>
</>
)
}

export default Table

import { useEffect, useState } from 'react'
import { TableConfig } from './TableConfig'

type SorterProps = [
TableConfig.Row[],
boolean,
TableConfig.Sorter | undefined,
(headerKey: string, isAsc: boolean) => void,
]

export const useSorter = (
rows: TableConfig.Row[],
sorters?: TableConfig.Sorter,
): SorterProps => {
const isSortable = Boolean(sorters)
const [sortedRows, setSortedRows] = useState(rows)
const [sortersData, setSortersData] = useState(sorters)
const [currentSort, setCurrentSort] = useState('')

useEffect(() => {
if (!isSortable || currentSort === '') {
return
}

const sortedRows = rows.sort(
(a: Record<string, string>, b: Record<string, string>) => {
if (sortersData![currentSort]) {
return a[currentSort] < b[currentSort] ? 1 : -1
} else if (!sortersData![currentSort]) {
return a[currentSort] > b[currentSort] ? 1 : -1
}

return 0
},
)
setSortedRows([...sortedRows])
}, [sortersData])

const sortToggled = (headerKey: string, isAsc: boolean) => {
if (!isSortable) {
return
}
const newIsAsc = !isAsc
sortersData![headerKey] = newIsAsc
setCurrentSort(headerKey)
setSortersData({ ...sortersData })
}

return [sortedRows, isSortable, sortersData, sortToggled]
}

Нам удалось уменьшить размер компонента почти на 40%! При этом можно достичь еще большего результата, если переместить дочерний компонент из файла.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Vitalii Shevchuk: Top 3 React Tricks Pros 😎 like to Use to Reduce the Size of a Component

Предыдущая статьяСоздание приложения на Python для систематизации фото по геолокации и дате
Следующая статья3 способа мониторинга изменений лог-файлов в Java