Skip to content

Instantly share code, notes, and snippets.

@promer94
Last active March 7, 2023 17:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save promer94/fb6cea9fac8ad7c2cd0d7fe291df22e1 to your computer and use it in GitHub Desktop.
Save promer94/fb6cea9fac8ad7c2cd0d7fe291df22e1 to your computer and use it in GitHub Desktop.

Problem

I simplified the reproduction

import useSWR from "swr";
import React, { useState, useEffect } from "react";

const sleep = (ms) =>
  new Promise((resolve) => {
    setTimeout(() => resolve("foo"), ms);
  });

export default function App() {
  const [state, setState] = useState(false);
  useEffect(() => {
    let timeout = setTimeout(() => {
      setState(true);
    }, 2000);
    return () => {
      clearTimeout(timeout);
    };
  }, []);

  const joke = useSWR("joke", () => sleep(1000));

  if (!state || !
  joke.data) {
    return <div>loading</div>;
  }
  return <div>{joke.data}</div>;
}
  1. In first render, joke.data will not be accessed.

  2. After 1 second, the cache will be updated to { data: "foo" }

  3. After 2 second, state will be true and joke.data will be accessed. However, changing stateDeps will not trigger another render, so joke.data will still be undefined.

  4. when a new validation is triggered, after sleep resolves, the joke.data will still be undefined because cache has been update.

const cached = useSyncExternalStore(
  useCallback(
    (callback: () => void) =>
      subscribeCache(
        key,
        (current: State<Data, any>, prev: State<Data, any>) => {
          // prev { data: 'foo' }  current {data: 'foo'} 
          if (!isEqual(prev, current)) callback();
        }
      ),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [cache, key]
  ),
  getSnapshot[0],
  getSnapshot[1]
);

callback will not be executed, then getSnapshot[0] will not have chance to update the joke.data

Thoughts

  • A quick but not complete fix for this could be removing the !isEqual(prev, current) in useSES sub function, then joke.data could be update in next validation. However it won't work for useSWRImmutable and it has to wait for another revalidation.
  • I think a complete fix require a mechanism to notify useSES immediately that stateDeps has changed, so useSES would calculate a new snapshot. But this kind of mechanism may hurt performace of general SWR usage.

Previously i said

IMO it'more like a limitation.

Because in my mental model of SWR's dependcies collection. The following code

const { data, error, isLoading } = useSWR('/api', fetcher)

are like

const swr = useSWR('/api', fetcher)
const data = useSubSWR(swr, 'data')
const error = useSubSWR(swr, 'error')
const isLoading = useSubSWR(swr, 'isLoading')

which means you should not do

const swr = useSWR('/api', fetcher)
let data
if(xxx) {
  data = useSubSWR(swr, 'data')
}

Trying to access swr.data conditionaly breaks the rule of hooks.

Currently, i am not sure what to do next, would love to hear your ideas about this problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment