[英]Optimistic rendering & useEffect
TL'DR:在使用useEffect進行API調用時,有沒有一種方法來悲觀地操作狀態更改?
假設您編寫了一個顯示分頁/排序的數據網格的組件,並且為該組件編寫了一個useEffect,類似於這個偽造的示例:
useEffect(() => { fetch(`https://some-random-api.com/data/page/{paging}/sort/{sorting}`) .then(response => response.json()) .then(data => setState({ data })); }, [paging, sorting]);
因此,如果我沒記錯的話,這將在更新分頁或排序狀態/屬性(whatev)時獲取。 這意味着它會在加載數據之前樂觀地呈現新的排序和分頁狀態。 基本上,該應用程序告訴用戶“我在請求的頁面上,但是請求仍在排序,但我仍在加載”。 換句話說,應用程序狀態的一部分(頁面調度和排序)說“我完成了”,狀態的另一部分(正在加載)說“我正在處理中”。
相比之下,如果我悲觀地更新狀態,則只會在獲取之前將加載狀態設置為true,然后在收到響應(並且只有在此之后)才設置分頁,排序(&數據和加載)。 因此,該應用程序告訴用戶“我聽到了您的聲音,現在正在處理它”,然后在收到成功的響應時,該應用程序會說“此處是請求的數據和更新的上下文(更新的頁面/排序狀態)”。
另一個問題是請求是否失敗。 在optimisitic / useEffect方法中,我現在需要恢復已更新的分頁和排序狀態,然后再嘗試為這些狀態更改加載數據。 這種復雜性隨依賴關系數量的增加而增加。 另外,這是一個關鍵(同樣,如果我沒有記錯的話),還原這些狀態將導致另一次獲取(可能會再次失敗)。 在悲觀方法中,您只需將發生錯誤時設置loading = false即可,並且沒有其他狀態更改,因為分頁和排序僅在成功接收數據時更新。 數據以悲觀的方式保持同步(不是樂觀的),這就是為什么我認為當使用useEffect時博客和視頻說“在同步方面考慮”時具有諷刺意味。
再說一遍,比如在useEffect中調用的API(在其他示例中)實際上是在服務器端進行更改。 在這種情況下,使用useEffect並應用它時,樂觀的方法只是對用戶說謊。 該應用說“好,我已更新”。 也許不吧。 如果沒有(如果有錯誤),則希望有狀態更新來還原對用戶造成的狀態更改,並在還原該狀態時以某種方式避免額外的API調用。 希望用戶能看到狀態變回,希望有消息...即使那樣,那是一個好的UX嗎?
我並不是說樂觀渲染總是不好的。 我說的是:“我大部分時間都喜歡悲觀。在使用useEffect時該怎么做?” :)
似乎useEffect已被使用和推廣用於應用程序的大多數副作用(很多)。 我不是經常發現它有用。 我是一個悲觀主義者,想啟用其他方法。
您描述它的方式聽起來像組件的其余部分一樣
[state, setState] => useState(initialState)
[paging, setPaging] => useState(initialPaging)
[sorting, setSorting] => useState(initialSort)
useEffect(() => {
fetch(`https://some-random-api.com/data/page/{paging}/sort/{sorting}`)
.then(response => response.json())
.then(data => setState({
data
}));
}, [paging, sorting]);
return <div>
<span>Current page is {paging}</span>
<Button onClick={() => setPaging(page => page + 1)}>Nav Next</Button>
<Button onClick={() => setPaging(page => page - 1)}>Nav Prev</Button>
// ... and so on and so forth
</div>
當您單擊“導航下一個”或“上一個”時,您不希望分頁更新,直到其觸發的效果解決后。 如果抓取失敗,則您不希望用戶看到分頁號,然后再進行所需的清理。
為此,您是否不能通過具有中間狀態“有希望”或“待定”來延遲設置“正向”值? 例如,這行得通嗎?
[state, setState] => useState(initialState)
// Component state values
[paging, setPaging] => useState(initialPaging)
[sorting, setSorting] => useState(initialSort)
// "Optimist's" state values
[pendingPaging, setPendingPaging] => useState(paging)
[pendingSorting, setPendingSorting] => useState(sorting)
useEffect(() => {
// checks if current state values differ from pending,
// meaning app wants to do something
// If pending and "actual" values match, do nothing
if( paging !== pendingPaging
|| sorting !== pendingSorting
){
fetch(`https://some-random-api.com/data/page/{paging}/sort/{sorting}`)
.then(response => response.json())
.then(data => {
// things worked, updated component state to match pending
setPaging(pendingPaging);
setSorting(pendingSorting);
setState({data});
})
.catch(err => {
// ugh, I knew things would fail. Let me reset my hopes
setPendingPaging(paging);
setpendingSorting(sorting);
setState({data});
});
}
}, [pendingPaging, pendingSorting, paging, sorting, setState]);
return <div>
// show user "actual" page number
<span>Current page is {paging}</span>
// but update only pending values in response to events
<Button onClick={() => setPendingPaging(page + 1)}>Nav Next</Button>
<Button onClick={() => setPendingPaging(page - 1)}>Nav Prev</Button>
// ... and so on and so forth
</div>
這樣,您就可以建立對狀態的希望,進行一些操作,然后設置狀態和期望值以匹配。 如果成功,顯示的狀態將更新以匹配樂觀值,如果失敗,則將待處理的值重新設置為現實。
我看到的最大問題是,異步調用解析后會同時進行多個setState調用。 React應該對這些調用進行批處理 ,但是您可能希望在單個調用中查找其他處理方式。
無論哪種方式,待定值和實際值都相等,因此雖然useEffect將觸發,但不會運行其他提取。
編輯1感謝您的積極反饋,很高興您喜歡這個概念。 我同意復雜性可能會變得笨拙,但是我認為您可以設置工廠類來更清楚地管理事情。
例如,您可以使用makePessimistic
類,該類最終輸出“實際”狀態,“希望”狀態和至少一個usePessimisticEffect
回調。 該類可能類似於:
class pessimistFactory {
constructor(){
this.state = {};
this.effects = [];
}
// util from https://dzone.com/articles/how-to-capitalize-the-first-letter-of-a-string-in
ucfirst = (string) => string.charAt(0).toUpperCase() + string.slice(1);
registerState(param, initialVal){
this.state[param] = initialVal;
}
makePessimisticEffect = (callback, dependencies) => useEffect(() => {
async function handlePessimistically(){
try {
const result = await callback(...dependencies);
// update theState with result
setState({...hopefulState, result})
}catch(err){
// reset hopefulState and handle error
setHopefulState({...theState})
}
}
// Do something if some action pending
if(/* pending and current state not the same */) handlePessimistically();
}, [callback, dependencies]);
registerEffect(callback, dependencies = []){
this.effects.push(
this.makePessimisticEffect(callback, dependencies)
);
}
makeHooks = () => {
const initialState = useState(this.state);
return {
theState: initialState,
hopefulState: initialState,
effects: this.effects
}
}
}
在實踐中:
// Make factory instance
const factory = new pessimistFactory();
// Register state and effects
factory.registerState('paging', initialPaging)
factory.registerState('sorting', initialSorting)
factory.registerEffect('fetchData', () => fetch(`https://some-random-api.com/data/page/{paging}/sort/{sorting}`))
// Make the hooks
const {theState, hopefulState, effects} = factory.makeHooks();
// Implement hooks in component
const SomeComponent = () => {
const [state, setState] = theState();
const [pendingState, setPendingState] = hopefulState();
effects.forEach(useEffect => useEffect());
return <div>displayed stuff</div>
}
最棘手的部分是設置makePessimisticEffect
效果,以讀取和處理由生成的setState
創建的值和回調,如果在編寫時完全破壞了這一點,我就會意識到。
我現在沒有時間真正地了解細節,但是我認為必須要有可用的新鈎子,例如useCallback
,才有可能。
不過,我確實很喜歡這個主意,因此我將在有空的時候再嘗試解決。 如果您在此之前上過工作的工廠班級,或者如果有人有一個完全不同的更好的解決方案,那么我將非常有興趣看到它。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.