Вопрос о правильности написания кода с точки зрения архитектуры React?

Есть многомерный массив. Я делаю маппинг по нему чтобы отрисовать таблицу. Данные получил, но мне также нужно отрисовать body таблицы данными из массива "data". В "data" лежит объект вида {title:string,number:number}, мне нужно добавить сюда свойство id чтобы сделать сортировку по этому полю и вот вопрос, на каком уровне правильнее мне изменить этот объект, соблюдая принцип иммутабельности и правильности написания React приложений?

export const TableBlock: FC<IParentData> = ({ title, subTitle, dateStart, dateEnd, data }) => {
const [tableVisible, setTableVisible] = useState(false);


return (
    <div className='table-wrapper'>
        <div className="table-block" >
            <div className='table-block__left'>
                <h5 className='table-block__title'>{title}</h5>
                <h6 className='table-block__subtitle'>{subTitle}</h6>
            </div>
            <div className='table-block__right'>
                <p className='table-block__date'>{dateStart} - {dateEnd}</p>
            </div>
        </div>
        {tableVisible && <Table data={data} />}

    </div>

)

}

export const Table: FC<IProps> = ({ data }) => {

return (
    <table className='table'>
        <thead className='thead'>
            <tr className='trow'>
                <th># <span className='icon'> <img src={ArrowIcon} alt="" /></span> </th>
                <th>Title  </th>
                <th>Number <span className='icon'> <img src={ArrowIcon} alt="" /></span> </th>
            </tr>
        </thead>
        {data.map((el,index) =>{
            return (
                <tbody key={el.title} className='tbody'>
                <tr className='trow'>
                    <td>{index}</td>
                    <td>{el.title}</td>
                    <td>{el.number}</td>
                </tr>
            </tbody>
            )
        })}
       

    </table>

)

Ответы (1 шт):

Автор решения: Трипольский Пётр

На мой взгляд, наиболее удобно мутировать объекты в массиве через словарь Map. Посмотрите пример хука и его использования

Хук useListEditor позволяет рендерить список объектов с возможностью их изменения по динамически генерирующемуся id

import React, { useState, useMemo, useEffect, useRef, Fragment } from "react";

type RowId = number;

export const useListEditor = <Data extends any = undefined>(renderItem: (id: RowId, item: Data) => React.ReactElement, {
    initialValue = [],
    onChange,
}: {
    initialValue?: Data[],
    onChange?: (items: Data[]) => void;
}) => {

    const [items, setItems] = useState(new Map<RowId, Data>(initialValue.map((item, idx) => [idx, item])));

    const initialChange = useRef(true);

    const createId = () => Math.max(...items.keys(), 0) + 1;

    const onAddItem = (data: Data) => {
        const id = createId();
        items.set(id, data);
        setItems(new Map(items));
    };

    const onRemoveItem = (id: RowId) => {
        items.delete(id);
        setItems(new Map(items));
    };

    const onUpdateItem = (id: RowId, data: Data) => {
        items.set(id, data);
        setItems(new Map(items));
    };

    const itemList = useMemo(() => [...items.values()], [items]);

    useEffect(() => {
        if (initialChange.current) {
            initialChange.current = false;
            return;
        }
        onChange && onChange(itemList);
    }, [itemList]);

    const render = () => (
        <>
            {[...items.entries()].map(([id, item], idx) => (
                <Fragment key={idx}>
                    {renderItem(id, item)}
                </Fragment>
            ))}
        </>
    );

    return {
        onAddItem,
        onUpdateItem,
        onRemoveItem,
        items: itemList,
        render,
    };
};

export default useListEditor;

Пример использования, можно запустить на codesandbox

import { useListEditor } from "react-declarative";

export const EditableList = () => {
  const { onRemoveItem, onUpdateItem, onAddItem, render } = useListEditor(
    (id, item) => (
      <>
        <span>{`Label: ${item.label}`}</span>
        <span>{`Value: ${item.value}`}</span>
        <button
          onClick={() =>
            onUpdateItem(id, {
              label: prompt("label", item.label),
              value: prompt("value", item.value)
            })
          }
        >
          update
        </button>
        <button onClick={() => onRemoveItem(id)}>remove</button>
      </>
    ),
    {
      initialValue: [
        {
          label: "foo",
          value: "bar"
        }
      ],
      onChange: (items) => console.log(items)
    }
  );

  return (
    <>
      <button
        style={{
          marginBottom: 5
        }}
        onClick={() =>
          onAddItem({
            label: "fiz",
            value: "baz"
          })
        }
      >
        Add
      </button>
      <div
        style={{
          display: "grid",
          gridTemplateColumns: "1fr 1fr auto auto",
          gridColumnGap: 5,
          gridRowGap: 5,
          maxWidth: 300
        }}
      >
        {render()}
      </div>
    </>
  );
};

export default EditableList;

screenshot

→ Ссылка