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?