В предыдущей статье я описал, как можно упростить код для редактирования данных в веб-формах, используя EntityData архитектуру, построенную на использовании path & value для событий изменений. Но бизнес-логика для приложений обычно состоит из гораздо большего количества обрабатывающего кода, чем просто он.

Примеры действий, которые вы можете захотеть реализовать в приложениях:

  • Получение данных с сервера (асинхронный процесс)
  • Индикаторы того, что данные извлекаются
  • Определите, устарели ли данные и их нужно снова получить.
  • Отображение и перемещение данных
  • Начать редактирование (переключиться из представления в форму)
  • Изменить данные, желательно без потери исходного значения
  • Показать, что было отредактировано
  • Редактировать часть набора данных (например, объект из списка)
  • Подтвердить данные
  • Отображать сообщения об ошибках (желательно в соответствующем поле)
  • Отменить редактирование
  • Сохраните отредактированные значения на сервере
  • Удалить данные
  • Удалить части наборов данных

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

Ручные события

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

Пользователь с именем Джон Сноу собирается изменить свое имя на Эйгон Таргариен. Для этого в приложении есть веб-форма редактирования данных профиля:

// actions.js
export const FETCH_USER_INITIATE = 'FETCH_USER_INITIATE';
export const FETCH_USER_COMPLETE = 'FETCH_USER_COMPLETE';
export const FETCH_USER_ERROR = 'FETCH_USER_ERROR';
export const CHANGE_USER = 'CHANGE_USER';
export function fetch = () => async (dispatch) => {
  const headers = new Headers();
  headers.append('Content-Type', 'application/json');
  try {
    const res = fetch('http://example.com/me', {
      credentials: 'include'
      headers
    });
    const response = await res.json();
    if (res.statusCode === 200) {
      dispatch({
        type: FETCH_USER_COMPLETE
        response
      });
    } else {
      dispatch({
        type: FETCH_USER_ERROR,
        statusCode: res.statusCode,
        error: new Error(response.message)
      });
    }
 } catch (e) {
    // No response
    dispatch({
      type: FETCH_USER_ERROR
      error: new Error('Could not connect to API');
    });
  }
}
export function change = (path, value) => {
  type: CHANGE_USER,
  path,
  value
};
// reducers.js
import {
  FETCH_USER_INITIATE,
  FETCH_USER_COMPLETE,
  FETCH_USER_ERROR,
  CHANGE_USER
} from './actions';
export default function(state = undefined, action) {
  switch (action.type) {
    case FETCH_USER_INITIATE:
    case FETCH_USER_COMPLETE:
    case FETCH_USER_ERROR:
      return {
        ...state,
        ...action.response,
        error: action.error,
        statusCode: action.statusCode
      };
    case CHANGE_USER:
      return {
        ...state,
        [action.path]: action.value
      };
      break;
    default:
      return state;
  }
}
// UserForm.jsx
import React from 'react';
class UserForm extends React.PureComponent {
  
  constructor(props) {
    super(props);
    
    this.state = {
      edit: false
    };
  }
  componentDidMount() {
    this.props.onFetch();
  }
 
  toggleEdit = () => {
    this.setState({
      edit: !this.state.edit
    };
  };
  render() {
    const {
      user,
      onChange
    } = this.props;

    return (
      this.state.edit ?
        <EntityData source={ user } onChange={ onChange }>
          <h1>Profile</h1>
          
          <EntityStringField label="Name" path="name" />
          <EntityStringField label="E-mail" path="email" />
        </EntityData>
      :
      <div className="user-form">
        <h1>
          Profile 
          (<a onClick={ this.toggleEdit }>Edit</a>)
        </h1>
        
        <label>Name:</label> { user.name }
        <label>E-mail: { user.email }
      </div>
    );
  }
  
}
const mapStateToProps = state => {
  return {
    user: state.user
  };
}
const mapDispatchToProps = dispatch => {
  onFetch: (...args) => dispatch(actions.fetch(...args)),
  onChange: (...args) => dispatch(actions.change(...args))
};
export default connect(mapStateToProps, mapDispatchToProps)(UserForm);

Это было не так уж и плохо! И это было ~ 140 строк кода. Мы поместили данные в Redux, и состояние контролирует работу представления. Затем некоторая компонентная логика для обработки действий пользователя. И формы используют компоненты ввода, как описано в первой статье, чтобы уменьшить шаблон самой формы, вероятно, уменьшив его более чем до половины объема кода. Конечно, это просто пример, вероятно, не то, как кто-то на самом деле это реализовал бы. Но чтобы показать разницу.

Состояние объекта

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

Примеры метаданных, которые нам нужны для таких процессов:

  • Время получения данных
  • Исходные данные и измененные значения рядом
  • Флаг, указывающий, извлекаются или обновляются данные (для просмотра счетчиков и т. Д.)
  • Сообщения об ошибках, индивидуально для каждого поля

Это привело к структуре данных, которую я называю« Entity State », которая представляет собой набор данных со связанными метаданными. Для получения дополнительной информации см. README сущность-состояние.

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

Сущность-государство не зависит от структуры.

React Entity Data

Поскольку мы используем React и Redux, и я использовал это в нескольких других проектах, я стандартизировал логику для этого в наборе вспомогательных функций и компонентов в библиотеке response-entity-data . Это включает компонент оболочки EntityData, как описано в первой статье, включая возможность вложения. Он также включает действия и редукторы Redux, которые упрощают написание кода для этих типов представлений в приложениях React.

Я продолжаю развивать обе библиотеки по мере необходимости для достижения описанных здесь эффектов.

Повторное использование кода для событий

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

  • Показать, какие из полей были изменены (до нажатия кнопки сохранения)
  • Отправьте изменения обратно на сервер

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

// actions.js
import { Http } from 'entity-state';
import { ReduxAC } from 'react-entity-data';
export const FETCH_USER = 'FETCH_USER';
export const TOGGLE_EDIT_USER = 'TOGGLE_EDIT_USER';
export const CHANGE_USER = 'CHANGE_USER';
export const UPDATE_USER = 'UPDATE_USER';
export const fetch = ReduxAC.httpRequest(
  FETCH_USER, 
  () => Http.get('http://example.com/me'),
  {
    loading: true,
    loadResponse: true
  }
);
export const toggleEdit = ReduxAC.toggleMode(TOGGLE_EDIT_USER, 'edit');
export const change = ReduxAC.stage(CHANGE_USER);
export const update = ReduxAC.httpRequest(
  UPDATE_USER,
  user=> Http.put(`http://example.com/users/${user.id}`,
  {
    updating: true,
    loadResponse: true,
    clean: true
  }
);
// reducers.js
import { EntityState, ReduxReducers } from 'react-entity-data';
import {
  FETCH_USER,
  TOGGLE_EDIT_USER,
  CHANGE_USER,
  UPDATE_USER
} from './actions';
export default ReduxReducers.createReducer(EntityState.initialize(), {
  [FETCH_USER]: ReduxReducers.httpRequest,
  [TOGGLE_EDIT_USER]: ReduxReducers.toggleMode,
  [CHANGE_USER]: ReduxReducers.stage,
  [UPDATE_USER]: ReduxReducers.httpRequest
});
// UserForm.jsx
class UserForm extends React.PureComponent {
  render() {
    const {
      userState,
      onToggleEdit,
      onChange,
      onUpdate
    } = this.props;

    return (
      userState.mode === 'edit' ?
        <EntityData state={ userState } onChange={ onChange }>
          <h1>Profile</h1>
          
          <EntityStringField label="Name" path="name" />
          <EntityStringField label="E-mail" path="email" />
          <button onClick={ onUpdate }>Save</button>
        </EntityData>
      :
      <div className="user-form">
        <h1>
          Profile 
          (<a onClick={ onToggleEdit }>Edit</a>)
        </h1>
        
        <label>Name:</label> { user.name }
        <label>E-mail: { user.email }
      </div>
    );
  }
  
}
const mapStateToProps = state => {
  return {
    userState: state.user
  };
}
const mapDispatchToProps = dispatch => {
  onFetch: () => dispatch(actions.fetch()),
  onToggleEdit: () => dispatch(actions.toggleEdit()),
  onChange: (...args) => dispatch(actions.change(...args)),
  onUpdate: (...args) => dispatch(actions.update(...args))
};
export default connect(mapStateToProps, mapDispatchToProps)(UserForm);

Это ~ 90 строк. Wohoo !! Вот что изменилось:

  1. Все действия заменяются результатами вызова ReduxAC

ReduxAC содержит функции, которые создают создателей действий. То есть, если быть точным, «создателей действий». Затем каждый создатель действия может быть вызван из другого действия для создания более сложных действий в каждом случае. Как например:

export const onSubmit = user => async dispatch => {
  try {
    await dispatch(update(user));
  } catch(e) {
    dispatch(ReduxAC.error(USER_ERROR)(e));
  }
  dispatch(toggleEdit());
}

2. Редуктор построен с вспомогательными функциями.

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

const initialState = EntityState.initialize();
export default function user(state = initialState, action) {
  switch (action.type) {
    case FETCH_USER:
    case UPDATE_USER:
      return ReduxReducers.httpRequest(state, action);
    case CHANGE_USER:
      return ReduxReducers.state(state, action);
  }
};

Просто и гибко, потому что все функции ReduxReducers сами являются редукторами, т.е. они получают state и action и возвращают новое состояние. ReduxReducers.createReducer поэтому создает новый редуктор, который запускает данные редукторы для заданных типов действий и в противном случае возвращает состояние нетронутым.

3. Переключатель состояния редактирования перемещен в Redux.

Используя метаданные mode из Entity State, можно легко переключаться между режимом редактирования и обычным.

4. Обновление выполняется с помощью той же многоразовой функции для HTTP-запросов, что и при выборке. Он также содержит ряд других проверок и обработки ошибок, что не является первым примером.

Масштабирование

Структура данных в приведенном выше примере представляет собой единый плоский объект. Но, как известно всем фронтенд-кодерам, вся логика как для отображения, так и для обработки данных может стать значительно более сложной и уродливой, когда кто-то начинает иметь дело с более сложными структурами. Например, вы загрузили с сервера список клиентов (массив объектов компании) и собираетесь редактировать одного из них. Есть несколько способов сделать это.

  1. Отдельная копия

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

2. Редактирование по индексу

Когда вы начинаете редактирование, вы можете разместить индекс, который говорит, какой элемент в массиве редактируется, например, в переменной состояния компонента. Это позволяет зрителю отображать форму, когда вы находитесь в цикле итерации объектов (в списке и т.п.), в то время как остальное отображается обычным образом. Логика для этого может быстро стать немного некрасивой, и, если вы не создадите объект «ключ-значение», только один элемент можно редактировать за раз.

3. EntityData итерация

Поскольку EntityData основывается на React Context, существует решение, которое позволяет вам вручную перемещаться по объектам в коде отображения и может легко извлекать и изменять режим редактирования (и другие метаданные, относящиеся к каждому объекту, а не ко всему массиву) . Вот упрощенный пример:

import { EntityDataContext } from 'react-entity-data';
class CompanyItem extends React.PureComponent {
  static contextType = EntityDataContext;
  render() {
    return this.context.mode === 'edit' ?
      <form>
        ...
      </form>
      :
      <div>
        ...
      </div>
    );
  }
}
class CompanyList extends React.PureComponent {
  render() {
    return (
      <EntityData state={ companyListState } iterate>
        { company => <CompanyItem company={ company } /> }
      </EntityData>
    );
  }
}

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

{
  data: [
    { id: 123, name: 'Company 1' }
    { id: 234, name: 'Company 2' }
  ],
  pathMode: {
    1: 'edit'
  }
}

Это означает, что данные по пути 1 (который является вторым объектом компании) редактируются. Таким образом, этот объект состояния сущности (включая остальные метаданные) является объектом, который EntityData получает через свойство state. Но поскольку установлено iterate, функция рендеринга в дочерних элементах будет получать по одной компании за раз и вызываться для всех объектов в наборе данных. И они будут предоставлять EntityDataContext для подкомпонентов, как если бы они были заключены в EntityData, который получил это:

{
  data: { id: 234, name: 'Company 2' }
  mode: 'edit'
}

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

Глубокие тропы

Способ отступа в структурах, показанных выше, с помощью iterate также может быть выполнен путем указания любого пути. Например, возьмем такую ​​структуру:

const example = {
  data: {
    id: 1,
    name: 'Our company'
    location: {
      address: 'The streets',
      city: {
        id: 45,
        name: 'Example city
      }
    }
  }
}

Здесь контекст EntityData для форм или других представлений может быть выполнен следующим образом:

<EntityData state={ example }>
  <h1>Company</h1> 
  <EntityStringField label="Company name" path="name" />
  <h1>Location</h1>
  
  <EntityData path="location.city">
    ...
  </EntityData>
</EntityData>

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

Дополнительные метаданные, предоставляемые компонентам, заключенным в withEntityData

Подобно тому, что было описано в первой статье, EntityData предоставляет компоненты, обернутые с помощью withEntityData, с тем, что имеет отношение к этому пути, поэтому вам не нужно предоставлять каждому компоненту ввода с ошибкой, режимом и всеми остальными, основанными на пути метаданные из состояния. Как в этом примере:

const state = {
  data: {
    name: 'John Doe',
    email: '[email protected]'
  },
  pathChange: {
    email: '[email protected]'
  }
};
function StringField({ value, changed, error }) {
  return (...);
}
const EntityStringField = withEntityData(StringField);
function UserForm({ user }) {
  return (
    <EntityData state={ state }>
      <EntityStringField label="Name" path="name" />
      <EntityStringField label="E-mail" path="email" />
    </EntityData>
  );
}

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

Проверка

Я также включил возможность проверки данных с помощью Json Schema и то, что функция проверки вставляет ошибки пути в состояние объекта, чтобы ошибки проверки могли отображаться непосредственно под каждым полем. Компоненты многоразового поля (аналогичные примеру из предыдущей статьи) могут затем отображать сообщение об ошибке для каждого поля без необходимости получать какие-либо дополнительные свойства из самой формы.

Цель этой стандартизации обработки данных и связанной с ними логики - упростить код и уменьшить количество ошибок без ограничения гибкости. На момент написания библиотеки охватывают довольно много основных функций для обработки данных, и, вероятно, со временем будет добавлено больше. Давайте поработаем над тремя S: Стандартизация, Масштабируемость и Упрощение!