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

Каков самый элегантный способ управления состоянием компонентов React? Он должен быть удобным в сопровождении, простым в реализации и эффективным.

Работа над следующим шаблоном продолжалась в течение последнего года, и я подумал, что сейчас самое время поделиться им с вами. Он начинался как POC, использующий потоки для рендеринга компонентов React, но использование больших потоковых библиотек того не стоило. Observables оказались хорошей альтернативой.

Почему наблюдаемые? Это простая функция, которая в настоящее время предлагается стать частью языка JavaScript. https://github.com/tc39/proposal-observable.

Состояние React должно быть изолировано по домену, а компоненты должны перерисовываться целенаправленно; должны обновляться только те компоненты, которые используют данные, и никакие другие. Мы не хотим сравнивать данные, проверять изменения или делать что-то еще, что снижает производительность.

Мы делаем это, разделяя наши источники данных на сервисный уровень. У нас будет разная услуга для каждого. тип данных. Например, у нас может быть UserService, который обновляет и извлекает пользовательские данные, или WeatherService, который обновляет информацию о погоде. Для каждого типа данных должен быть отдельный сервис. Например, на блог-сайте может быть служба ArticleService.

Служба в этой парадигме возвращает ‹‹Observable›› при извлечении данных и обычную синхронную операцию при изменении данных. Таким образом, мы гарантируем, что у нас есть единственный источник правды.

Например, типы методов TypeScript будут выглядеть примерно так:

class UserService {
   getUser(id:string):Observable<UserDTO | Error> {}
   updateUser(user:UserDto):void {}
}

Когда мы хотим получить пользователя, мы получаем наблюдаемую обратную связь, которая разрешает один или несколько объектов пользовательских данных (UserDTO). Почему один ИЛИ несколько объектов данных? Это наблюдаемая величина, которая разрешается один или несколько раз, пока мы не отменяем подписку. Всякий раз, когда вызывается метод updateUser, новая обновленная версия будет проталкиваться через наблюдаемую.

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

Эта архитектура соответствует следующим архитектурным принципам:

  • Службы и представления не связаны
  • Благодаря высокой связности легко увидеть, как все работает и как передаются данные. Это проще, чем, например, редукторы, которые также сильно развязаны, но им не хватает связности.
  • Описательные модули; легко определить, что делает каждый компонент.
  • Расширяемый, сервисы легко добавлять в сервисы, не раздувая представления или перехватчики. Бизнес-логика добавляется туда, где ей самое место.
  • Данные могут быть разделены между компонентами, чего не может сделать простой хук выборки.
  • Можно использовать любой бэкенд, этот шаблон ведет к WebSockets, HTTP-запросам и даже к localStorage.

Покажи мне код

Так как же выглядит код? См. пример пользовательской службы ниже.

class UserService {
constructor() {
  this.observers = [];
  this.state = {};
  this.observable = new Observable((observer) => {
    this.observers.push(observer);
    observer.next(this.state);
    return () => {
      this.observers = this.observers.filter((obs) => obs !== observer);
    };
  });
}
write(state): void {
  this.state = state;
  this.observers.forEach((observer) => {
  if (observer && observer.next) observer.next(state);
  });
}
getUser(): Observable<ServiceStatus> {
  const shouldUpdate = !this.observers.length;
  if (shouldUpdate) {
    this.write({ ...this.state, status: Status.Loading });
    this.repo.getUser()
    .then((user) => {
       this.write({
          ...this.state,
          status: Status.Success,
          user,
      })
    })
    .catch((e) => {
      this.write({
        error: e.message,
        status: Status.Error,
        user: undefined,
      });
    });
  }
  return this.observable;
 }
updateUser(data){
  this.write({
    ...this.state,
    status: Status.Success,
    user,
  });
  await this.repo.updateUser()
  // handle errors and roll back the updated user data on failure 
}
}

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

Пример хука

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

const useUserService = (userService) => {
  const [user, setUser] = useState({});
  const [status, setStatus] = useState(Status.Loading);
  
  const update = (newValue)=> userService.updateUser(newValue);

  useEffect(() => {
    const observable = userService.getUser();
    const subscription = observable.subscribe((state) => {
      if (state.status === Status.Error) 
        setUser(null);
        setStatus(Status.Error);
      } else if (state.Status === Status.Success)
        setUser(state.user);
        setStatus(Status.Success);
      }
    });
    return () => {
      subscription.unsubscribe();
    };
  }, []);
  return { user, status, update};
};

Этот крючок также относительно прост. Он обеспечивает связующее звено между компонентом React ниже и сервисом.

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

const UserView = (props) => {
  const { user, status, update} = useUserService();
  if(status.LOADING){
    return <LoadingIndicator /> 
  }
  return (
    <article> 
    <div>{user}</div>
    <button onClick={()=>update(user)} >Save</button>
    </article>
  );
};

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

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

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

Инструмент с открытым исходным кодом Bit помогает более чем 250 000 разработчиков создавать приложения с компонентами.

Превратите любой пользовательский интерфейс, функцию или страницу в компонент многократного использования — и поделитесь им со своими приложениями. Легче сотрудничать и строить быстрее.

Подробнее

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

Микро-интерфейсы

Система дизайна

Совместное использование кода и повторное использование

Монорепо

Узнать больше