Skip to content

[pickers] onAccept not called when clearing a value that was set externally via controlled prop #21962

@Daymannovaes

Description

@Daymannovaes

Steps to reproduce

  1. Mount a controlled DatePicker with value={null} and an action bar that includes the clear action
  2. Update the controlled value prop externally (e.g. via Redux or parent state change — not through the picker UI)
  3. Open the picker and click the Clear action bar button

Expected: onAccept(null) fires, allowing the consumer to react to the clear.
Actual: onAccept is not called. Only onChange(null) fires (which many consumers treat as an intermediate/uncommitted change).

Why it happens

In useValueAndOpenStates.ts, when the controlled value prop changes externally, lastCommittedValue is not updated:

// useValueAndOpenStates.ts lines ~161-168
if (value !== state.lastExternalValue) {
  setState(prevState => ({
    ...prevState,
    lastExternalValue: value,
    clockShallowValue: undefined,
    hasBeenModifiedSinceMount: true,
    // ⚠️ lastCommittedValue is NOT updated here
  }));
}

Then when Clear is clicked, clearValue()setValue(emptyValue, { source: 'view' }) → the accept guard compares:

shouldFireOnAccept = changeImportance === 'accept'
  && !valueManager.areValuesEqual(adapter, newValue, state.lastCommittedValue);
//                                         null          null (stale from mount!)
// → areValuesEqual(null, null) → true → shouldFireOnAccept = false → onAccept skipped

The same issue affects any action that compares against lastCommittedValue — Clear, Accept (if re-accepting the same externally-set value), and it also means Cancel would restore to the wrong value.

Contrast with working case

If the user sets the date through the picker UI (selecting from the calendar), onAccept fires and updates lastCommittedValue. Then clicking Clear correctly sees null !== dateonAccept(null) fires.

Minimal reproduction

import React, { useState, useEffect } from 'react';
import { DatePicker } from '@mui/x-date-pickers';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { parseISO } from 'date-fns';

export default function App() {
  return (
    <LocalizationProvider dateAdapter={AdapterDateFns}>
      <BugRepro />
    </LocalizationProvider>
  );
}

function BugRepro() {
  const [externalValue, setExternalValue] = useState(null);

  return (
    <div style={{ padding: 24, fontFamily: 'monospace' }}>
      <h3>onAccept not called after external value change</h3>

      <button onClick={() => setExternalValue('2025-06-15')}>
        1. Set value externally (simulates Redux update)
      </button>
      <button onClick={() => setExternalValue(null)}>
        Reset
      </button>
      <p>External value: <strong>{String(externalValue)}</strong></p>
      <p>2. Now open the picker and click <strong>Clear</strong>. Check the console.</p>

      <ControlledPicker
        rawValue={externalValue}
        onDateChange={(val) => {
          console.log('onDateChange (from onAccept):', val);
          setExternalValue(val);
        }}
      />
    </div>
  );
}

function ControlledPicker({ rawValue, onDateChange }) {
  const [value, setValue] = useState(rawValue ? parseISO(rawValue) : null);

  useEffect(() => {
    setValue(rawValue ? parseISO(rawValue) : null);
  }, [rawValue]);

  return (
    <DatePicker
      label="Test"
      value={value}
      onChange={(date) => {
        console.log('onChange:', date);
        setValue(date);
      }}
      onAccept={(date) => {
        console.log('✅ onAccept:', date);
        onDateChange(date ? date.toISOString().slice(0, 10) : null);
      }}
      slotProps={{
        actionBar: { actions: ['clear', 'cancel', 'accept'] },
        textField: { size: 'small' },
      }}
      format="yyyy-MM-dd"
    />
  );
}

Steps with the repro:

  1. Click "Set value externally" — the picker displays 2025-06-15
  2. Open the picker → click Clear
  3. Observe the console: onChange: null fires, but onAccept never fires

Now compare:

  1. Click Reset to go back to null
  2. Open the picker → select any date from the calendar → click Accept (this fires onAccept and updates lastCommittedValue)
  3. Open the picker again → click Clear
  4. This time onAccept(null) does fire

Proposed fix

Update the external value sync block in useValueAndOpenStates.ts to also update lastCommittedValue:

if (value !== state.lastExternalValue) {
  setState(prevState => ({
    ...prevState,
    lastExternalValue: value,
    lastCommittedValue: value, // ← sync committed value with external changes
    clockShallowValue: undefined,
    hasBeenModifiedSinceMount: true,
  }));
}

System info

Tech Version
@mui/x-date-pickers 8.27.0
@mui/material 7.3.8
React 18.3.1
Browser Chrome 135

Search keywords: onAccept not called, clear button, controlled DatePicker, external value change, lastCommittedValue

Metadata

Metadata

Assignees

No one assigned

    Labels

    scope: pickersChanges related to the date/time pickers.type: bugIt doesn't behave as expected.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions