7

I am trying to run a redux action that fetches a db and store it in a state variable. I am then using this variable as a default for a Material UI Slider. I am initializing this default value (to avoid being null which usually solves the problem) and then using useState to set the value when the fetch is complete. I cant get rid of the warning A component is changing the default value state of an uncontrolled Slider after being initialized. although my slider value is never set to Null Below is my page code and reducer Page:

import React, { useState, useEffect } from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Typography from '@material-ui/core/Typography';
import Slider from '@material-ui/core/Slider';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { getSettings } from '../../actions/settingsActions';

const Settings = ({ getSettings, settings }) => {
  React.useEffect(() => {
    getSettings();

    // eslint-disable-next-line
  }, []);

  const [current, setCurrent] = useState({
    ageAtDeath: 50,
  });

  React.useEffect(() => {
    if (settings.ageAtDeath) {
      //only loads when settings is not null as set in initial state getSettings() completed!
      setCurrent({
        ageAtDeath: settings.ageAtDeath,
      });
    }
  }, [settings]);

  return (
    <Card>
      <CardContent>
        <Typography color='textSecondary' gutterBottom>
          Age at death:
        </Typography>
        <Slider
          defaultValue={current.ageAtDeath}
          aria-labelledby='discrete-slider'
          valueLabelDisplay='auto'
          step={10}
          marks
          min={10}
          max={110}
        />
      </CardContent>
    </Card>
  );
};

Settings.propTypes = {
  getSettings: PropTypes.func.isRequired,
  settings: PropTypes.object.isRequired,
};

const mapStateToProps = (state) => ({
  settings: state.settings.settings,
});

export default connect(mapStateToProps, {
  getSettings,
})(Settings);

Reducer Code:

import {
  GET_SETTINGS,
 
} from '../actions/Types';

const initialState = {
  settings: {ageAtDeath: 0},

};

export default (state = initialState, action) => {
  switch (action.type) {
    case GET_SETTINGS:
      return {
        ...state,
        settings: action.payload,
        
      };
    default:
      return state;
  }
};
Mohamed Daher
  • 609
  • 1
  • 10
  • 23
  • I think the issue is for uncontrolled inputs the default value shouldn't change once mounted. I don't see an attached change handler, no does it look like you do anything with the value later if it's changed. Is this correct? – Drew Reese Jul 03 '20 at 08:16
  • of course I am trying to pull the current values and reset them and save the new settings. I didnt include this to make the code simpler as i dont think they are related to the solution. Reading your comment i replaced `defaultValue={current.ageAtDeath}` with `value={ageAtDeath}` Now the warning is gone which seems to solve the issue. The problem is that the slider now is stuck at this value and cant be changed even after i added `onChange={onChange}` functionality – Mohamed Daher Jul 03 '20 at 08:20
  • 1
    Well, usually with uncontrolled you attach a ref so you can read the value at some point in the future to include with any actions. As-is, it doesn't appear to be relevant to update the slider value. Perhaps use conditional rendering to wait for your redux state to update *then* render the slider with the real default value you want. – Drew Reese Jul 03 '20 at 08:29
  • the reference here is the current. it is a useState that setCurrent when the value of changes to be sent later when the update button is clicked lets say. Any idea why the slider is not moving? if i replace the slide with a text field it works just fine. Something special about sliders i am missing? – Mohamed Daher Jul 03 '20 at 08:32
  • Sorry, I was referring to a react ref. Nope, sliders are unique but not particularly special or different than other inputs. When using `defaultValue` and it changes then react warns that you may be intending to use the input as a controlled input. Using `value` indicates *it is* a controlled input, but if you don't attach an `onChange` handler the value won't change. This is why I asked what/how you get any updated value from the "uncontrolled" input, it isn't clear in your snippets if you even try. – Drew Reese Jul 03 '20 at 08:46

4 Answers4

25

⚠️ You should use value attribute instead of defaultValue⚠️

  <Slider
      value={current.ageAtDeath} /*defaultValue={current.ageAtDeath}*/
      aria-labelledby='discrete-slider'
      valueLabelDisplay='auto'
      step={10}
      marks
      min={10}
      max={110}
    />
Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
23

I solved this issue by adding key to component

<Slider
    key={`slider-${currentConcurrency}`} /* fixed issue */
    defaultValue={currentConcurrency}
    step={1}
    max={maxConcurrency}
    min={1}
    marks
    valueLabelDisplay={'auto'}
    onChangeCommitted={this.handleSliderChange('currentConcurrency')} /* increase render performance */
/>
Cong Dan Luong
  • 1,577
  • 17
  • 15
  • 2
    Where u find out the solution, was it in matarial-ui documentation, just want understand why it works like that? – squashpat Jan 19 '21 at 07:06
  • 1
    @squashpat when the `key` value changes, react creates a new `Slider` and initializes it with the new `defaultValue` as an uncontrolled component. `key` is component id for react. If we don't use `key`, when `defaultValue` changes, react complains that it has initialized the `Slider` component as uncontrolled (not controlled that code can change its value) & can malfunction. You can use `value` instead of `defaultValue` to make `Slider`s controlled, however, atm controlled Sliders won't let users drag the pin around - only by clicking on the final destination which might not be desired. – Mahsa2 Sep 03 '21 at 13:22
  • 1
    Thanks for this workaround! Also kudos to @Mahsa2 for the detailed explanation behind the fix! – Lee Merlas Sep 22 '21 at 03:52
  • Thanks man, this was a life saver for me :) – oak Aug 04 '23 at 07:34
2

I solved this by following the answer from this post.

Here is the final code.

const onChange = (name) => (e, value) => {
    setCurrent({
      ...current,
      [name]: value,
    });
  };

make sure to add the name of the value you are changing when calling onChange

               <Slider
                id='costInflation'
                name='costInflation'
                value={costInflation}
                onChange={onChange('costInflation')}
                min={0}
                max={50}
                valueLabelDisplay='on'
              />
Mohamed Daher
  • 609
  • 1
  • 10
  • 23
0

Use value, where the value is set by the state that changes with the onChange handler.

  const [endDate, setEndDate] = useState<Date>(defaultDate);

  function handleChange(
    event: React.ChangeEvent<Record<string, unknown>>,
    sliderValue: number | number[]
  ) {
    if (typeof sliderValue === "number") {
      const newEndDate = getCloseDate({
        date: addDays(startDate, sliderValue),
      });
      setEndDate(newPayoutDate); // set the state
    }
  }

  const duration = differenceInDays(endDate, startDate);

  return (
    <Slider
      className={classes.sliderRoot}
      value={duration}
      getAriaValueText={valuetext}
      aria-labelledby="discrete-slider-small-steps"
      onChange={handleChange}
      step={1}
      marks={marks}
      min={30}
      max={180}
      valueLabelDisplay="auto"
    />
  );
};
OctaviaLo
  • 1,268
  • 2
  • 21
  • 46