---
title: "Reading Apollo Client Source Code: useQuery"
description: "從 Apollo Client 原始碼拆解 React useQuery 何時發出請求、如何訂閱查詢結果，以及資料更新為何觸發重新算繪。"
lang: "zh-Hant-TW"
tags: ["apollo-client", "GraphQL", "React", "JavaScript"]
pubDate: 2022-09-11T00:00:00.000Z
---

主要想要知道：

1. 什麼時候會發出 request？re-render 就會發嗎？
2. data 什麼時候會更新？

## 3.4.10 (支援 React 18 concurrent 前)

### `useQuery`

Pseudo Code:

```javascript
function useQuery(query, options) {
  const context = useContext(getApolloContext());
  const [tick, setTick] = useState(0);
  const forceUpdate = () => {
    setTick((t) => t + 1);
  };
  const updatedOptions = options ? { ...options, query } : { query };

  const queryDataRef = useRef();
  const queryData =
    queryDataRef.current ||
    (queryDataRef.current = new QueryData({
      options: updatedOptions,
      context,
      onNewData() {
        // sometimes delayed into micro task queue actually
        forceUpdate();
      },
    }));

  queryData.setOptions(updatedOptions);
  queryData.context = context;

  const resultRef = useRef();
  const resultDependenciesRef = useRef();
  const prevRestulDependencies = resultDependenciesRef.current;
  const currentResultDependencies = {
    options: _.omit(updatedOptions, ["onError", "onComplete"]),
    context,
    tick,
  };
  // `equal` from `@wry/equality`:
  // https://github.com/benjamn/wryware/blob/main/packages/equality/src/tests.ts
  if (!equal(prevResultDependencies, currentResultDependencies)) {
    resultRef.current = lazy ? queryData.executeLazy() : queryData.execute();
  }

  // Clear queryData when unmounting
  useEffect(() => {
    return () => {
      queryData.cleanup();
      queryDataRef.current = void 0;
    };
  }, []);

  // afterExecute
  useEffect(
    () => queryData.afterExecute({ lazy }),
    [
      queryResult.loading,
      queryResult.networkStatus,
      queryResult.error,
      queryResult.data,
      queryData.currentObservable,
    ],
  );

  return result;
}
```

幾個要點：

- 只有第一次 render 會做的事：
  - render
    - `new QueryData()`
- 每次 render 會做的事：
  - render
    - 當 `options`, `context`, `tick` 和前次 render 不同時 -> `result = lazy ? queryData.executeLazy() : queryData.execute()`
  - effects
    - 當 `{ loading, networkStatus, error, data } = result` 或 `queryData.currentObservable` 和前次 render 不同時 -> 執行 `queryData.afterExecute({ lazy })`
- unmount 前會做的事：

  - `queryData.cleanup();` 和 `queryDataRef.current = void 0;`

- `QueryData` instance 的 `onNewData` 觸發時會做的事：
  - `setState(x => x+1)` 觸發 re-render，下次 render 會執行`queryData.execute()`

### QueryData

#### `constructor`

- `super(options, context);`
  - `this.options = options || {};`
  - `this.context = context || {};`
- `this.onNewData = onNewData;`

#### `queryData.execute()`

- `this.refreshClient();`
  - `this.client` 指到現在 options 或 context 裡的 `client`
- `this.updateObservableQuery();`
  - initiate observableQuery if needed:
    ```typescript
    this.currentObservable = this.refreshClient().client.watchQuery({
      ...observableQueryOptions,
    });
    ```
    - `queryManager.watchQuery()`
      - `new QueryInfo(this)`
      - `new ObservableQuery({ queryManager: this, queryInfo, options })`
      - `queryInfo.init({ document: options.query, observableQuery: observable, variables: options.variables })`
  - update observableQuery options:
    ```typescript
    if (!equal(newObservableQueryOptions, this.previous.observableQueryOptions)) {
      this.currentObservable.setOptions(newObservableQueryOptions);
    }
    ```
    - 會觸發 reobserve 可能 refetch
- `reutrn this.getExecuteResult()`
  - 就只是拿 `this.currentObservable.getCurrentResult()`

#### `queryData.afterExecute()`

- `this.startQuerySubscription();`
  - > Setup a subscription to watch for Apollo Client `ObservableQuery` changes. When new data is received, and it doesn't match the data that was used during the last `QueryData.execute` call (and ultimately the last query component render), trigger the `onNewData` callback. If not specified, `onNewData` will fallback to the default `QueryData.onNewData` function (which usually leads to a query component re-render).
  - `this.currentObservable!.subscribe({ next, error })`
    - 會觸發 `currentObservable` 的 subscriber 如果是第一個 observer 會進而觸發 reobserve 打 HTTP request
- `this.handleErrorOrCompleted();`

## 3.6.8 (支援 React 18 concurrent 後)

Psudo code: 3.6.8

```javascript
function useQuery(options) {
  const [_tick, setTick] = useState(0);
  const forceUpdate = () => {
    setTick((tick) => tick + 1);
  };

  const resultRef = useRef();
  actutally;

  const obsQuery = client.watchQuery(options);

  const subscribe = useCallback(
    () => {
      const onNext = () => {
        const nextResult = obsQuery.getCurrentResult();
        if (!isSame(result, nextResult)) {
          resultRef.current = nextResult;
          forceUpdate();
        }
      };

      const onError = (error) => {
        if (!isSame(result.error, error)) {
          resultRef.current = { ...resultRef.current, error, loading: false };
          forceUpdate();
        }
      };

      const subscription = obsQuery.subscribe(onNext, onError);
      return () => {
        subscription.unsubscribe();
      };
    },
    [/* dependencies */],
  );

  const result = useSyncExternalStore(
    subscribe,
    () => resultRef.current,
    () => resultRef.current,
  );

  return result;
}
```

Source: [useQuery.ts](https://github.dev/apollographql/apollo-client/blob/v3.6.8/src/react/hooks/useQuery.ts)

幾個要點：

- `apollo-client-react` 自己 implement `useSyncExternalStore`。實作大致跟 React 官方的 [use-sync-external-store shim](https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js) 相同。差在檢查 snapshot 變化是用 `===`，React 是用 `Object.is`
