Как стать автором
Обновить

Комментарии 12

Да выбросьте вы из головы Redux, крайне неэффективный стейт-менеджер. Понимаю, что взяли его скорее для опыта и понимания работы легаси-проектов, но на том же MobX сделать намного проще. Вот пример реализации на нем + TS.

Импорты и типы

import _ from 'lodash';
import axios from 'axios';
import { observer } from 'mobx-react';
import { autorun, observable, runInAction, toJS } from 'mobx';
import { ChangeEvent, Component, ContextType, createContext } from 'react';
import Selectable from 'selectable.js';

import styles from './Table.scss';

type TypeStarship = {
  url: string;
  name: string;
  crew: string;
  model: string;
  length: string;
  edited: string;
  created: string;
  passengers: string;
  consumables: string;
  manufacturer: string;
  vehicle_class: string;
  cargo_capacity: string;
  cost_in_credits: string;
  max_atmosphering_speed: string;
};

Контекст для прямого доступа из дочерних компонентов

// eslint-disable-next-line @typescript-eslint/naming-convention
const StarshipsStore = createContext<{ starships: Array<TypeStarship> }>({ starships: [] });

type TypeStarshipsStoreContext = ContextType<typeof StarshipsStore>;

class ConnectedComponentStarship<TProps = any> extends Component<TProps> {
  static context: TypeStarshipsStoreContext;
  static contextType = StarshipsStore;
  declare context: TypeStarshipsStoreContext;
}

Верхняя обертка с пробросом контекста

export class App extends Component {
  render() {
    return (
      <StarshipsStore.Provider value={observable({ starships: [] })}>
        <Table />
      </StarshipsStore.Provider>
    );
  }
}

Верхний компонент таблицы. В нем загружается контент, набрасывается Selectable и трекаются измененные значения в ячейках

@observer
class Table extends ConnectedComponentStarship {
  localState: { prevStarships: Array<TypeStarship> } = observable({
    prevStarships: [],
  });

  select: any;
  trackDisposer: IReactionDisposer | null = null;

  componentDidMount() {
    void axios({
      method: `get`,
      url: `http://swapi.dev/api/vehicles`,
    }).then((result: any) => {
      runInAction(() => {
        this.context.starships = result.data.results;
        this.localState.prevStarships = result.data.results;
      });

      this.select = new Selectable({
        appendTo: `.${styles.table}`,
        filter: `.${styles.tableCell}`,
        autoRefresh: false,
        lasso: {
          border: '1px solid blue',
          backgroundColor: 'rgba(52, 152, 219, 0.1)',
        },
        ignore: [`input`],
      });

      document.addEventListener(`keydown`, this.escKeyDownHandler);

      this.trackChangedStarships();
    });
  }

  escKeyDownHandler = (evt: any) => {
    if (evt.key === `Escape` || evt.key === `Esc`) {
      evt.preventDefault();
      this.select.clear();
    }
  };

  trackChangedStarships = () => {
    this.trackDisposer = autorun(() => {
      if (_.isEqual(this.localState.prevStarships, this.context.starships)) return;

      const changedStarships = _.differenceWith(
        this.localState.prevStarships,
        this.context.starships,
        _.isEqual
      );

      changedStarships.forEach((starship) => {
        console.log('starship data changed', toJS(starship));
      });

      runInAction(() => {
        this.localState.prevStarships = toJS(this.context.starships);
      });
    });
  };

  componentWillUnmount() {
    document.removeEventListener(`keydown`, this.escKeyDownHandler);
    this.trackDisposer?.();
  }

  render() {
    const { starships } = this.context;

    const renderedKeys: Array<keyof TypeStarship> = [
      'cargo_capacity',
      'cost_in_credits',
      'max_atmosphering_speed',
      'name',
    ];

    return (
      <div className={styles.table}>
        <TableHeader renderedKeys={renderedKeys} />
        {starships.map((starship, index) => (
          <TableRow key={index} starship={starship} renderedKeys={renderedKeys} />
        ))}
      </div>
    );
  }
}

Заголовки и строки таблицы. Добавил динамический вывод столбцов, по сравнению с оригинальным кодом

@observer
class TableHeader extends ConnectedComponentStarship<{ renderedKeys: Array<keyof TypeStarship> }> {
  render() {
    const { renderedKeys } = this.props;

    return (
      <div className={styles.tableRowHeader}>
        {renderedKeys.map((param) => (
          <div key={param} className={styles.tableCell}>
            {param}
          </div>
        ))}
      </div>
    );
  }
}

@observer
class TableRow extends ConnectedComponentStarship<{
  starship: TypeStarship;
  renderedKeys: Array<keyof TypeStarship>;
}> {
  render() {
    const { starship, renderedKeys } = this.props;

    return (
      <div className={styles.tableRow}>
        {renderedKeys.map((param) => (
          <TableCell key={param} starship={starship} param={param} />
        ))}
      </div>
    );
  }
}

Ячейка. Если выбрано несколько ячеек, то в хранилище целевой корабль ищется по url, значения мутируются одним батчем благодаря runInAction, в отличие от оригинального решения, где много последовательных dispatch. Если выбрана одна ячейка, значение просто мутируется.

@observer
class TableCell extends ConnectedComponentStarship<{
  starship: TypeStarship;
  param: keyof TypeStarship;
}> {
  handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { starships } = this.context;
    const { starship, param } = this.props;

    const selectedFields = document.querySelectorAll(`.ui-selected`);

    if (!selectedFields.length) {
      runInAction(() => (starship[param] = event.target.value));

      return;
    }

    runInAction(() => {
      selectedFields.forEach(({ dataset }) => {
        const targetStarship = starships.find((s) => s.url === dataset.url);

        if (targetStarship) targetStarship[dataset.param] = event.target.value;
      });
    });
  };

  render() {
    const { starship, param } = this.props;

    return (
      <div data-url={starship.url} data-param={param} className={styles.tableCell}>
        <input onChange={this.handleChange} value={starship[param]} type={'text'} />
      </div>
    );
  }
}

Также исправил ряд ошибок оригинального кода, но все равно это все - чисто для примера, в реальном проекте нужен будет ряд доработок (обработка ошибок, валидации ответа запроса, слой запросов, вместо нереактового Selectable я бы предпочел кастомное решение, без необходимости делать querySelectorAll) и т.п.

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

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

Также присмотритесь к CSS Components вместо строкового указания глобальных классов. К теме статьи не относится, но видеть БЭМ в 2021 как-то очень удивительно.

Чтобы сделать изменение только одной колонки, достаточно добавить фильтр

selectedFields.filter(({ dataset }) => dataset.param === param)

Благодарю за ответ, интересно было посмотреть на решение через MobX, с ним не работал раньше, попробую ваш вариант.

Вопросы:

  1. Почему классовые компоненты? Сами создатели библиотеки говорят о переходе на функциональные компоненты, классовые уходят в прошлое, они дороже и более громоздки.

  2. Почему БЭМ в 2021 удивительно? Он прекрасно ложится на компонентный подход Реакта.

Почему классовые компоненты?

Потому что функциональный компонент на 100 строк — это кошмар, в то время как класс на 10 методов по 10 строк — совершенно нормально организованный код.


Какие-нибудь TableHeader и TableRow и правда лучше как функцию оформить, а вот за оформление чего-то вроде Table через хуки надо по рукам бить.

Проблема не в хуках, а в прослойке между монитором и креслом. Если делать все правильно, у вас не будет 100 строк в функциональном компоненте.

Разумеется, и "правильно" тут вовсе не писать функциональный компонент.

Правильно для того, кто не способен написать функциональный компонент грамотно

Попробуйте, должно понравиться — бойлерплейта в разы меньше (по факту только observer и проброс контекста, в остальном работа со стором — как с обычным объектом, только реактивным).


Писал несколько проектов на хуках, но вернулся к классам из-за удобства организации кода. Как писал выше, в хуках получается смешивание разнородной логики (сайд-эффекты, локальное хранилище, обработка пользовательских событий, асинхронные вызовы, управление жизненным циклом), они несемантичны, больше забот о равенстве по ссылкам и необходимости оптимизации, нет метода для componentWillMount — это прямо серьезный недостаток, так как я в проектах вызываю в нем асинхронные действия и дожидаюсь их выполнения для SSR (есть, конечно, и схожие библиотеки для хуков — но это лишняя зависимость и определенное усложнение). И в целом подход, когда внутри функции есть некое состояние (useState), которое хранится где-то внутри фреймворка и не изменяется при повторных вызовах функций — это как-то не по джавоскриптовому. При больших компонентах функция рендера раздувается из-за комбинации десятка(-ов) хуков, при этом они не имеют доступа к результатам выполнения друг друга, пропсам и контексту без явной передачи — в классах же методы легко комбинировать и в каждом можно получить к ним доступ и к локальному стейту.


Это не значит, что не умею "готовить" функциональные компоненты и грамотно выделять кастомные хуки — просто с классами работать намного удобнее и организовывать код в них проще.


БЭМ — это один из вариантов решения проблемы глобальной области видимости, когда создаются околоуникальные именования (хотя нередко они все же пересекаются и возникают баги). Минусы — длинные названия, нет автодополнения, нет быстрого перехода в стилевые файлы на конкретный класс, сложнее отслеживать наличие неиспользованных или отсутствующих классов, сложнее придумывать названия. Так как в реакт-проектах практически везде используется сборщик, то намного эффективнее использовать CSS Modules и получать все преимущества — автоматические суффиксы и префиксы по названию файла в наименованиях (исключают возможность пересечения), быстрые переходы сразу на нужный класс в стилях, автодополнение, возможность проверки неиспользуемых или отсутствующих классов. Это намного удобнее.

Не нужно советовать то, что вы не понимаете

Вы о чём конкретно?

Да выбросьте вы из головы React, крайне неэффективный рендерер. Понимаю, что взяли его скорее для опыта и понимания работы легаси-проектов, но на том же $mol сделать намного проще. Вот пример реализации на нем + произвольные формулы в ячейках: https://habhub.hyoo.ru/#!author=nin-jin/repo=HabHub/article=10

с таким количеством кода - проще на ванильном js писать.

А как насчет варианта на контексте без внешнего стейт-менеджера?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации