Calendar reopens after clicking outside: resetInputValue writes a stale state snapshot on blur

Author: KirylkaCreated Sep 14, 2026Updated Sep 14, 2026

Describe the bug

resetInputValue writes a full snapshot of this.state back through setState:

https://github.com/Hacker0x01/react-datepicker/blob/main/src/index.tsx#L702-L707

javascript
resetInputValue = () => {
  this.setState({
    ...this.state,
    inputValue: null,
  });
};

handleBlur calls it. When the user clicks away, two things happen in one task:

  1. The document mousedown listener in ClickOutsideWrapper runs handleCalendarClickOutside, which calls setOpen(false).
  2. Focus leaves the input, so handleBlur runs.

That listener is a plain addEventListener, so React has not committed the open: false update yet when step 2 runs. this.state still holds open: true, and the snapshot spread writes it back. The calendar reopens immediately after closing and stays on screen, covering whatever is below it.

Only inputValue needs to be set here. Spreading this.state is what causes the problem:

javascript
resetInputValue = () => {
  this.setState({ inputValue: null });
};

To Reproduce

Failing test, jsdom + React 19 + @testing-library/react 16. No other dependencies:

typescript
import { useState } from "react";
import { render, act } from "@testing-library/react";
import DatePicker from "react-datepicker";

const Fixture = () => {
  const [date, setDate] = useState<Date | null>(null);
  return (
    <div>
      <DatePicker id="start" selected={date} onChange={setDate} />
      <button id="outside">outside</button>
    </div>
  );
};

const openCalendars = () => document.querySelectorAll(".react-datepicker");

it("closes when an outside mousedown and the input blur land in the same task", () => {
  render(<Fixture />);
  const input = document.querySelector("#start") as HTMLInputElement;
  const outside = document.querySelector("#outside") as HTMLButtonElement;

  act(() => {
    input.focus();
    input.dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
  });
  expect(openCalendars()).toHaveLength(1);

  // A real browser dispatches the outside mousedown and the resulting input
  // blur in one task, so React has not committed setOpen(false) yet when the
  // blur handler runs. Both inside one act() reproduces that ordering.
  act(() => {
    outside.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
    input.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
  });

  expect(openCalendars()).toHaveLength(0);
});

On 9.1.0 this fails with Expected length: 0, Received length: 1. With the one-line change above it passes.

Note that userEvent.click does not reproduce it. userEvent awaits between pointer events, which lets React commit open: false before the blur handler reads this.state. The two events have to land in the same task, as they do in a browser.

Expected behavior

Clicking outside the calendar closes it and it stays closed.

Desktop

  • OS: macOS 15
  • Browser: Chrome / Electron 118
  • Version: react-datepicker 9.1.0, React 19.2.7

Additional context

Introduced in 8.5.0 by 58f4afc5 ("Reset inputValue back to null on blur of input (even when the calendar popup is not open)"). resetInputValue does not exist in 8.4.0 or earlier — I checked the published tarballs from 7.6.0 through 9.1.0. The line is unchanged on main today.

It likely became easier to hit in 9.x: PR #6180 added the masked-input clearing work to handleBlur immediately before the resetInputValue() call.

This is not #6292. There the outside element calls stopPropagation() on mousedown so the document listener never fires. Here the listener fires and closes the calendar correctly, and the blur handler then reopens it.

We hit this in a Chakra UI app after upgrading 7.6.0 to 9.1.0: five browser tests started failing because a date picker stayed open over a <select> beneath it. Reverting that one line fixes all of them with no change to our own code.

Source: Hacker0x01/react-datepicker