Как обновлять состояние с помощью крючков реакции и наблюдаемого шаблона проектирования?

#javascript #reactjs #design-patterns #observable

Вопрос:

Вот ссылка для codesandbox

Привет, друзья, я изучаю наблюдаемый шаблон проектирования. Я пытаюсь отобразить обновление цены биткойна, но в настоящее время я получаю price as undefined . У меня вызывается API, а также создаются наблюдаемые методы для subscribe и notify . У меня есть наблюдатель, создаваемый как конструктор из наблюдаемого и пытающийся обновить состояние цены из API. Но цена указана как неопределенная. Не могли бы вы, пожалуйста, помочь мне, где я ошибаюсь ? Спасибо

 export function Observable() {
this.observers = []; //array of observer functions
}

Observable.prototype = {
subscribe: function (fn) {
this.observers.push(fn);
},
fire: function () {
 this.observers.forEach((fn) => {
   fn.call();
  });
 }
};

import React from "react";
import { getBitcoinPrice } from "./api";
import { Observable } from "./observable";

const CryptoPrice = () => {
const [price, setPrice] = React.useState(0);

React.useEffect(() => {
  const bitcoinObservable = new Observable();
  console.log({ bitcoinObservable });
  let livePrice = bitcoinObservable.subscribe(getBitcoinPrice);
  setPrice(livePrice);
  console.log({ livePrice });
  let newPrice = bitcoinObservable.fire();
  console.log({ newPrice });
}, [price]);

return (
  <>
    <h3>{price ? price : "undefined"}</h3>
  </>
 );
};

export default CryptoPrice;
 

Ответ №1:

С кодом было несколько проблем. Позвольте мне начать с реализации наблюдаемых:

  1. Огонь не передает значение наблюдателям! Их уведомляют только о том, что «что — то произошло», но не о том, что именно, — чтобы наблюдатели никогда не услышали фактическое price .
  2. Вам определенно нужен unsubscribe метод!

Так:

 export function Observable() {
  this.observers = []; //array of observer functions
}

Observable.prototype = {
  subscribe: function (fn) {
    this.observers.push(fn);
  },
  unsubscribe: function (fn) {
    const index = this.observers.findIndex(fn);
    if (index >= 0) {
      this.observers.splice(index, 1);
    }
  },
  fire: function (x) {
    this.observers.forEach((fn) => {
      fn(x);
    });
  }
};
 

Несколько вещей с getBitcoinPrice :

  1. const data = await response.data; это излишне. Вы await на обещаниях и только axios.get(url) на обещаниях. После этого response.data это реальный объект, просто используйте его. Никакого вреда, однако, не причинено, если вы ждете невыполнения обещания, оно просто запускается немедленно.
  2. Я увидел, что на самом data.ticker.price деле это строка, поэтому изменил ее data.ticker.price на число. Опять же, в этом приложении нет вреда, но я думаю, что это лучше в контексте более крупного приложения.

Так:

 export const getBitcoinPrice = async () => {
  const url = `https://api.cryptonator.com/api/ticker/btc-usd`;
  try {
    const response = await axios.get(url);
    console.log('[getBitcoinPrice]', response.data.ticker.price);
    return  response.data.ticker.price;
  } catch (error) {
    console.log("[getBitcoinPrice] error: ", error);
  }
};
 

Теперь перейдем к сути, к CryptoPrice компоненту. Следующий способ-это один из способов сделать это, я уверен, что есть лучшие. В любом случае, я разделил обработку на 2 этапа: один-периодическая выборка цены и запуск наблюдаемого. Другой на самом деле наблюдает за изменением и производит эффект (здесь обновляется дисплей). Обратите внимание на использование функции очистки React useEffect — никогда не забывайте освобождать ресурсы!

 const CryptoPrice = () => {
  const [price, setPrice] = React.useState(0);
  const [bitcoinObservable] = React.useState(() => new Observable());

  // this effect is responsible for triggering the price load periodically
  React.useEffect(() => {
    const interval = setInterval(async () => {
      const x = await getBitcoinPrice();
      bitcoinObservable.fire(x);
    }, 5000);
    // the cleanup function of the effect, never forget!!!
    return () => {
      clearInterval(interval);
    };
  }, [bitcoinObservable]);

  // this effect is responsible for the subscription
  React.useEffect(() => {
    const handler = function(price) {
      console.log('[CryptoPrice]', price);
      setPrice(price);
    };
    bitcoinObservable.subscribe(handler);
    return () => {
      bitcoinObservable.unsubscribe(handler);
    };
  }, [bitcoinObservable]);

  return (
    <>
      <h3>{typeof price === 'number' ? price : "undefined"}</h3>
    </>
  );
};
 

И последнее, ноль является ложным в Javascript, поэтому price ? price : 'undefined' будет давать неопределенную начальную цену 0 . Отсюда и проверка типа typeof price === 'number' ? ... : ... .

Еще пара вещей/указателей:

  • Я бы предпочел иметь «1-ю фазу», т. е. создание «наблюдаемой цены биткойна» за пределами компонента, возможно, в сервисе. В основном разделение забот.
  • Если вы используете инфраструктуру управления состоянием, такую как Redux, «служба» выше будет своего рода промежуточным программным обеспечением.
  • Очень интересным обобщением обещаний и наблюдаемой закономерности являются реактивные потоки. Поначалу может быть трудно понять, но в долгосрочной перспективе они очень сильны.
  • Если вы объедините Redux RxJS, проверьте redux-наблюдаемый.

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

1. В вашем ответе содержится много информации, о которой я не знал. Спасибо. Но у меня есть несколько вопросов 1. Почему мы устанавливаем государство таким образом ? Я не вижу здесь Сетстейта. const [bitcoinObservable] = React.useState(() => new Observable()); 2. Добавляя символ к data.ticker.price тому, как он становится числом ? 3. Используем ли мы setInterval для моделирования наблюдаемого поведения ? или это то, как наблюдаемые тоже работают в реальном времени, как вызов через некоторый интервал? Разве это не должно называться только тогда, когда есть изменения ?

2. И тебе тоже спасибо. 1. Это стандартно, но используется реже: меня интересует только начальное состояние, поэтому я выбрасываю ..., setBitcoinObservable] часть массива. И, используя функцию в качестве инициализатора, я гарантирую, что за время существования этого компонента будет создан только 1 наблюдаемый объект — setState не будет вызывать функцию инициализатора при последующих отображениях. Я делаю это, потому что не хочу создавать Observable приложение, чтобы просто выбрасывать его при каждом рендеринге.

3. 2. Это стандартный «трюк» JS для преобразования числовой строки в фактическое число. Например, см. MDN . 3. Я использовал setInterval , потому что предполагал, что вы хотите, чтобы цена периодически обновлялась. Я предположил это, потому что это причина для фактического использования наблюдаемого, чтобы наблюдать изменения. Наблюдаемое, которое никогда не меняет значения, было бы действительно печальным наблюдаемым 😀

4. Спасибо вам за разъяснения. Но разве наблюдаемое уже не делает этого, например, обновляет цену всякий раз, когда происходит изменение, вместо того, чтобы явно использовать setInterval ? или это просто для того, чтобы пользовательский интерфейс видел новые цены ?

5. Нет, я думаю, вы неправильно поняли 🙂 useEffect Зависит от цены и получает новую цену. «Зависит» означает, что при CryptoPrice каждом отображении React будет сравнивать текущую цену с предыдущей. Если изменить, он useEffect снова запускает тело. Начальная цена равна нулю,а при первом getBitcoinPrice возврате она ненулевая. useEffect чувствует это изменение и getBitcoinPrice снова срабатывает. Если во второй раз цена останется прежней (а это так и есть), дальнейшие обновления не будут запущены!

Ответ №2:

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

Теперь перейдем к вашему коду.

1. Вы перепутали подписку и эмиссию ( fire в вашем случае)

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

 export function Observable() {
  this.observers = []; //array of observer functions
}

Observable.prototype = {
  subscribe: function (fn) {
    this.observers.push(fn);
  },
  // add parameter value
  fire: function (value) {
    this.observers.forEach((fn) => {
      // call the observer callbacks directly with value
      fn(value);
    });
  }
};
 

2. Вы никогда не «запускаете» свой наблюдаемый результат с помощью API.

Вместо этого вы передали функции Обещание, которое возвращает цену биткойна subscribe . Как было сказано ранее, таким образом, ни один другой наблюдатель никогда не сможет получить результат API.

Вы должны изменить свой useEffect таким образом:

 React.useEffect(() => {
  const bitcoinObservable = new Observable();

  bitcoinObservable.subscribe((livePrice) => {
    console.log("sub!", { livePrice });
    setPrice(livePrice);
  });

  getBitcoinPrice().then((livePrice) => {
    console.log('fire!', { livePrice });
    bitcoinObservable.fire(livePrice);
  });
}, [])
 

Теперь ваш пример песочницы кода уже работает нормально. Конечно, создание наблюдаемого и подписка на него в рамках одного и того же useEffect вызова не имеет большого смысла. Но если вы хотите использовать свой наблюдаемый объект более чем в одном месте, вам следует сделать еще кое-что:

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

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

 Observable.prototype = {
  subscribe: function (fn) {
    this.observers.push(fn);
    // return an object with the unsubscribe function
    return {
      unsubscribe: () => {
        this.observers.splice(this.observers.indexOf(fn));
      }
    };
  },
  // ...
};
 

Затем в своем useEffect , вы можете отказаться от подписки в рамках обратного вызова очистки эффектов:

 React.useEffect(() => {
  // ...
  // save the return object of the subscribe call
  const subscription = bitcoinObservable.subscribe((livePrice) => {
    console.log("sub!", { livePrice });
    setPrice(livePrice);
  });

  // ...

  // add cleanup logic
  return () => {
    subscription.unsubscribe();
  };
}, []);
 

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

Если вы хотите узнать больше о наблюдаемых объектах, я рекомендую такие статьи, как эта: https://indepth.dev/posts/1155/build-your-own-observable-part-1-arrays

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

1. Спасибо @maximillian за ответ