再戰 React Concurrent Mode
Concurrent Mode 的第一站:Fiber
在 Fiber 出現之前:
- 
瀏覽器是單線程操作
 
- 一次性大量更新時
 main thread 就會卡住。
Fiber:
- 
把一次性更新拆分成 一個個小更新
 
- 每次更新之間的空檔去處理別的事情



偵錯:componentDidCatch 和 getDerivedStateFromError
getDerivedStateFromError
componentDidCatch
更進一步 的 Concurrent Mode


不同裝置的體驗
不同網速的體驗
useTransition
Suspense
Suspense for Data fetching
💡「延遲」 非同步操作,等非同步操作同步後,才開始 Render。
import {unstable_createResource as createResource} from 'react-cache';
const resource = createResource(fetchDataApi);
const Foo = () => {
  const result = resource.read();
  return (
    <div>{result}</div>
  );
// ...
<Suspense>
   <Foo />
</Suspense>};實作原理
const wrapPromise = promise => {
  let status = "pending";
  let result = "";
  let suspender = promise.then(
    r => {
      status = "success";
      result = r;
    },
    e => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      }
      return result;
    }
  };
};

Suspense
ErrorBoundary
- 捕捉 Promise 回傳的 Error
- 包在 Suspense 外層,Literally 像個邊界
- 以 Class Component 的方式實現
import React from "react";
export class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  componentDidCatch(error, errorInfo) {
    console.log(error, errorInfo);
  }
  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return (
        <div>
          Something went wrong: {this.state.error.message}
        </div>
      );
    }
    return this.props.children;
  }
}<ErrorBoundary>
  <Suspense fallback={null}>
    <ComponentThatThrowError />
  </Suspense>
</ErrorBoundary>SuspenseList
<SuspenseList tails="collapsed" revealOrder="together">
  <Suspense fallback={<h1>loading num...</h1>}>
    <ComponentOne />
  </Suspense>
  <Suspense fallback={<h1>loading person...</h1>}>
    <ComponentTwo />
  </Suspense>
</SuspenseList>- revealOrder 實現 載入順序
	- 如:forwards、backwards、together
 
- tail 管理 fallback 狀態
	- 強制關閉其下的 fallback ( loading state )
- 壓縮他們。
 
Normal
forwards
together
tail: collapsed
why useTransition?
Default : Receded → Skeleton → Complete
useTransition : Pending → Skeleton → Complete

Default
useTransition
再進一步延遲 : useTransition
集中 管理 和 延遲 頁面 更新
(其實在後台繼續 render 但延遲 commit 到真正畫面上的時間)
function ProfilePage() {
  const [
    startTransition,
    isPending
  ] = useTransition({
    // Wait 10 seconds before fallback
    timeoutMs: 10000
  });
  const [resource, setResource] = useState(
    initialResource
  );
  function handleRefreshClick() {
    startTransition(() => {
      setResource(fetchProfileData());
    });
  }
  return (
    <Suspense
      fallback={<h1>Loading profile...</h1>}
    >
      <ProfileDetails resource={resource} />
      <button
        onClick={handleRefreshClick}
        disabled={isPending}
      >
        {isPending ? "Refreshing..." : "Refresh"}
      </button>
      <Suspense
        fallback={<h1>Loading posts...</h1>}
      >
        <ProfileTimeline resource={resource} />
      </Suspense>
    </Suspense>
  );
}流程:
1. 將 Suspense 變更為 Pending 狀態
2. 為 Pending 狀態的更新都會被延遲
(此時都還在上一個畫面)
3. 拉完資料 or 超時( 進入 fallback )
4. Render 新畫面
實作原理
function updateTransition(
  config: SuspenseConfig | void | null,
): [(() => void) => void, boolean] {
  const [isPending, setPending] = updateState(false); // = useState
  const startTransition = updateCallback(             // = useCallback
    callback => {
      setPending(true); //  pending: true
      // 調降執行的優先級
      Scheduler.unstable_next(() => {
        // 設定 suspenseConfig
        const previousConfig = ReactCurrentBatchConfig.suspense;
        ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
        try {
          // 還原 pending: false
          setPending(false);
          // 執行 fetch OR sth else...
          callback();
        } finally {
          // 還原 suspenseConfig
          ReactCurrentBatchConfig.suspense = previousConfig;
        }
      });
    },
    [config, isPending],
  );
  return [startTransition, isPending];
}- unstable_next : 調降更新的優先次序
- suspenseConfig : 計算自己的優先次序
- scheduler : React 內部的更新順序
useDeferredValue
相較於 useTransition 是延遲更新
useDeferredValue 則是取上一個值
function ProfilePage({ resource }) {
  const deferredResource = useDeferredValue(
    resource,
    {
      timeoutMs: 1000
    }
  );
  return (
    <Suspense
      fallback={<h1>Loading profile...</h1>}
    >
      <ProfileDetails resource={resource} />
      <Suspense
        fallback={<h1>Loading posts...</h1>}
      >
        <ProfileTimeline
          resource={deferredResource}
          isStale={deferredResource !== resource}
        />
      </Suspense>
    </Suspense>
  );
}實作原理
function useDeferredValue<T>(
  value: T,
  config: TimeoutConfig | void | null,
): T {
  const [prevValue, setValue] = useState(value);
  const [startTransition] = useTransition(config)
  useEffect(
    () => {
      startTransition(() => {
	  setValue(value);
	})
  },
  [value, config],
 );
  return prevValue;
}
1.用 useEffect 監聽 value 的變化
2. 在 startTransition 中更新 value
小結
- 讓非同步操作看起來像同步
 
- 依此大幅減少 Loading (等資料) 的“感覺”
 
- 可以安排頁面載入順序,讓UI更好維護
謝謝大家!
參考資料
- https://www.lizenghai.com/archives/47192.html#SuspenseListuseDeferredValue
- https://medium.com/@chentsulin/%E7%90%86%E8%A7%A3-react-%E7%9A%84%E4%B8%8B%E4%B8%80%E6%AD%A5-concurrent-mode-%E8%88%87-suspense-327b8a3df0fe
- https://juejin.im/post/5c7f6106e51d45055e26df9a
- https://juejin.im/post/5db65d87518825648f2ef899
- https://zh-hant.reactjs.org/docs/error-boundaries.html
- https://juejin.im/post/5dbee8e7e51d4558040f0830
- https://www.zhihu.com/question/268028123/answer/332182059
- https://zhuanlan.zhihu.com/p/34210780
- https://www.youtube.com/watch?v=fTFoBr5LJGE&list=PLN3n1USn4xln7sHUudKJEmMe7gFKtuNww
React Concurrent Mode
By parkerhiphop
React Concurrent Mode
- 529
 
   
   
  