Что определяет, является ли кто-то хорошим разработчиком? Это кто-то, кто быстро продвигает изменения? Это кто-то, кто пишет код без ошибок? Это кто-то, кто может работать над всей функцией самостоятельно?

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

Если, читая этот пост, вы продолжаете спрашивать себя 'Почему он это делает?' имейте в виду, что я привожу примеры, поэтому вам нужно больше видеть концепции, чем сам код. Так что не спрашивайте себя:

  • Почему вы используете red вместо #ff0000?
  • Почему вы используете CSS-in-JS вместо CSS и имен классов?
  • Где обработка ошибок? Где пустые состояния?

Ответ на эти вопросы всегда один и тот же: потому что это всего лишь пример, а не реальный код. С учетом сказанного, мы можем начать.

Все началось со стола, о котором я ранее писал. Но что делало этот стол особенным, так это несколько вещей:

  1. Поведение прокрутки, которое я объяснял ранее.
  2. Его собирались повторно использовать для нескольких экранов.
  3. Панель инструментов для фильтрации и сортировки.

Я хотел быть «хорошим разработчиком», поэтому для решения № 2 я решил создать таблицу «включай и работай», в которой не нужно ничего, кроме простого определения того, что представляют собой столбцы были (т. е. тип данных, имя столбца) и необработанные данные, и моя волшебная таблица отобразит идеально подходящую таблицу.

Чтобы начать объяснять, что я сделал, я сначала представлю столбцы, которые будут использоваться в будущем (имейте в виду, что это упрощенный пример, а не то, как выглядел мой код).

const columns = {
  date: {
    type: 'date',
    header: 'Date',
    align: 'center',
    accessor: 'date', 
  },
  author: {
    type: 'text',
    header: 'Author',
    align: 'left',
    accessor: 'author',
  },
  book: {
    type: 'book',
    header: 'Book',
    align: 'center',
    accesor: 'book',
  },
  purchaseQuantity: {
    type: 'number',
    header: 'Purchased Qty',
    align: 'center',
    accessor: 'purchaseQuantity',
  },
  salesPerson: {
    type: 'text',
    header: 'Sales Person',
    align: 'left',
    accessor: 'salesPerson',
  },
  stock: {
    type: 'stock',
    header: 'Stock',
    align: 'center',
    accessor: 'stock',
  },
  paymentMethod: {
    type: 'string',
    header: 'Payment Method',
    align: 'center',
    accessor: 'paymentMethod',
  },
  shipmentStatus: {
    type: 'shipmentStatus',
    header: 'Shipment Status',
    align: 'center',
    accessor: 'shipmentStatus',
  },
};

Так это хорошо, да? У меня определены все столбцы, поэтому позже я могу определить список столбцов, которые будут использоваться для каждой таблицы.

// tableColumns.js

const salesColumns = [
  columns.date,
  columns.salesPerson,
  columns.book,
  columns.author,
  columns.purchaseQuantity,
  columns.paymentMethod,
];

const stockColumns = [
  columns.book,
  columns.author,
  columns.stock,
];

const shipmentColumns = [
  columns.quantity,
  columns.book,
  columns.author,
  columns.paymentMethod,
  columns.shipmentStatus,
];

export default {
  sales: salesColumns,
  stock: stockColumns,
  shipments: shipmentColumns,
}

Важно отметить, что мои столбцы имеют type, который иногда просто number или text, но иногда это что-то настраиваемое, например author или shipmentStatus, потому что у меня есть компонент Cell, который работает следующим образом.

// Cell.js
const Cell = ({ 
  align, 
  data,
  type, 
}) => {
  switch (type) {
    case 'book': 
      return (
        <td>
          <p>{data.title}</p>
          <p>{data.isbn}</p>
        </td>
      );
    case 'shipmentStatus':
      return (
        <td>
          <p>{data.status}</p>
          <p>
            <a href={data.trackingUrl}>
              {data.trackingCode}
            </a>
          </p>
        </td>
      );
    case 'stock':
      return (
        <td>
          <p style={{ color: data < 10 ? 'red' : 'black' }}>
            {data}
          </p>
        </td>
      );
    case 'text':
    case 'number':
    default:
      return (
        <td style={{ textAlign: align}}>
          {data}
        </td>
      );
  }
}

И тогда моя таблица будет отображать что-то вроде этого

// Table.js
import { useCallback } from 'react';

import Cell from '#components/Cell';

import columns from '#constants/tableColumns';

const table = ({
  type,
  data,
  // otherProps
}) => {
  const tableColumns = columns[type];

  const renderRow = useCallback((data) => (
    <tr key={data.id}>
      {
        tableColumns.map(({ accesor, header, type }) => (
          <Cell type={type} key={header} data={data[accesor]} />
        ))
      }
    </tr>
  ), [tableColumns]);
  
  return (
    <table>
      <thead>
        <tr>
          {
            tableColumns.map(({ align, header }) => (
              <th 
                key={header}
                style={{ textAlign: align }}
              >
                {header}
              </th>
            ))
          }
        </tr>
      </thead>
      <tbody>
        {
          data.map(renderRow)
        }
      </tbody>
    </table>
  );
}

И это сработало, с этой таблицей я мог просто отобразить, выполнив

import Table from '#components/Table';

const StocksPage = () => {
  const { data: stocks, isLoading } = useFetchStocksData();

  if (isLoading) {
    return (
      <div>
        Loading...
      </div>
    );
  }

  return (
    <Table
      type="stocks"
      data={stocks}
    />
  )
}

Но потом, как это часто бывает, требования начали меняться.

Итак, например, предположим, что если таблица является таблицей stocks, мы хотим также показать общее количество книг, которые есть у автора в магазине, мы можем обновить столбец author, чтобы он был

const columns = [
  // ...otherColumns,
  author: {
    type: 'author', // changed from 'text',
    accessor: 'author',
    align: 'center', // is this necessary anymore?
    header: 'Author',
  },
]

И Cell, чтобы добавить общее количество книг

case 'author':
  return (
    <td>
      <p>{data.name} ({data.totalBooks} total titles)</p>
    </td>
  );

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

/* Option A: the cell decides what to show */

// We add a `tableType` prop to the cell
const Cell = ({ data, type, align, tableType }) => {
  switch (type) {
    // Old code stays the same
    case 'author':
      return (
        <td>
          <p>
            {data.name} {tableType === 'stocks' ? `(${data.totalBooks} total titles)`: null}
          </p>
        </td>
      );
  }
}

/* Option B: the column changes depending on the tableType */

const columns = {
  author: {
    type: 'text',
    header: 'Author',
    align: 'left',
    accessor: 'author.name',
  },
  stockAuthor: {
    type: 'author',
    header: 'Author',
    align: 'left',
    accessor: 'author',
  },
};

const stockColumns = [
  columns.book,
  columns.stockAuthor,
  columns.stock,
];

Итак, вы можете видеть, как простое изменение запускает весь процесс рефакторинга, который в конечном итоге создает множество пограничных сценариев, усложняет отладку, а также вы должны убедиться, что изменение в таблице stocks не повлияло на таблицы sales или shipments. .

Что нужно было сделать, чтобы предотвратить это?

Когда я увидел, что мне нужно создать три очень похожие таблицы, у меня возник соблазн создать один общий компонент React в качестве универсального решения. Что я должен был сделать, так это задать себе очень простой вопрос

Эти три таблицы действительно похожи?

В моем случае должно было быть ясно, что это не так. Это не то же самое, что говорить о запасах, продажах и поставках, даже если три из них тесно связаны.

То, что я думаю, что я должен был сделать, очень просто: извлечь то, что я знаю наверняка, это то же самое для трех таблиц.

Проблема не столько в компоненте Cell, сколько в компоненте Table. Он может отображать заголовки и получать children строк, поэтому, если мне нужно внести какие-либо изменения в строки, я могу просто соответствующим образом адаптировать этот конкретный экран.

Так что, если бы мне пришлось начинать свой стол с начала, я бы сделал что-то вроде этого

// Table.js
import { useCallback } from 'react';
import columns from '#constants/tableColumns';

const Table = ({
  headers,
  children,
}) => (
  <table>
    <thead>
      <tr>
        {
          headers.map(({ align, header }) => (
            <th 
              key={header}
              style={{ textAlign: align }}
            >
              {header}
            </th>
          ))
        }
      </tr>
    </thead>
    <tbody>
      {children}
    </tbody>
  </table>
);
import Table from '#components/Table';

const StocksPage = () => {
  const { data: stocks, isLoading } = useFetchStocksData();

  if (isLoading) {
    return (
      <div>
        Loading...
      </div>
    );
  }

  return (
    <Table
      headers={stockColumns}
    >
      {stocks.map((row) => (
        <tr key={row.id}>
          <Cell type="book" data={row.book} />
          <Cell type="author" data={row.author} />
          <Cell type="stock" data={row.stock} />
        </tr>
      ))}
    </Table>
  );
}

Я бы также сделал еще один шаг и полностью отказался от компонента Cell, а также добавил бы несколько компонентов ячеек (например, AuthorCell, TextCell, NumberCell и т. д.), и это стало бы еще проще в обслуживании, поскольку мы можем быть более описательными с полями data. , и мы бы отказались от реквизита type.

import Table from '#components/Table';

const StocksPage = () => {
  const { data: stocks, isLoading } = useFetchStocksData();

  if (isLoading) {
    return (
      <div>
        Loading...
      </div>
    );
  }

  return (
    <Table
      headers={stockColumns}
    >
      {
        stocks.map(({ id, book, author, stock }) => (
          <tr key={id}>
            <BookCell title={book.title} isbn={book.isbn} />
            <AuthorCell name={author.name} totalBooks={author.totalBooks} showTotalBooks />
            <StockCell stock={stock} hasLowStock={stock < 10} />
          </tr>
        ))
      }
    </Table>
  );
}

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

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

Вам может быть интересно

Как я могу узнать, когда должен быть общий компонент, а когда несколько?

Жаль вас разочаровывать, но, как и большинство вещей в жизни, это не черно-белая ситуация.

Есть много факторов, которые могут повлиять на это решение, но самый важный из них — это опыт, ваш или чей-то еще.

Когда вы впервые начинаете писать свой компонент, вам нужно спросить себя: легко ли настроить этот компонент, или мне нужно будет возвращаться снова и снова, чтобы внести изменения?

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

Еще одна вещь, на которую следует обратить внимание, это то, что вещи выглядеть одинаково и действовать одинаково. В моем примере авторы выглядели одинаково в таблицах stocks и sales, но вели себя по-разному в зависимости от того, где они отображались.

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

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

И это то, что я хотел бы знать, прежде чем я начал работать над своей общей таблицей.