Warm tip: This article is reproduced from stackoverflow.com, please click
forms javascript reactjs focus material-ui

React Beginner Question: Textfield Losing Focus On Update

发布于 2020-03-27 10:31:40

I wrote a component that is supposed to list out a bunch of checkboxes with corresponding textfields. When you click on the checkboxes, or type in the fields it's meant to update state.

The textbox is working ok, but when I type in the fields, it updates state ok, but I lose focus whenever I tap the keyboard.

I realized this is probably due to not having keys set, so I added keys to everything but it still is losing focus. At one point I tried adding in stopPropegation on my events because I thought maybe that was causing an issue?? I'm not sure.. still learning...didn't seem to work so I removed that part too.

Still can't seem to figure out what is causing it to lose focus... does anyone have any advice/solves for this issue?

I consolidated my code and cut out the unnecessary bits to make it easier to read. There are three relevant JS files.. please see below:

I'm still a beginner/learning so if you have useful advice related to any part of this code, feel free to offer. Thanks!

App.js

import React, { Component } from 'react';
import Form from './Form'

class App extends Component {
constructor() {
  super();
  this.state = {
      mediaDeliverables: [
        {label: 'badf', checked: false, quantity:''},
        {label: 'adfadf', checked: false, quantity:''},
        {label: 'adadf', checked: false, quantity:''},
        {label: 'addadf', checked: false, quantity:''},
        {label: 'adfdes', checked: false, quantity:''},
        {label: 'hghdgs', checked: false, quantity:''},
        {label: 'srtnf', checked: false, quantity:''},
        {label: 'xfthd', checked: false, quantity:''},
        {label: 'sbnhrr', checked: false, quantity:''},
        {label: 'sfghhh', checked: false, quantity:''},
        {label: 'sssddrr', checked: false, quantity:''}
      ]
  }
}

setMediaDeliverable = (value, index) => {
  let currentState = this.getStateCopy();
  currentState.mediaDeliverables[index] = value;
  this.setState(currentState);
} 

getStateCopy = () => Object.assign({}, this.state);

render() {
  return (
    <div className="App">
          <Form 
            key="mainForm"
            mediaDeliverablesOptions={this.state.mediaDeliverables}
            setMediaDeliverable={this.setMediaDeliverable}
          />  
    </div>
  );
  }
}
export default App;

Form.js

import React from 'react';
import { makeStyles, useTheme } from '@material-ui/core/styles';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import MediaDeliverablesCheckBox from './MediaDeliverablesCheckBox';


const useStyles = makeStyles(theme => ({
  container: {
    display: 'inline-block',
    flexWrap: 'wrap',
  },
  root: {
    display: 'inline-block',
    flexWrap: 'wrap',
    maxWidth: 600,
    textAlign: 'left',
  },
  extendedIcon: {
    marginRight: theme.spacing(1),
  },
  formControl: {
    margin: theme.spacing(1),
    minWidth: 120,
    maxWidth: 300,
  },
  textField: {
    marginLeft: theme.spacing(1),
    marginRight: theme.spacing(1),
    width: 370,
  },
  dense: {
    marginTop: 19,
  },
  chips: {
    display: 'flex',
    flexWrap: 'wrap',
  },
  chip: {
    margin: 2,
  },
  noLabel: {
    marginTop: theme.spacing(3),
  },
}));

const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
  PaperProps: {
    style: {
      maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
      width: 250,
    },
  },
};

function getStyles(name, accountName, theme) {
  // console.log('>> [form.js] (getStyles) ',accountName)
  return {
    fontWeight:
      accountName.indexOf(name) === -1
        ? theme.typography.fontWeightRegular
        : theme.typography.fontWeightMedium,
  };
}



export default function Form(props) {

  const mediaDeliverablesOptions = props.mediaDeliverablesOptions;
  const classes = useStyles();
  const theme = useTheme();

  const CheckboxGroup = ({ values, label, onChange }) => (
  <FormControl component="fieldset">
    <FormLabel component="legend">{label}</FormLabel>
    <FormGroup>
      {values.map((value, index) => (
        <FormControlLabel
          key={index}
          control={
            <Checkbox
              checked={value.checked}
              onChange={onChange(index)}
            />
          }
          label={value.label}
        />
      ))}
    </FormGroup>
  </FormControl>
);

  const MediaDeliverableCheckBoxList = ({values, label}) => (
    <FormControl component="fieldset">
    <FormLabel component="legend">{label}</FormLabel>
    <FormGroup>
    {values.map((value, index) => (
        <MediaDeliverablesCheckBox
        key={index}
        mediaDeliverablesOptions={value}
        onMediaDeliverableChange={onMediaDeliverableChange(index)}
      />
      ))}
    </FormGroup>
  </FormControl>
    );



  const onCheckBoxChange = index => ({ target: { checked } }) => {
    const newValues = [...values];
    const value = values[index];
    newValues[index] = { ...value, checked };
    props.setDesignOrDigital(newValues);
  };

  const onMediaDeliverableChange = index => (deliverableData, e) => {
    props.setMediaDeliverable(deliverableData, index);
  }

  return (
    <div className={classes.root}>
      <MediaDeliverableCheckBoxList
        label="Please Choose Deliverables:"
        values={mediaDeliverablesOptions}
        key="media-deliverable-checkbox-list"
      />
    </div>
  );
}

MediaDeliverablesCheckbox.js

import React from 'react';
import Checkbox from '@material-ui/core/Checkbox';
import { makeStyles, useTheme } from '@material-ui/core/styles';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import TextField from '@material-ui/core/TextField';

export default function MediaDeliverablesCheckBox(props) {

  let deliverableData = Object.assign({}, props.mediaDeliverablesOptions);

  const onCheckBoxChange = (e) => {
     deliverableData.checked = e.target.checked;
     props.onMediaDeliverableChange(deliverableData, e);
  }

  const onQuantityChange = (e) => {
     deliverableData.quantity = e.target.value;
     props.onMediaDeliverableChange(deliverableData, e);
  }

  const CheckboxGroup = ({ value, label }) => (

  <FormControl component="fieldset">
    <FormGroup>
        <FormControlLabel
          control={
            <Checkbox
              key={props.index}
              checked={value.checked}
              onChange={onCheckBoxChange}
            />
          }
          label={label}
        />
    </FormGroup>
  </FormControl>
  );

return(
    <div className="MediaDeliverablesCheckBox">
      <CheckboxGroup
            key={props.index}
            label={props.mediaDeliverablesOptions.label}
            value={props.mediaDeliverablesOptions}
          />
      <TextField
        key={'tf'+props.index}
        id={'quantity-'+props.index}
        label="Quantity"
        placeholder="How many do you need?"
        multiline
        variant="outlined"
        value={props.mediaDeliverablesOptions.quantity}
        onChange={onQuantityChange}
        fullWidth
      />
    </div>
  );
}

Updated Form.js based on recommended edits by Ryan C.

import React from 'react';
import { makeStyles, useTheme } from '@material-ui/core/styles';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import MediaDeliverablesCheckBox from './MediaDeliverablesCheckBox';


const useStyles = makeStyles(theme => ({
  container: {
    display: 'inline-block',
    flexWrap: 'wrap',
  },
  root: {
    display: 'inline-block',
    flexWrap: 'wrap',
    maxWidth: 600,
    textAlign: 'left',
  },
  extendedIcon: {
    marginRight: theme.spacing(1),
  },
  formControl: {
    margin: theme.spacing(1),
    minWidth: 120,
    maxWidth: 300,
  },
  textField: {
    marginLeft: theme.spacing(1),
    marginRight: theme.spacing(1),
    width: 370,
  },
  dense: {
    marginTop: 19,
  },
  chips: {
    display: 'flex',
    flexWrap: 'wrap',
  },
  chip: {
    margin: 2,
  },
  noLabel: {
    marginTop: theme.spacing(3),
  },
}));

const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
  PaperProps: {
    style: {
      maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
      width: 250,
    },
  },
};

function getStyles(name, accountName, theme) {
  return {
    fontWeight:
      accountName.indexOf(name) === -1
        ? theme.typography.fontWeightRegular
        : theme.typography.fontWeightMedium,
  };
}


// Failed to compile
// ./src/Form.js
//   Line 86:  Parsing error: Unexpected token, expected ","

//   84 | 
//   85 | const MediaDeliverableCheckBoxList = ({values, label, onMediaDeliverableChange}) => (
// > 86 |     {values.map((value, index) => (
//      |            ^
//   87 |         <MediaDeliverablesCheckBox
//   88 |         key={index}
//   89 |         index={index}
// This error occurred during the build time and cannot be dismissed.

const MediaDeliverableCheckBoxList = ({values, label, onMediaDeliverableChange}) => (
    {values.map((value, index) => (
        <MediaDeliverablesCheckBox
        key={index}
        index={index}
        mediaDeliverablesOptions={value}
        onMediaDeliverableChange={onMediaDeliverableChange(index)}
      />
      ))}
);

export default function Form(props) {

  const mediaDeliverablesOptions = props.mediaDeliverablesOptions;
  const classes = useStyles();
  const theme = useTheme();

  const CheckboxGroup = ({ values, label, onChange }) => (
  <FormControl component="fieldset">
    <FormLabel component="legend">{label}</FormLabel>
    <FormGroup>
      {values.map((value, index) => (
        <FormControlLabel
          key={index}
          control={
            <Checkbox
              checked={value.checked}
              onChange={onChange(index)}
            />
          }
          label={value.label}
        />
      ))}
    </FormGroup>
  </FormControl>
);


  const onCheckBoxChange = index => ({ target: { checked } }) => {
    const newValues = [...values];
    const value = values[index];
    newValues[index] = { ...value, checked };
    props.setDesignOrDigital(newValues);
  };

  const onMediaDeliverableChange = index => (deliverableData, e) => {
    props.setMediaDeliverable(deliverableData, index);
  }

  return (
    <div className={classes.root}>
      <MediaDeliverableCheckBoxList
        onMediaDeliverableChange={onMediaDeliverableChange}
      />
    </div>
  );
}
Questioner
Cmaxster
Viewed
55
Ryan Cogswell 2019-07-04 02:38

I see two main issues:

  1. How you are defining your different components (nesting component types)
  2. Not passing the index prop through to components that are expecting it

You have the following structure (leaving out details that are not directly related to my point):

export default function Form(props) {

  const onMediaDeliverableChange = index => (deliverableData, e) => {
    props.setMediaDeliverable(deliverableData, index);
  }

  const MediaDeliverableCheckBoxList = ({values, label}) => (
    <FormGroup>
    {values.map((value, index) => (
        <MediaDeliverablesCheckBox key={index} onMediaDeliverableChange={onMediaDeliverableChange(index)}/>
      ))}
    </FormGroup>
    );

  return (
      <MediaDeliverableCheckBoxList/>
  );
}

The function MediaDeliverableCheckBoxList represents the component type used to render the <MediaDeliverableCheckBoxList/> element. Whenever Form is re-rendered due to props or state changing, React will re-render its children. If the component type of a particular child is the same (plus some other criteria such as key being the same if specified), then it will update the existing DOM node(s). If the component type of a particular child is different, then the corresponding DOM nodes will be removed and new ones added to the DOM.

By defining the MediaDeliverableCheckBoxList component type within the Form function, you are causing that component type to be different on every render. This will cause all of the DOM nodes to be replaced rather than just updated and this will cause the focus to go away when the DOM node that previously had focus gets removed. It will also cause performance to be considerably worse.

You can fix this by moving this component type outside of the Form function and then adding any additional props that are needed (e.g. onMediaDeliverableChange) to convey the context known inside of Form. You also need to pass index as a prop to MediaDeliverablesCheckBox since it is using it.

const MediaDeliverableCheckBoxList = ({values, label, onMediaDeliverableChange}) => (
    <FormGroup>
    {values.map((value, index) => (
        <MediaDeliverablesCheckBox key={index} index={index} onMediaDeliverableChange={onMediaDeliverableChange(index)}/>
      ))}
    </FormGroup>
);


export default function Form(props) {

  const onMediaDeliverableChange = index => (deliverableData, e) => {
    props.setMediaDeliverable(deliverableData, index);
  }


  return (
      <MediaDeliverableCheckBoxList onMediaDeliverableChange={onMediaDeliverableChange}/>
  );
}

You have this same issue with CheckboxGroup and possibly other components as well.