DevGang
Авторизоваться

Сделайте React более реактивным с помощью RxJs Observer: Шаблон проектирования

Привет, разработчики, мы рассмотрим статью об использовании шаблонов проектирования в React.

И сегодня мы поговорим о шаблоне проектирования Observer в контексте React.

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

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

Angular по умолчанию использует шаблон проектирования Observer, адаптируя RxJs к фреймворку. RxJS — это библиотека, которая реализует шаблон проектирования Observer с реактивным программированием. В этой статье мы не будем углубляться в библиотеку RxJs, а скорее найдем несколько творческих идей о том, как ее можно использовать в вашем приложении React, чтобы сделать его еще более реактивным. Прилагаем дополнительно для вас статью о том, как сделать динамическое приглашение с React при каждом обновлении вашего проекта.

Улучшение обработки событий в React с помощью Observer

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

С RxJS вы можете использовать функцию fromEvent для создания наблюдаемого объекта, который выдает значение поля ввода поиска каждый раз, когда пользователь вводит новый символ. Затем вы можете использовать такие операторы, как debounceTime, DifferentUntilChanged и switchMap, для управления потоком данных и обеспечения отображения пользователю только самых последних и релевантных данных.

const getProductsBySearch = () => {
  return Promise.resolve([
    { id: "1", name: "Bob" },
    { id: "2", name: "John" }
  ]);
};
function ProductList() {
  const [products, setProducts] = useState([]);
  const searchInputRef = useRef(null);

  useEffect(() => {
    const searchInput = searchInputRef.current;

    const search$ = fromEvent(searchInput, "input").pipe(
      debounceTime(500),
      map((event) => event.target.value),
      distinctUntilChanged(), // filters out if new value is not unique
      switchMap((query) => {  // change stream from input value to search response
        if (query.trim() === "") {
          return of([]);
        } else {
          return getProductsBySearch(query);
        }
      })
    );

    const subscription = search$.subscribe((products) => {
      setProducts(products);
    });

    return () => {
      subscription.unsubscribe();
    };
  }, []);

  return (
    <div>
      <input type="text" id="searchInput" ref={searchInputRef} />
      <ul>
        {products.map((product) => (
          <li key={product?.id}>{product?.name}</li>
        ))}
      </ul>
    </div>
  );
}

В этом примере мы создаем наблюдаемый search$, который прослушивает поисковый ввод пользователя, ждет 500ms между нажатиями клавиш и выдает новый поисковый запрос, только если он изменился. Затем мы используем операторы RxJS для переключения на новый наблюдаемый поток, который извлекает продукты, соответствующие поисковому запросу.

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

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

Использование RxJs в React для проверки сложной формы

Еще один очень мощный пример использования RxJs в качестве наблюдаемого шаблона в React — проверка формы. В нашем сценарии у нас есть три разных поля формы: name, email и password. У каждого из них есть условие проверки, и функция validateInput проверит, действительно ли поле ввода, чтобы показать ошибку. Вот как выглядит код:

const submitFormToServer = () => Promise.resolve({});

function validateInput(name, value) {
  let error = "";
  if (name === "name") {
    if (!value) {
      error = "Name is required";
    } else if (value.length < 3) {
      error = "Name must be at least 3 characters";
    }
  } else if (name === "email") {
    if (!value) {
      error = "Email is required";
    } else if (!/\S+@\S+\.\S+/.test(value)) {
      error = "Email is invalid";
    }
  } else if (name === "password") {
    if (!value) {
      error = "Password is required";
    } else if (value.length < 8) {
      error = "Password must be at least 8 characters";
    }
  }
  return error;
}

function Form() {
  const [formData, setFormData] = useState({});
  const [formErrors, setFormErrors] = useState({});

  const nameRef = useRef(null);
  const emailRef = useRef(null);
  const passwordRef = useRef(null);
  const submitRef = useRef(null);

  useEffect(() => {
    const subscription = new Subscription();
    const name$ = fromEvent(nameRef.current, "input").pipe(
      pluck("target", "value"), // pluck will distuct the value from the object
      map((value) => ({ name: "name", value }))
    );

    const email$ = fromEvent(emailRef.current, "input").pipe(
      pluck("target", "value"),
      map((value) => ({ name: "email", value }))
    );

    const password$ = fromEvent(passwordRef.current, "input").pipe(
      pluck("target", "value"),
      map((value) => ({ name: "password", value }))
    );

    const submit$ = fromEvent(submitRef.current, "click").pipe(
      startWith(null),
      mapTo({ isSubmit: true })
    );

    const form$ = combineLatest(name$, email$, password$); // combine latest will emit the value when each of the observer is completed 

    const formSub = merge(name$, email$, password$) // merge will emit when any is changed
      .pipe(
        tap((input) => {
          const { name, value } = input;
          setFormData((data) => ({ ...data, [name]: value }));
          setFormErrors((errors) => ({
            ...errors,
            [name]: validateInput(name, value)
          }));
        })
      )
      .subscribe();

    const submitSub = submit$
      .pipe(withLatestFrom(form$), tap(([, formData]) => {
        const hasError = formData.some(
          ({ name, value }) => !!validateInput(name, value)
        ); // check if any field has error
        if(!hasError){
          submitFormToServer(formData).then();
        }
      }))
      .subscribe();

    subscription.add(formSub); // we can add multiple subscription to the Subscription object, to unsubscribe in one call
    subscription.add(submitSub);

    return () => subscription.unsubscribe();
  }, []);

  return (
    <div>
      <input type="text" id="name" placeholder="Name" ref={nameRef} />
      {formErrors.name && <span>{formErrors.name}</span>}
      <br />
      <input type="text" id="email" placeholder="Email" ref={emailRef} />
      {formErrors.email && <span>{formErrors.email}</span>}
      <br />
      <input
        type="password"
        id="password"
        placeholder="Password"
        ref={passwordRef}
      />
      {formErrors.password && <span>{formErrors.password}</span>}
      <br />
      <button type="button" id="submit" ref={submitRef}>
        Submit
      </button>
    </div>
  );
}

В приведенном примере мы строим довольно сложную логику, используя RxJs Observers. Мы можем еще больше улучшить его, добавив дополнительные операторы такие как differentUntilChanged, выдающие только в том случае, если новое значение уникально, или debounceTime, чтобы улучшить производительность проверки ввода. RxJs дает нам большую гибкость, единственное, не забывайте отписываться, чтобы очистить ваши наблюдаемые.

Использование RxJs для обработки состояния в React

RxJs могут улучшить обработку состояния в React, чтобы добавить дополнительный контроль и мощность операторов RxJs. Давайте посмотрим пример.

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

function Chat() {
  const [message, setMessage] = useState('');
  const [messages, setMessages] = useState([]);
  const [message$] = useState(new Subject()); // Subject creates empty Observable
  // Note, we need to use useState for our observable to trigger reconciliation after calling message$.next  
  useEffect(() => {
    const messages$ = message$.pipe(
      scan((acc, curr) => [...acc, curr], []) // scan lets you dynamically update the state in Redux fashion
    );

    const subscription = messages$.subscribe(newMessages => {
      setMessages(newMessages);
    });

    return () => {
      subscription.unsubscribe();
    };
  }, []);

  const handleMessageSend = () => {
    message$.next({
      user: 'Me',
      text: message,
      timestamp: Date.now(),
    });
    setMessage('');
  };

  return (
    <div>
      <ul>
        {messages.map((msg, i) => (
          <li key={i}>
            {msg.user}: {msg.text}
          </li>
        ))}
      </ul>
      <input type="text" value={message} onChange={e => setMessage(e.target.value)} />
      <button onClick={handleMessageSend}>Send</button>
    </div>
  );
}

Здесь мы создаем новую тему с именем message$ для обработки входящих сообщений. Мы также создаем новый Observable с именем messages$, используя оператор scan() для накопления входящих сообщений в массив.

В хуке useEffect мы подписываемся на messages$ Observable и обновляем список сообщений всякий раз, когда приходит новое сообщение. Когда компонент размонтирован, мы отписываемся от Observable, чтобы предотвратить утечку памяти.

В функции handleMessageSend мы отправляем новое сообщение, вызывая message$.next() с новым объектом сообщения, содержащим текст сообщения, имя пользователя и отметку времени. Мы также очищаем поле ввода сообщения, вызывая setMessage('').

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

RxJs и Custom Hook — лучший способ проектирования состояния React

Взяв предыдущий пример, мы рискуем открыть компоненту слишком много логики. Это может привести к ускорению кода компонента, особенно когда мы имеем дело со многими наблюдаемыми. Лучший дизайн — извлечь наше управляемое состояние RxJs в Custom Hook, давайте посмотрим на пример.

function useMessageObservable() {
  const [messages, setMessages] = useState([]);
  const [message$] = useState(new Subject()); // Subject creates empty Observable
  // Note, we need to use useState for our observable to trigger reconciliation after calling message$.next  
  useEffect(() => {
    const messages$ = message$.pipe(
      scan((acc, curr) => [...acc, curr], []) // scan lets you dynamically update the state in Redux fashion
    );

    const subscription = messages$.subscribe(newMessages => {
      setMessages(newMessages);
    });

    return () => {
      subscription.unsubscribe();
    };
  }, []);

  return {message$, messages};
}

Позже мы можем обновить наш компонент Chat:

function Chat() {
  const [message, setMessage] = useState('');
  const {message$, messages} = useMessageObservable()
 
  const handleMessageSend = () => {
    message$.next({
      user: 'Me',
      text: message,
      timestamp: Date.now(),
    });
    setMessage('');
  };

  return (
    <div>
      <ul>
        {messages.map((msg, i) => (
          <li key={i}>
            {msg.user}: {msg.text}
          </li>
        ))}
      </ul>
      <input type="text" value={message} onChange={e => setMessage(e.target.value)} />
      <button onClick={handleMessageSend}>Send</button>
    </div>
  );
}

Таким образом, мы инкапсулируем наше наблюдаемое состояние.

Redux, Context + RxJs Observable в React

Чтобы лучше понять, можем ли мы получить какие-либо преимущества от использования RxJs Observable в связке с Redux в React, мы должны прояснить основную идею Redux.

Redux — это библиотека управления состоянием, которая широко используется в приложениях React. Состояние часто разбросано по нескольким компонентам и может стать сложным и трудным для управления по мере роста приложения. Redux помогает решить эту проблему, предоставляя единый источник достоверной информации о состоянии приложения, к которому может получить доступ любой компонент.

Другими словами, он служит для совместного использования и кэширования состояния, давайте пока запомним это утверждение. Позже в React был представлен Context, и вместе с useReducer хук постепенно заменяет цель использования Redux. Так как делал то же самое, что и библиотека Redux, но нативно, без добавления дополнительной библиотеки. Итак, где RxJs Observable стоит в концепции управления состоянием. Мы бы предпочел думать об этом как об улучшении управления состоянием, а не как о замене.

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

// Define a context to expose the message state to child components
const MessageContext = createContext();

// Define a provider component that listens for messages
function MessageProvider({ children }) {
  // Define the initial state of the chat
  const {message$, messages} = useMessageObservable()

  return <MessageContext.Provider value={{message$, messages}}>{children}</MessageContext.Provider>;
}

После включения нашего родительского компонента в MessageProvider мы можем получить доступ к контексту состояния из его дочерних элементов:

function Chat() {
  const [message, setMessage] = useState('');
  const {message$, messages} = useContext(MessageContext);
 
  ...
}

Заключение

Изучая некоторые творческие идеи, вы можете использовать этот шаблон в своем приложении React, чтобы сделать его еще более реактивным. Мы предоставляем еще одну статью на тему отправки кроссплатформенных нативных уведомлений в Node JS для вашего ознакомления, тема тоже довольно интересна. Несмотря на то, что библиотека RxJs активно используется в Angular, это не значит, что вы не можете использовать лучшее от конкурента React.

#JavaScript #React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться