Peter Penzov
Peter Penzov

Reputation: 1678

A component is changing the uncontrolled value state of Select to be controlled

I'm trying to create a edit form to edit data from database by id. I tried this:

    import React, {FormEvent, useEffect, useState} from "react";
    import TextField from "@material-ui/core/TextField";
    import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
    import {
      TicketFullDTO,
      TicketStatusTypesDTO,
    } from "../../service/support/types";
    import {
      getTicket,
      getTicketStatusTypes,
      updateTicket,
    } from "../../service/support";
    import { useHistory, useParams } from "react-router-dom";
    import InputLabel from "@mui/material/InputLabel";
    import Select from "@mui/material/Select";
    import MenuItem from "@mui/material/MenuItem";
    import { FormControl } from "@mui/material";
    import { Moment } from "moment";
    import { RouteParams } from "../../service/utils";
       
    export default function TicketProfile(props: any) {
      const classes = useStyles();
      let history = useHistory();
      let requestParams = useParams<RouteParams>();

      const [status, setStatus] = useState<string>("");
      const [submitDate, setSubmitDate] = useState<Moment | null>(null);
      const [ticket, setTicket] = useState<TicketFullDTO>();
    
      const formSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log(e);

        updateTicket(requestParams.id, data)
          .then(({ data }) => {
            console.log(data.title);
            history.replace("/support");
          })
          .catch((err) => {
            console.log(err);
          });
      };
    
      const [ticketCategoriesList, setTicketCategoriesList] = useState<
        TicketCategoryTypesDTO[]
      >([]);
      const [ticket, setTicket] = useState<TicketFullDTO>();
            
      const getSingleTicket = async () => {
        getTicket(requestParams.id)
          .then(({ data }) => {
            setTicket(data);
          })
          .catch((error) => {
            console.error(error);
          });
      };
    
      const [ticketStatusList, setTicketStatusList] = useState<
        TicketStatusTypesDTO[]
      >([]);
    
      useEffect(() => {
        ticketStatusData();
        getSingleTicket();
      }, []);
    
      const ticketStatusData = async () => {
        getTicketStatusTypes()
          .then((resp) => {
            setTicketStatusList(resp.data);
          })
          .catch((error) => {
            console.error(error);
          });
      };
    
      return (
        <Container>
            <form onSubmit={onSubmit}>

                          .........

                          <TextField
                            value={ticket?.title}
                            id="title"                                             
                            onChange={({ target: { value } }) => {
                              setTicket({ ...ticket, title: value });
                            }}
                          />

                          .........

                          <FormControl>
                            <TextField
                              label="Submit Date"
                              id="submit-date"
                              type="date"
                              defaultValue={ticket?.submitDate}                             
                              //@ts-ignore
                              onInput={(e) => setSubmitDate(e.target.value)}
                            />
                          </FormControl>
                       
                          ..........

                            <Select
                              labelId="status-label"
                              id="status-helper"
                              value={ticket?.status}
                              onChange={(e) => setStatus(e.target.value)}
                              required
                            >
                              {ticketStatusList.map((element) => (
                                <MenuItem value={element.code}>
                                  {element.name}
                                </MenuItem>
                              ))}
                            </Select>
                          </FormControl>

                         ...........
                      
                          <Button
                            type="submit"
                          >
                            Update Ticket
                          </Button>                       
    
        </Container>
      );
    }


.....


export async function updateTicket(
    id: string,
    data: TicketFullDTO
): Promise<AxiosResponse<TicketFullDTO>> {
  return await axios.post<TicketFullDTO>(
      `${baseUrl}/management/support/tickets/ticket/${id}`,
      {
        data,
      }
  );
}

export interface TicketFullDTO {
    id?: number,
    title?: string,
    status?: string,
    submitDate?: Moment | null
}

I get error in Chrome console:

MUI: A component is changing the uncontrolled value state of Select to be controlled. Elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled Select element for the lifetime of the component. The nature of the state is determined during the first render. It's considered controlled if the value is not undefined.

The value for Select should be selected using the value ticket?.status when list ticketStatusList But the data object is not running before rendering the UI content and the value into Select dropdown is not selected.

Do you know how I can fix this issue?

Upvotes: 7

Views: 12026

Answers (5)

Nguyen Thanh Ha
Nguyen Thanh Ha

Reputation: 238

Try this

                            <Select
                              labelId="status-label"
                              id="status-helper"
                              value={status}
                              onChange={(e) => setStatus(e.target.value)}
                              required
                            >

And to synchronize status ticket with ticket state

      const [status, setStatus] = useState<string>("");
      const [submitDate, setSubmitDate] = useState<Moment | null>(null);
      const [ticket, setTicket] = useState<TicketFullDTO>();
      React.useEffect(() => setTicket(previousTicket => 
          ({ ...previousTicket, status })), [status]);

Upvotes: 2

Giancarlo
Giancarlo

Reputation: 9

The problem is that the optional chaining operator returns undefined. Try replacing

value={ticket?.status}

with

value={ticket?.status || null}

To solve the TypeScript error, in the useState hook, just declare all possible types of the state variable:

const [status, setStatus] = useState<string | null>("");

Upvotes: 1

Madhuri
Madhuri

Reputation: 1100

for typescript error: you need to write the same type and just set value only if its not null, so type error won't bother you

const handleChange = (value: string | null) => {
  if (!value) {
    setStatus(value)
  } else setStatus('')
}

 
<Select
   labelId="status-label"
   id="status-helper"
   value={ticket.status || null}  // default value
   onChange={(event, value) => handleChange(value)}
   required
>

Upvotes: 2

NearHuscarl
NearHuscarl

Reputation: 81370

First, in your MenuItem, set the value prop to object (element) instead of a string (element.code), this is because you pass an object as a value prop to your Select. The current value of the Select must have the same type as the value in MenuItem:

<Select
  labelId="status-label"
  id="status-helper"
  required
  value={status} // <------------------------- because value is an object here
  onChange={(e) => setStatus(e.target.value)}
>
  {ticketStatusList.map((element) => (
    <MenuItem
      key={element.code}
      value={element}  // <--- this one should be an object too as a result
    >
      {element.name}
    </MenuItem>
  ))}
</Select>

Then in your status state declaration, add a null object to change your Select to controlled mode

const [status, setStatus] = useState<TicketStatusTypesDTO>({});

Codesandbox Demo

Upvotes: 1

Mordechai
Mordechai

Reputation: 16234

React figures out if a component is controlled or not by checking if value is set on the first render. defaultValue should only be used for uncontrolled components.

Since you're working with controlled components you must provide a default value other than undefined in the value prop:

<Select
    labelId="status-label"
    id="status-helper"
    value={ticket?.status ?? null}
    onChange={(e) => setStatus(e.target.value)}
    required
>

More on this topic in the docs.

Upvotes: 16

Related Questions