MrJibus
MrJibus

Reputation: 113

React Hooks - Input loses focus when adding or removing input fields dynamically

I have a form displayed in a modal window. This form is divided into several tabs. One of them has two grouped field: a dropdown list countries and a description textfield. There is an "Add button" which allows to create a new grouped field.

The problem is that each time, I filled the textfield, i lost the focus, because the form is re-rendered. I tryed to move the form outside of the default function but i still have the same issue.

I also set unique keys to each element, but still.

I know there is a lot of documentation of this, but despite this, its not working. I could set the autofocus, but when there is more than a one group field, the focus will go to the last element.

I am using Material UI (react 17)

Anyway, below is the code (which has been truncated for a better visibility) :

  function GeoForm (props) {
            return(
            <React.Fragment>
                <Autocomplete
                  id={"country"+props.i}
                  style={{ width: 300 }}
                  options={Object.keys(countries.getNames('fr')).map(e => ({code: e, label: countries.getNames('fr')[e]}))}
                  getOptionSelected={(option, value) => (option.country === value.country)}
                  classes={{
                    option: props.classes.option,
                  }}
                  
                  defaultValue={props.x.country}
                  key={"country"+props.i}
                  name="country"
                  onChange={(e,v) => props.handleInputGeoCountryChange(e, v, props.i)}
                  getOptionLabel={(option) => (option ? option.label : "")}
                  renderOption={(option) => (
                    <React.Fragment>
                      {option.label}
                    </React.Fragment>
                  )}
                  renderInput={(params) => (
                    <TextField
                      {...params}
                      label="Choose a country"
                      variant="outlined"
                      inputProps={{
                        ...params.inputProps,
                        autoComplete: 'new-password', // disable autocomplete and autofill
                      }}
                    />
                  )}
                />
              <TextField
                  id={"destination"+props.i}
                  onChange={e => props.handleInputGeoDestinationChange(e, props.i)}
                  defaultValue={props.x.destination}
                  name="destination"
                  key={"destination"+props.i}
                  margin="dense"
                  label="Destination"
                  type="text"
                />
                {props.inputGeoList.length !== 1 && <button
                className="mr10"
                onClick={() => props.handleRemoveGeoItem(props.i)}>Delete</button>}
                {props.inputGeoList.length - 1 === props.i && 
                  <Button
                  onClick={props.handleAddGeoItem}
                  variant="contained"
                  color="primary"
                  //className={classes.button}
                  endIcon={<AddBoxIcon />}
                  >
                  Add
                  </Button>
                }
            </React.Fragment>
          )
        }
    
 export default function modalInfo(props) {
    const classes = useStyles();
      const [openEditDialog, setOpenEditDialog] = React.useState(false);
    
      const handleAddGeoItem = (e) => {
        console.log(e);
        setInputGeoList([...inputGeoList, { country: "", destination: "" }]);
      };
    
       // handle input change
      const handleInputGeoCountryChange = (e, v, index) => {
        const list = [...inputGeoList];
        list[index]['country'] = v;
        setInputGeoList(list);
      };
    
      const handleInputGeoDestinationChange = (e, index) => {
        const { name, value } = e.target;
        console.log(name);
        const list = [...inputGeoList];
        list[index][name] = value;
        setInputGeoList(list);
        console.log(inputGeoList)
      };
    
      // handle click event of the Remove button
      const handleRemoveGeoItem = index => {
        const list = [...inputGeoList];
        list.splice(index, 1);
        setInputGeoList(list);
      };
    
    const TabsEdit = (props) => {
    return(
          <div className={classes.root}>
            <form className={classes.form} noValidate onSubmit={onSubmit}>
            <Tabs
              orientation="vertical"
              variant="scrollable"
              value={value}
              onChange={handleChange}
              aria-label="Vertical tabs example"
              className={classes.tabs}
            >
            [...]
              <Tab label="Geo-targeting" {...a11yProps(4)} disableRipple />
            </Tabs>
            [...]
            </TabPanel>
            <TabPanel value={value} index={4}>
            { 
              inputGeoList.map((x, i)=>{
                return(
                  <GeoForm 
                    inputGeoList={inputGeoList}
                    x={x}
                    i={i}
                    handleRemoveGeoItem={handleRemoveGeoItem}
                    handleInputGeoDestinationChange={handleInputGeoDestinationChange}
                    handleInputGeoCountryChange={handleInputGeoCountryChange}
                    handleAddGeoItem={handleAddGeoItem}
                    handleInputGeoDestinationChange={handleInputGeoDestinationChange}
                    classes={classes}
                  />
                )
              })
            }
            </TabPanel>
            <TabPanel value={value} index={5}>
              Mobile-targeting
            </TabPanel>
            <DialogActions>
              <Button onClick={props.handleClose} color="primary">
                Annuler
              </Button>
              <Button type="submit" color="primary">
                Enregistrer
              </Button>
          </DialogActions>
            </form>
          </div>
        )
      }
    
     return (
        <div>
          <div>
          <EditIconButton onClickEdit={() => setOpenEditDialog(true)} />
          </div>
          <div>
            <EditDialog open={openEditDialog} handleClose={() => setOpenEditDialog(false)} >
              <TabsEdit/>
            </EditDialog>
          </div>
    
        
        </div>
      );
    

codesandbox

Any help or suggestion are welcome. Thank you

Upvotes: 1

Views: 1488

Answers (1)

David I. Samudio
David I. Samudio

Reputation: 2693

TL;DR: Your TabsEdit component was defined within another component, thus React was remounting it as a new component each time, making the focused state to be lost. This Pastebin fixes your code, it maintains the focus as you type.

NL;PR: I suffered from this same issue for months, the props are not the only reference checked for reconciliation, the component's memory ref is too. Since the component's function ref is different each time, React process it as a new component, thus unmounting the previous component, causing the state to be lost, in your case, the focus.

Upvotes: 3

Related Questions