reactjs - 使用 React Hooks 的值流
我有來自伺服器的值 X。我想要提供一個類似的介面
interface Xclient {
getX(): Promise<X>
}
我稍後會從我的 React 函數組件中使用它。
我說相似是因為在幕後我希望它能夠:
- 從儲存中第一次返回的值(它是 react-native)
- 同時發送網路請求以獲取 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
};
