Skip to content

useReducer: stale value in closure or lost update? #2913

@mischnic

Description

@mischnic

Reproduction

https://codesandbox.io/s/nostalgic-wilson-3vs0s

import React, {
  createElement,
  createContext as createContextOrig,
  useContext as useContextOrig,
  useLayoutEffect,
  useReducer,
  useRef,
  useState,
} from "preact/compat";

function createContext() {
  const context = createContextOrig();

  const ProviderOrig = context.Provider;
  context.Provider = ({ value, children }) => {
    const valueRef = useRef(value);
    const contextValue = useRef();

    if (!contextValue.current) {
      contextValue.current = {
        value: valueRef,
        listener: null,
      };
    }

    useLayoutEffect(() => {
      valueRef.current = value;
      if (contextValue.current.listener) {
        contextValue.current.listener([value]);
      }
    }, [value]);
    return <ProviderOrig value={contextValue.current}>{children}</ProviderOrig>;
  };

  return context;
}

function useContextSelector(context) {
  const contextValue = useContextOrig(context);
  const {
    value: { current: value },
  } = contextValue;
  const [state, dispatch] = useReducer(
    () => {
      return {
        value,
      };
    },
    {
      value,
    }
  );
  useLayoutEffect(() => {
    contextValue.listener = dispatch;
  }, []);
  return state.value;
}

const context = createContext(null);

function Child() {
  const [count, setState] = useContextSelector(context);
  const [c, setC] = useState(0);
  return (
    <div>
      <div>Context count: {count}</div>
      <div>Local count: {c}</div>
      <button
        onClick={() => {
          setC((s) => s + 1);
          setState((s) => s + 1);
        }}
      >
        +1
      </button>
    </div>
  );
}

// Render this
export function App() {
  const [state, setState] = useState(0);
  return (
    <context.Provider value={[state, setState]}>
      <Child />
    </context.Provider>
  );
}

Steps to reproduce

Click on the "+1" button multiple times.

Expected Behavior

What React does: both counters increment simultaneously.

Actual Behavior

With Preact, the first update is ignored and the "local counter" is offset by one to the "global counter".


Came up in dai-shi/use-context-selector#36

cc @marvinhagemeister @dai-shi

Metadata

Metadata

Assignees

No one assigned

    Labels

    hooksneeds-more-infoThe issue doesn't contain enough information to be able to helpquestion

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions