Search code examples
reactjsmaterial-uionchangereact-hook-form

Textfield loses focus after one key in react onChange


When I enter a key in my textFields they lose focus. I have found out that it's because of the onChange but I don't see anything wrong with it

onChange(e.target.value)

I have made a sample in codesandbox.io https://codesandbox.io/s/gallant-dust-3wdzhh?file=/src/App.js

Here's the code (from that sandbox) for the text fields:

import * as React from "react";
import { styled } from "@mui/material/styles";
import Box from "@mui/material/Box";
import {
  TextField,
  Input,
  inputBaseClasses,
  FormControl,
  InputLabel
} from "@mui/material";
import { BiShow, BiHide } from "react-icons/bi";
import { useState } from "react";

const ValidationTextField = styled(TextField)({
  "& input:valid + fieldset": {
    borderColor: "green",
    borderWidth: 2
  },
  "& input:invalid + fieldset": {
    borderColor: "red",
    borderWidth: 2
  },
  "&:hover:valid + fieldset": {
    borderColor: "yellow"
  },
  "& input:valid:focus + fieldset": {
    borderLeftWidth: 6,
    borderColor: "purple",
    color: "pink",
    padding: "4px !important"
  },
  "& .MuiOutlinedInput-root": {
    "&:hover fieldset": {
      borderColor: "yellow",
      color: "orange"
    }
  }
});

const StyledInput = styled(Input)({
  borderRadius: 4,
  border: "2px solid blue",
  padding: 4,
  [`&.${inputBaseClasses.multiline}`]: {
    height: "auto",
    border: "2px solid red"
  }
});

export default function Text({
  errors,
  label,
  rows,
  type,
  defaultValue,
  required,
  onChange,
  setValue,
  name
}) {
  const [showPass, setShowPass] = useState(false);

  /*const handleChange = (e) => {
    const eTarget = e.target.value;
    const eName = e.target.name;
    setValue(eName, eTarget);
  };*/

  const PassWordIcon = () =>
    showPass ? (
      <BiHide size={40} onClick={() => setShowPass(false)} />
    ) : (
      <BiShow size={40} onClick={() => setShowPass(true)} />
    );

  const Multiline = () => {
    return (
      <FormControl variant="outlined">
        <InputLabel>{label}</InputLabel>
        <StyledInput
          sx={{
            "&:hover": {
              border: "2px solid yellow"
            },
            "&.Mui-focused": {
              borderColor: "purple",
              color: "pink",
              padding: "4px !important"
            }
          }}
          onChange={(e) => onChange(e.target.value)}
          disableUnderline
          multiline
          rows={rows}
        />
      </FormControl>
    );
  };

  const Password = () => {
    return (
      <Box>
        <ValidationTextField
          label={label}
          required={required}
          error={errors}
          onChange={(e) => onChange(e.target.value)}
          type={showPass ? "text" : "password"}
          sx={{
            input: {
              color: "red",
              "&::placeholder": {
                color: "darkgreen",
                "&:hover fieldset": {
                  color: "orange"
                }
              }
            },
            label: {
              color: "pink"
            }
          }}
          variant="outlined"
          defaultValue={defaultValue}
        />
        <PassWordIcon />
      </Box>
    );
  };

  const TextInput = () => {
    return (
      <ValidationTextField
        label={label}
        required={required}
        type={type}
        onChange={(e) => onChange(e.target.value)}
        sx={{
          input: {
            color: "red",
            "&::placeholder": {
              color: "darkgreen",
              "&:hover fieldset": {
                color: "orange"
              }
            }
          },
          label: {
            color: "pink"
          }
        }}
        variant="outlined"
        defaultValue={defaultValue}
      />
    );
  };

  const inputType = (type) => {
    switch (type) {
      case "multiline":
        return <Multiline />;
      case "password":
        return <Password />;
      default:
        return <TextInput />;
    }
  };

  return <Box>{inputType(type)}</Box>;
}

Solution

  • You are defining the PasswordIcon, Multiline, Password, and TextInput components inside of the Text component. Though it is fine to use components inside other components, you should never define components inside other components. Doing so causes the component to be redefined on each render of the containing component which then prevents React from recognizing it as the same type of component. This in turn causes React to remount the element (i.e. completely remove it from the DOM and then add a new element back into the DOM in its place) rather than just re-render it.

    Instead, you should define all components at the top level (i.e. not nested within another component or function). Any variables from the containing component that the nested components were dependent on can be passed as props.

    Here's what this would look like in your case:

    import * as React from "react";
    import { styled } from "@mui/material/styles";
    import Box from "@mui/material/Box";
    import {
      TextField,
      Input,
      inputBaseClasses,
      FormControl,
      InputLabel
    } from "@mui/material";
    import { BiShow, BiHide } from "react-icons/bi";
    import { useState } from "react";
    
    const ValidationTextField = styled(TextField)({
      "& input:valid + fieldset": {
        borderColor: "green",
        borderWidth: 2
      },
      "& input:invalid + fieldset": {
        borderColor: "red",
        borderWidth: 2
      },
      "&:hover:valid + fieldset": {
        borderColor: "yellow"
      },
      "& input:valid:focus + fieldset": {
        borderLeftWidth: 6,
        borderColor: "purple",
        color: "pink",
        padding: "4px !important"
      },
      "& .MuiOutlinedInput-root": {
        "&:hover fieldset": {
          borderColor: "yellow",
          color: "orange"
        }
      }
    });
    
    const StyledInput = styled(Input)({
      borderRadius: 4,
      border: "2px solid blue",
      padding: 4,
      [`&.${inputBaseClasses.multiline}`]: {
        height: "auto",
        border: "2px solid red"
      }
    });
    
    const PassWordIcon = ({ showPass, setShowPass }) =>
      showPass ? (
        <BiHide size={40} onClick={() => setShowPass(false)} />
      ) : (
        <BiShow size={40} onClick={() => setShowPass(true)} />
      );
    const Multiline = ({ label, onChange, rows }) => {
      return (
        <FormControl variant="outlined">
          <InputLabel>{label}</InputLabel>
          <StyledInput
            sx={{
              "&:hover": {
                border: "2px solid yellow"
              },
              "&.Mui-focused": {
                borderColor: "purple",
                color: "pink",
                padding: "4px !important"
              }
            }}
            onChange={(e) => onChange(e.target.value)}
            disableUnderline
            multiline
            rows={rows}
          />
        </FormControl>
      );
    };
    const Password = ({ label, required, errors, onChange, defaultValue }) => {
      const [showPass, setShowPass] = useState(false);
      return (
        <Box>
          <ValidationTextField
            label={label}
            required={required}
            error={errors}
            onChange={(e) => onChange(e.target.value)}
            type={showPass ? "text" : "password"}
            sx={{
              input: {
                color: "red",
                "&::placeholder": {
                  color: "darkgreen",
                  "&:hover fieldset": {
                    color: "orange"
                  }
                }
              },
              label: {
                color: "pink"
              }
            }}
            variant="outlined"
            defaultValue={defaultValue}
          />
          <PassWordIcon showPass={showPass} setShowPass={setShowPass} />
        </Box>
      );
    };
    const TextInput = ({ label, required, type, onChange, defaultValue }) => {
      return (
        <ValidationTextField
          label={label}
          required={required}
          type={type}
          onChange={(e) => onChange(e.target.value)}
          sx={{
            input: {
              color: "red",
              "&::placeholder": {
                color: "darkgreen",
                "&:hover fieldset": {
                  color: "orange"
                }
              }
            },
            label: {
              color: "pink"
            }
          }}
          variant="outlined"
          defaultValue={defaultValue}
        />
      );
    };
    
    export default function Text({
      errors,
      label,
      rows,
      type,
      defaultValue,
      required,
      onChange,
      setValue,
      name
    }) {
      const inputType = (type) => {
        switch (type) {
          case "multiline":
            return <Multiline label={label} onChange={onChange} rows={rows} />;
          case "password":
            return (
              <Password
                label={label}
                required={required}
                errors={errors}
                onChange={onChange}
                defaultValue={defaultValue}
              />
            );
          default:
            return (
              <TextInput
                label={label}
                required={required}
                type={type}
                onChange={onChange}
                defaultValue={defaultValue}
              />
            );
        }
      };
    
      return <Box>{inputType(type)}</Box>;
    }
    

    Edit top-level component definitions

    Related answers (same root cause):

    Related documentation: https://react.dev/learn/your-first-component#nesting-and-organizing-components

    Excerpt:

    Components can render other components, but you must never nest their definitions. Instead, define every component at the top level.