iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Software Development

Effect 魔法:打造堅不可摧的應用程式系列 第 23

22. Effect 應用 1 : 如何在 React 中呼叫 Effect 的程式

  • 分享至 

  • xImage
  •  

這篇要來介紹怎麼在實際的環境中使用 Effect ,這次介紹的是在 React 中會怎麼使用,除了簡單的情境我們可以直接用 Efect.runPromise 外,這篇還會介紹到一些其它的場景中要怎麼使用 Effect 寫的程式

最簡單的情況

你的 React 程式裡可能會有很多的狀態,你也有可能在使用一些全域的狀態管理的套件,所以,在 React 中怎麼呼叫 Effect 呢?就像上面所說的,簡單的情況下我們就用 Effect.runPromise 就行了

import { useQuery } from "@tanstack/react-query";
import { $fetch, type FetchError } from "ofetch";

function fetchPost(id: number) {
  return Effect.tryPromise({
    try: () => $fetch(`https://jsonplaceholder.typicode.com/posts/${id}`),
    catch: (error) => error as FetchError,
  });
}

export function MyComponent() {
  const { data } = useQuery({
    queryKey: ["post", 1],
    queryFn: () => Effect.runPromise(fetchPost(1)),
  });

  return <div>{JSON.stringify(data)}</div>;
}

就像這樣就可以在 React 裡使用了,當你的 Effect 不依賴任何 React 特有的 hooks 或上下文時,這是最直接的方式,接下來我們就來看一些比較複雜的情況

存取全域的 store

在 React 中我們常會使用全域的狀態管理套件,最有名的像是 Redux ,或是一些其它的解決方案,例如 jotai 或是 zustand ,通常而言這些狀態管理的套件實際上都可以單獨使用與在 React 外部使用,很少有跟 React 完全綁死的,這邊使用 jotai 示範

假設我們有這樣的 store 與 atom

import { atom, createStore } from "jotai";

export const globalDataAtom = atom<number[]>([]);

export const store = createStore();

我們事實上是可以直接在 jotai 外存取 jotai 中的狀態的

// 取得 atom 的內容
store.get(globalDataAtom)
// 設定 atom 的內容
store.set(globalDataAtom, [])

不過通常而言我會偏好包成一個 service 來使用,這樣測試時就可以提供測試用的 store 了

// 提供 jotai 的 store 的 service
export class StoreService extends Effect.Service<StoreService>()("Store", {
  accessors: true,
  succeed: { store },
}) {}

// 存取 store 裡的資料的 service
export class StoreRepositoryService extends Effect.Service<StoreRepositoryService>()(
  "StoreRepository",
  {
    accessors: true,
    effect: Effect.gen(function* () {
      const { store } = yield* StoreService;

      return {
        globalData: Effect.sync(() => store.get(globalDataAtom)),
        setGlobalData: (data: number[]) =>
          Effect.sync(() => store.set(globalDataAtom, data)),        
      };        
    }),
    dependencies: [StoreService.Default],
  }
) {}

存取 React 中的 state

如果今天你要在 Effect 中使用到 React 中的 state ,那我們有個簡單的方法是直接當成參數傳入,或是直接在 Effect 中使用到 React 中的 state ,例如我們提到過,在之前的「15. Effect 實戰分享 3: 資料遷移」中的案例我們使用過 Effect.onExit 來收集成功與失敗的任務數量

const tasks = [
  /* 我們的任務,可能有的成功有的失敗 */
];

export function CollectTaskResult() {
  const [status, setStatus] = useState({
    success: 0,
    error: 0,
  });
  useEffect(() => {
    pipe(
      tasks,
      Effect.allWith({ concurrency: "unbounded", mode: "either" }),
      Effect.tap((results) => {
        const newStatus = {
          success: 0,
          error: 0,
        };
        // 收集結果
        for (const result of results) {
          if (Either.isRight(result)) {
            newStatus.success += 1;
          } else {
            newStatus.error += 1;
          }
        }
        setStatus(newStatus);
      }),
      Effect.runPromise
    );
  }, []);

  return <div>{JSON.stringify(status)}</div>;
}

或是我們也可以把 React 中的 state 包成 service ,並在執行時 provide 給 Effect

function Component() {
  const [state, setState] = useState();
  useEffect(() => {
    pipe(
      effect,
      // 用 service 提供進去
      Effect.provide(SomeLayer.Default({ state })),
      Effect.runPromise
    );
  }, []);

  return null;
}

Effect 的大小

既然是前端就需要來看一下打包後的大小的問題, Effect 其實佔了一定的體積,如果我們把上面的範例程式打包一下,然後看一下裡面的各個套件佔的大小的話,你會看到 Effect 大概佔了 42.5kb (gzipped) 跟 react-dom 其實差不多,不過你也可以從這張 treemap 中看到,這邊的 module 的數量比 Effect 真正有的 module 數量要少的很多, Effect 支援 tree shaking 來減少實際包進去的 module 的效果很好

https://ithelp.ithome.com.tw/upload/images/20251007/20111802p5qDatGZKg.png

上面這張圖是用 sonda 產生的,這套是我認為非常好用的一個 bundle 大小的分析工具

在前端就常需要做這種決擇,是否要引入一個套件,增加的大小是否會讓網頁的載入速度變的更慢等等,不過我自己的想法是:如果你的流程複雜到需要 Effect 來幫助你管理這些複雜的操作,那你很可能不是在寫那種主要以顯示資料為主的頁面,而是寫一個複雜的應用程式,這種時候,其實你的使用者大概更加在意你的功能的正確性與穩定性,而比較不會在意載入速度,所以視情況大膽的用吧

這邊我們講到了 Effect 在前端使用的情況,下一篇我們要來聊聊在後端可以怎麼應用


上一篇
21. Effect runtime :自訂如何執行 effect
下一篇
23. Effect 應用 2 :用 orpc 與 Effect 打造強韌的 API 介面
系列文
Effect 魔法:打造堅不可摧的應用程式25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言