Источник: Nuances of Programming
В процессе работы многие фрагменты кода увеличиваются настолько, что над ними легко потерять контроль. Речь идет о компонентах 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] ? <>∧</> : <>∨</>}
</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. Разделение на субкомпоненты и совместное использование состояния
На данный момент мы знаем, куда можно переместить типы, константы и вспомогательные функции. Теперь разбираемся, что делать с субкомпонентами. Это маленькие дочерние компоненты, на которые разделяются основные большие компоненты. Следует учитывать две особенности.
- Всегда создавайте дочерние компоненты вне тела основного компонента: либо в том же файле, либо в других файлах. В противном случае возникнет проблема с производительностью, и при каждом обновлении состояния будут создаваться дочерние компоненты.
- Используйте 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] ? <>∧</> : <>∨</>}
</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] ? <>∧</> : <>∨</>}
</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%! При этом можно достичь еще большего результата, если переместить дочерний компонент из файла.
Читайте также:
Перевод статьи Vitalii Shevchuk: Top 3 React Tricks Pros 😎 like to Use to Reduce the Size of a Component