簡體   English   中英

樂觀渲染和使用效果

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM