kiw
kiw

Reputation: 808

Why does React.StrictMode break calculating a diff of props/state?

function SchrödingersDiff()
{
  const [count, setCount] = useState(0);
  // simulate a changing state
  useEffect(() =>
  {
    const int = window.setInterval(() => setCount(s => s + 1), 1000);
    return () => window.clearInterval(int);
  }, []);

  const last = useRef(0);
  const diff = useMemo(() =>
  {
    const lastValue = last.current;
    last.current = count;
    const diff = count - lastValue;
    // console.log(diff);
    return diff;
  }, [count]);

  // console.log(diff);
  return diff;
}

Why does this component always render "0" when running under <React.StrictMode>? It works fine (i.e. renders "1") without strict mode.

I know strict mode renders things twice, which would explain it, but I would expect useMemo to catch that - and it does: the log in the memo function prints the correct "1" every second. Even the one outside does.

Is there a better way to calculate that diff?


This is my actual useState/useEffect/setInterval, already adapted to keep the last:

export function useChannel(channelName, initialData, currentOnly)
{
  const socket = useSc(s => s.socket); // gets a SocketCluster socket
  const [data, setData] = useState(currentOnly ? initialData : {current: initialData});

  useEffect(() =>
  {
    if (!socket) return;

    const channel = socket.subscribe(channelName);
    (async () =>
    {
      for await (const data of channel) setData(currentOnly ? data : s => ({current: data, last: s.current}));
    })();

    return () => channel.unsubscribe();
  }, [socket, channelName]);

  return data;
}

Upvotes: 2

Views: 563

Answers (2)

Bergi
Bergi

Reputation: 664297

Is there a better way to calculate that diff?

I would extract the useState hook, and put the calculation logic into the setState function. You kinda did that already, but we can make this a bit more generic and reusable:

export function useCurrentChannel(channelName, initialData) {
  const [data, setData] = useState(initialData)
  useChannelSubscription(channelName, setData);
  return data;
}
export function useChannelSubscription(channelName, onNewData) {
  const socket = useSc(s => s.socket);
  useEffect(() => {
    if (!socket) return;

    const channel = socket.subscribe(channelName);
    (async () => {
      for await (const data of channel) {
        onNewData(data);
      }
    })();

    return () => channel.unsubscribe();
  }, [socket, channelName, onNewData]); // or put `onNewData` in a Ref
  return socket;
}

Now you can also write

export function useHistoricChannel(channelName, initialData) {
  const [data, setData] = useState({current: initialData})
  const handleNewData = useCallback(
    (data) => setData(s => ({current: data, last: s.current})),
    [setData]
  );
  useChannelSubscription(channelName, handleNewData);
  return data;
});

which might be nicer with a reducer:

export function useHistoricChannel(channelName, initialData) {
  const [data, handleNewData] = useReducer(
    (s, data) => ({current: data, last: s.current}),
    {current: initialData}
  );
  useChannelSubscription(channelName, handleNewData);
  return data;
});

Of course you can make the reducer compute anything (like a diff between current and last), without having to change useChannelSubscription.

Upvotes: 1

coglialoro
coglialoro

Reputation: 773

You can define a useDiff custom hook that internally keeps a state with the current and the last values so you can update both from your setInterval callback like so:

const useDiff = () => {
  const [state, setState] = useState({
    current: 0,
    last: 0,
  });

  useEffect(() => {
    const interval = window.setInterval(() => {
      setState((s) => ({ current: s.current + 1, last: s.current }));
    }, 1000);
    return () => window.clearInterval(interval);
  }, []);

  return state.current - state.last;
};

Demo: https://stackblitz.com/edit/react-akituv

Upvotes: 1

Related Questions