javascriptreactjsmaterial-uislider

Material-UI slider with multiple useState is duplicated on click


I'm trying to create a font size and font weight previewer using Material-Ui sliders and useState, but when I click the first slider after clicking the second, the first slider is duplicated.

screenshot

Here is my code:

import React from "react";
import { withStyles, makeStyles } from "@material-ui/core/styles";
import Slider from "@material-ui/core/Slider";
import Typography from "@material-ui/core/Typography";

import Container from "@material-ui/core/Container";
import TextField from '@mui/material/TextField';

const useStyles = makeStyles((theme) => ({
  root: {
    width: 300 + theme.spacing(3) * 2
  },
  margin: {
    height: theme.spacing(3)
  }
}));

const iOSBoxShadow =
  "0 3px 1px rgba(0,0,0,0.1),0 4px 8px rgba(0,0,0,0.13),0 0 0 1px rgba(0,0,0,0.02)";

const marks = [
  {
    value: 0
  },
  {
    value: 25
  },
  {
    value: 50
  },
  {
    value: 75
  },
  {
    value: 100
  }
];

const IOSSlider = withStyles({
  root: {
    color: "#3880ff",
    height: 2,
    padding: "15px 0"
  },
  thumb: {
    height: 28,
    width: 28,
    backgroundColor: "#fff",
    boxShadow: iOSBoxShadow,
    marginTop: -14,
    marginLeft: -14,
    "&:focus, &:hover, &$active": {
      boxShadow:
        "0 3px 1px rgba(0,0,0,0.1),0 4px 8px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.02)",
      // Reset on touch devices, it doesn't add specificity
      "@media (hover: none)": {
        boxShadow: iOSBoxShadow
      }
    }
  },
  active: {},
  valueLabel: {
    left: "calc(-50% + 12px)",
    top: -22,
    "& *": {
      background: "transparent",
      color: "#000"
    }
  },
  track: {
    height: 2
  },
  rail: {
    height: 2,
    opacity: 0.5,
    backgroundColor: "#bfbfbf"
  },
  mark: {
    backgroundColor: "#bfbfbf",
    height: 8,
    width: 1,
    marginTop: -3
  },
  markActive: {
    opacity: 1,
    backgroundColor: "currentColor"
  }
})(Slider);

export default function CustomizedSlider() {
  const classes = useStyles();

  const [userFontSize, setUserFontSize] = React.useState("1rem");
  const [userFontWeight, setUserFontWeight] = React.useState("300");
  const [sliderSizeTracker, setSliderSizeTracker] = React.useState(0);
  const [sliderWeightTracker, setSliderWeightTracker] = React.useState(999);


  const handleSizeChange = (e, newSizeValue) => {
    switch (newSizeValue) {
      case 0: {
        setUserFontSize("1rem");
        setSliderSizeTracker(0);
        break;
      }
      case 25: {
        setUserFontSize("1.19rem");
        setSliderSizeTracker(25);
        break;
      }
      case 50: {
        setUserFontSize("1.37rem");
        setSliderSizeTracker(50);
        break;
      }
      case 75: {
        setUserFontSize("1.55rem");
        setSliderSizeTracker(75);
        break;
      }
      case 100: {
        setUserFontSize("1.75rem");
        setSliderSizeTracker(100);
        break;
      }
      default: {
        break;
      }
    }
  };

  const handleWeightChange = (e, newWeightValue) => {
    switch (newWeightValue) {
      case 0: {
        setUserFontWeight("300");
        setSliderWeightTracker(0);
        break;
      }
      case 25: {
        setUserFontWeight("400");
        setSliderWeightTracker(25);
        break;
      }
      case 50: {
        setUserFontWeight("500");
        setSliderWeightTracker(50);
        break;
      }
      case 75: {
        setUserFontWeight("700");
        setSliderWeightTracker(75);
        break;
      }
      case 100: {
        setUserFontWeight("900");
        setSliderWeightTracker(100);
        break;
      }
      default: {
        break;
      }
    }
  };

  return (
    <Container maxWidth="sm" style={{ marginTop: "2em" }}>
      <div className={classes.root}>
        <Typography gutterBottom>Font Size Slider</Typography>
        <IOSSlider
          aria-label="Font Size Slider"
          key={sliderSizeTracker}
          defaultValue={sliderSizeTracker}
          marks={marks}
          step={25}
          onChangeCommitted={handleSizeChange}
          valueLabelDisplay="off"
        />
        <Typography gutterBottom>Font Weight Slider</Typography>
        <IOSSlider
          aria-label="Font Weight Slider"
          key={sliderWeightTracker}
          defaultValue={sliderWeightTracker}
          marks={marks}
          step={25}
          onChangeCommitted={handleWeightChange}
          valueLabelDisplay="off"
        />
        <div className={classes.margin} />
        <TextField
          sx={{ border: 'none', "& fieldset": { border: 'none' }, }}
          margin="normal"
          required
          fullWidth
          inputProps={{ style: { fontSize: `${userFontSize}`, fontWeight: `${userFontWeight}` } }}
          defaultValue="The quick brown fox jumps over the lazy dog"
          multiline
          rows={2}
        />
      </div>
    </Container>
  );
}

codesandbox

I think it has something to do with the way I'm using useState, but the functionality itself seems to work.


Solution

  • Removing the key props on the IOSSlider components appears to fix the issue in your sandbox.

    <IOSSlider
      aria-label="Font Size Slider"
      // key={sliderSizeTracker} // <-- remove this
      defaultValue={sliderSizeTracker}
      marks={marks}
      step={25}
      onChangeCommitted={handleSizeChange}
      valueLabelDisplay="off"
    />
    
    <IOSSlider
      aria-label="Font Weight Slider"
      // key={sliderWeightTracker} // <-- remove this
      defaultValue={sliderWeightTracker}
      marks={marks}
      step={25}
      onChangeCommitted={handleWeightChange}
      valueLabelDisplay="off"
    />
    

    The other issue in your sandbox is that you are using React 18 but using the pre-React 18 ReactDOM pattern to render your app. It should be updated to the following:

    index.js

    import React from "react";
    import { createRoot } from "react-dom/client";
    import Demo from "./demo";
    
    const rootElement = document.getElementById("root");
    const root = createRoot(rootElement);
    
    root.render(<Demo />);