0

In a React v16.13.1 using Material-UI v4.11.0 I have the following component

  const GuestSignup = (props: GuestSignupProps) => {

    return <Grid container spacing={1} alignItems="flex-end">
      <Grid item xs={1}>
        <Tooltip title="Uncheck to cancel individual signup" arrow>
          <FormControl>
            <FormControlLabel
              control={<Checkbox key="ck1" onChange={props.handleSignupCheckChangeGuest(props.gs.guestSignupID === global.NULL_ID ? props.gs.index : props.gs.guestSignupID)} value={props.gs.guestSignupID} checked={props.gs.selected} />}
              label=""
            />
          </FormControl>
        </Tooltip>
      </Grid>
      <Grid item xs={2}>
        <TextField label="First Name" value={props.gs.firstName} key={props.gs.guestSignupID.toString() + 'fn'} required onChange={props.handleFirstNameChange(props.gs.guestSignupID, props.gs.index)} />
      </Grid>
      <Grid item xs={2}>
        <TextField label="Last Name" value={props.gs.lastName} key={props.gs.guestSignupID.toString() + 'ln'} required onChange={props.handleLastNameChange(props.gs.guestSignupID, props.gs.index)} />
      </Grid>
      <Grid item xs={2}>
        <GuestTypes value={props.gs.guestType.guestTypeID} key="gt" gt={props.gt} handleGuestTypeChange={props.handleGuestTypeChange(props.gs.guestSignupID, props.gs.index)} />
      </Grid>
      <Grid item xs={2}>
        <KeyboardDatePicker
          autoOk
          className={classes.guestDate}
          disablePast
          disableToolbar
          variant="inline"
          format="MM/dd/yyyy"
          key="ad"
          margin="dense"
          label="Arrival"
          required
          value={props.gs.arrivalDate === global.EMPTY_STRING ? null : new Date(props.gs.arrivalDate)}
          onChange={props.handleArrivalDateChange(props.gs.guestSignupID === global.NULL_ID ? props.gs.index : props.gs.guestSignupID, GUEST_SIGNUP)}
          KeyboardButtonProps={{
            'aria-label': 'change date',
          }}
        />
      </Grid>
      <Grid item xs={2}>
        <KeyboardDatePicker
          autoOk
          className={classes.guestDate}
          disablePast
          disableToolbar
          variant="inline"
          format="MM/dd/yyyy"
          key="dd"
          margin="dense"
          label="Departure"
          required
          value={props.gs.departureDate === global.EMPTY_STRING ? null : new Date(props.gs.departureDate)}
          onChange={props.handleDepartureDateChange(props.gs.guestSignupID === global.NULL_ID ? props.gs.index : props.gs.guestSignupID, GUEST_SIGNUP)}
          KeyboardButtonProps={{
            'aria-label': 'change date',
          }}
        />
      </Grid>
    </Grid>
  }

with these input props

interface GuestSignupProps {
  id: number;
  key: number;
  gs: GuestSignup;
  gt: Array<GuestType>;
  handleArrivalDateChange(id: number, type: string): (date: Date | null) => void;
  handleDepartureDateChange(id: number, type: string): (date: Date | null) => void;
  handleFirstNameChange(id: number, index: number): (event: React.ChangeEvent<HTMLInputElement>) => void;
  handleGuestTypeChange(id: number, index: number): (event: React.ChangeEvent<{ value: unknown }>) => void;
  handleLastNameChange(id: number, index: number): (event: React.ChangeEvent<HTMLInputElement>) => void;
  handleSignupCheckChangeGuest(id: number): (event: React.ChangeEvent<HTMLInputElement>) => void;
}

which uses this data structure

export interface GuestSignup {
  guestSignupID: number;
  index: number;
  signupID: number;
  firstName: string;
  lastName: string;
  gender: string;
  guestType: GuestType;
  arrivalDate: string;
  departureDate: string;
  cancelDate: string;
  notes: string;
  cancel: boolean;
  selected: boolean;
}

which is stored in state as an array of multiple instances

  const [guestSignups, setGuestSignups] = React.useState<Array<GuestSignup>>([]);

and rendered like this where there are multiple GuestSignup instances in the page.

  {guestSignups.length > 0 &&
    <React.Fragment>
      {
        guestSignups.map((gs: GuestSignup, index: number) => (
          <GuestSignup id={gs.guestSignupID} key={gs.guestSignupID > 0 ? gs.guestSignupID : index} gs={gs} gt={guestTypes} handleArrivalDateChange={handleArrivalDateChange} handleDepartureDateChange={handleDepartureDateChange} handleFirstNameChange={handleFirstNameChange} handleGuestTypeChange={handleGuestTypeChange} handleLastNameChange={handleLastNameChange} handleSignupCheckChangeGuest={handleSignupCheckChangeGuest} />
        ))
      }
    </React.Fragment>
  }

The first and last name have handlers which are called in onChange w/ each keystroke

  const handleFirstNameChange = (id: number, index: number) => (event: React.ChangeEvent<HTMLInputElement>) => {

    // Used either GuestSignupID for existing guests or array index for new guests
    const newGuestSignup = produce(guestSignups,
      draft => {
        const i = draft.findIndex(gs => { return gs.guestSignupID > 0 ? gs.guestSignupID === id : gs.index === index });
        draft[i].firstName = event.target.value;
      }
    );

    setGuestSignups([ ...newGuestSignup ]);

  }

The problem is that after each keystroke the first and last name text inputs lose focus. They have unique keys amongst their siblings. It appears the entire set of guest signups is being re-rendered after the handler because if you hit TAB the first checkbox in the topmost guest signup gets focus.

What would be the proper approach to enable the text input elements to retain focus after each keystroke?

ChrisP
  • 9,796
  • 21
  • 77
  • 121
  • 1
    The way you are using `key` for `GuestSignup` is not advised. Since `key` helps React understand redraws, the way you're using it *might* be causing those redraws, which would make the inputs lose focus (since they'd be a new input each redraw) – gonzofish Oct 22 '20 at 14:41
  • 1
    Why do your `TextField` elements have a `key` at all? You only need keys when you render multiple components of the same type in a loop. – trixn Oct 22 '20 at 14:44
  • Please reproduce your problem in a [code sandbox](https://codesandbox.io/s/new). This is probably a similar root cause as [this question](https://stackoverflow.com/questions/56873912/react-beginner-question-textfield-losing-focus-on-update/56874906#56874906), but without a full reproduction it is not possible to be sure. – Ryan Cogswell Oct 22 '20 at 15:41
  • Since all the elements are within GRID cells are they considered siblings from a React perspective? Do they literally have to be element siblings or does React just consider siblings for adjacent elements that are tied to rendering? A simple UL as in the React documentation is straight forward. What about the above structure? Trying to understand what needs a keyl – ChrisP Oct 22 '20 at 16:44
  • @ChrisP You only need keys when rendering an array of elements (e.g. elements rendered in a `.map` call such as your `GuestSignup` elements). When the elements are explicitly laid out, keys aren't necessary (though they also won't hurt anything as long as the key doesn't change for a given element). – Ryan Cogswell Oct 22 '20 at 18:22
  • @ryancogswell Ok. What is required when a .map renders the GuestSignup element which contains other elements? Does only GuestSignup need a key or do the child input elements etc need keys? Thanks – ChrisP Oct 26 '20 at 13:40
  • @ChrisP Only `GuestSignup` (just the top-most elements in the array). – Ryan Cogswell Oct 26 '20 at 13:42

2 Answers2

0

Your whole problem is due to the fact that each State change leads to a re-render of the parent of the GuestSignup component. Which causes guestSignups.map((gs: GuestSignup, index: number). Accordingly, the page changes, updates and loses focus.

You need to make sure that when you change TextField only the component guestSignup had a re-render

Unfortunately, you have not fully specified the code of the parent in which you are changing the state, so I cannot show in your code what needs to be changed. But the solution to your problem for sure is that the state does not cause the parent's pre-render, move it to another place and just pass it through the props GuestSignup

BuGaGa
  • 302
  • 2
  • 9
-1

I also faced the same problem few months back .You need to remove the key attribute from all TextField tag. If you add key attribute , React will re render the component if key will change and your input will lose the focus.you can read more about key from here.

  • 2
    This answer is missing the reasoning. Why should all `key` attributes from all `TextField` tags be removed? Why does having these keys result in the described behaviour? – 3limin4t0r Oct 22 '20 at 14:45