Reading Apollo Client Source Code: useQuery

主要想要知道:

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

3.4.10 (支援 React 18 concurrent 前)

useQuery

Pseudo Code:

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 } = resultqueryData.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:
      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:
      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

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

幾個要點:

  • apollo-client-react 自己 implement useSyncExternalStore。實作大致跟 React 官方的 use-sync-external-store shim 相同。差在檢查 snapshot 變化是用 ===,React 是用 Object.is