Skip to content
Learn Netverks
2

Why does useEffect show an old external store value after React Activity is shown again?

asked 16 hours ago by @qa-cf4qb44rrvipusduedef 0 rep · 64 views

javascript reactjs react hooks react state management

I was testing Activity in react 19.2 and noticed that a component using useEffect keeps the old store value after the activity is hidden and then shown again.

The same example works correctly with useSyncExternalStore.

import {
 Activity,
 useEffect,
 useState,
 useSyncExternalStore
} from "react";

let value = 0;
const listeners = new Set();
const store = {
 getSnapshot() {
   return value;
  },

 subscribe(listener) {
   listeners.add(listener);

   return () => {
     listeners.delete(listener);
    };
  },

 increment() {
   value += 1;
   listeners.forEach((listener) => listener());
  }
};

function EffectCounter() {
 const [count, setCount] = useState(store.getSnapshot);

 useEffect(() => {
   return store.subscribe(() => {
     setCount(store.getSnapshot());
    });
  }, []);

 return <p>useEffect: {count}</p>;
}

function ExternalStoreCounter() {
 const count = useSyncExternalStore(
   store.subscribe,
   store.getSnapshot
  );

 return <p>useSyncExternalStore: {count}</p>;
}

export default function App() {
  const [visible, setVisible] = useState(true);

  return (
    <>
     <button onClick={() => setVisible(false)}>Hide</button>
     <button onClick={() => setVisible(true)}>Show</button>
     <button onClick={store.increment}>Increment</button>

      <Activity mode={visible ? "visible" : "hidden"}>
       <EffectCounter />
       <ExternalStoreCounter />
      </Activity>
    </>
  );
}

I hide the activity, increment the store, and then show it again.

The useEffect still has the previous value, but the useSyncExternalStore component has the current value.

I know Activity removes effects while it is hidden but is the useEffect version stale because subscribe only receives future changes and does not read the current value when it reconnects?

Do I call setCount with the current snapshot when the effect runs again? Or is useSyncExternalStore the correct way?

Comments on this question (0)

Use comments to ask for clarification — answers go in the answer box below.

Log in to comment on this question.

3

Do I call setCount with the current snapshot when the effect runs again?

Yes, it appears that you need to do a separate explicit synchronization with the current snapshot when the component mounts. What's different here is that when using Activity it appears to fully unmount and remount (verified with mounting useEffect hook call and cleanup function), but the remount appears to act more like a suspended render in that the state from the previous mounting is maintained and restored.

See Restoring the state of hidden components:

When you hide a component using an Activity boundary instead, React will “save” its state for later:

<Activity mode={isShowingSidebar ? "visible" : "hidden"}>
  <Sidebar />
</Activity>

This makes it possible to hide and then later restore components in the state they were previously in.

Adding an explicit state update with the current snapshot appears to resolve the discrepancy of the external state while the component was hidden.

useEffect(() => {
  // Sync current snapshot on mounting
  setCount(store.getSnapshot());

  // Subscribe to future updates and cleanup
  return store.subscribe(() => {
    setCount(store.getSnapshot());
  });
}, []);

Or is useSyncExternalStore the correct way?

This is subjective and likely depends on the specific use case and what the side-effect is. In this particular use case I'd agree that using useSyncExternalStore appears to be the correct usage. It's less moving parts and does exactly what you expect it to do.

Sam Diaz · 0 rep · 16 hours ago

Your answer