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

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

https://t.me/about_javascript

В процессе работы многие фрагменты кода увеличиваются настолько, что над ними легко потерять контроль. Речь идет о компонентах 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%! При этом можно достичь еще большего результата, если переместить дочерний компонент из файла.

https://t.me/front_tester

источник


Report Page