網站搜尋

reactjs - 使用 React Hooks 的值流


我有來自伺服器的值 X。我想要提供一個類似的介面

interface Xclient {
    getX(): Promise<X>
}

我稍後會從我的 React 函數組件中使用它。

我說相似是因為在幕後我希望它能夠:

  1. 從儲存中第一次返回的值(它是 react-native)
  2. 同時發送網路請求以獲取 X 的新版本,並在收到回應後重新渲染元件

所以與其使用 Promise,我可能需要 Observable。但是,接下來如何將它一般地與 React 使用,以及特別是在 React Hooks 中使用呢?

我來自後端背景,所以可能有些更規範的方法我不太了解。如果可能的話,我真的不想使用 redux!

答案#1

如果我理解正確,你有兩個數據來源(本地存儲和你的 API),你想先從本地存儲獲取值,然後再從 API 獲取實際值。所以,你應該這樣做:

import { useState } from "react";

const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();

export function SimpleView() {
  const [state, setState] = useState("default value");

  localProvider()
    .then((response) => {
        setState(response);

        apiProvider()
            .then((actualResponse) => {
                setState(actualResponse);
            })
            .catch(/* */);
    })
    .catch(/* */);
}

但我看不出有理由要同步調用它,你可以想要並行運行它:

import { useState } from "react";

const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();

export function SimpleView() {
  const [state, setState] = useState("default value");

  localProvider()
    .then((response) => {
        setState(response);
    })
    .catch(/* */);

  apiProvider()
    .then((actualResponse) => {
        setState(actualResponse);
    })
    .catch(/* */);
}

如果你想封裝這個邏輯,你可以做一個像這樣的函數:

import { useState } from "react";

const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();

function getValue(localProvider, apiProvider, consumer) {
  localProvider()
    .then((response) => {
        consumer(response);
    })
    .catch(/* */);

  apiProvider()
    .then((actualResponse) => {
        consumer(actualResponse);
    })
    .catch(/* */);
}

export function SimpleView() {
  const [state, setState] = useState("default value");

  getValue(localProvider, apiProvider, setState);
}

更新:正如 @PatrickRoberts 正確指出的,我的範例包含競態條件,這段程式碼解決了它:

import { useState } from "react";

const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();

function getValue(localProvider, apiProvider, consumer) {
  let wasApiResolved = false;

  localProvider()
    .then((response) => {
        if (!wasApiResolved) {
            consumer(response);
        }
    })
    .catch(/* */);

  apiProvider()
    .then((actualResponse) => {
        wasApiResolved = true;
        consumer(actualResponse);
    })
    .catch(/* */);
}

export function SimpleView() {
  const [state, setState] = useState("default value");

  getValue(localProvider, apiProvider, setState);
}

答案#2

我個人會為此撰寫一個自訂 hook,以封裝 React 的 useReducer()。

這種方法的靈感來自函數 Object.assign() 的簽名,因為 sources 剩餘參數會在每個 Promise 解決時,優先使用後面的來源而不是前面的來源:

import { useEffect, useReducer, useState } from 'react';

function useSources<T> (initialValue: T, ...sources: (() => Promise<T>)[]) {
  const [fetches] = useState(sources);
  const [state, dispatch] = useReducer(
    (oldState: [T, number], newState: [T, number]) =>
      oldState[1] < newState[1] ? newState : oldState,
    [initialValue, -1]
  );

  useEffect(() => {
    let mounted = true;

    fetches.forEach(
      (fetch, index) => fetch().then(value => {
        if (mounted) dispatch([value, index]);
      });
    );

    return () => { mounted = false; };
  }, [fetches, dispatch]);

  return state;
}

每當一個 promise 解決時,這會自動更新 state,使其參考最新可用來源的結果,以及提供該值的來源的索引。

我們加入 const [fetches]=useState(sources); 的原因,是為了讓對陣列 fetches 的參考在呼叫 useSources() 鉤子的組件重新渲染時保持不變。

如果你不介意元件在其生命週期內可能對每個來源發出多次請求,那麼這一行可以改為 const fetches=useMemo(() => sources);,因為 useMemo() 在語意上並不保證一定會記憶化。這在文件中有說明:

您可以依賴 useMemo 作為效能優化手段,而不是語意上的保證。 將來,React 可能會選擇「忘記」一些之前已快取的值,並在下一次渲染時重新計算它們,例如為了釋放離屏組件的記憶體。

這裡是一個使用 useSources() hook 的範例:

const storageClient: Xclient = new StorageXclient();
const networkClient: Xclient = new NetworkXclient();

export default () => {
  const [x, xIndex] = useSources(
    'default value',
    () => storageClient.getX(),
    () => networkClient.getX()
  );

  // xIndex indicates the index of the source from which x is currently loaded
  // or -1 if x references the default value
};