reactjsjsxreact-portal

React child component lossing focus onChange


I have a parent component MasterList and I need to open a modal popup which contains a form to add designation and organization condionally based on activeTab (Designation or Organization) as I have two grids for Designation and Organization. I have ModalContainer component used for modal popup , which has the Input component as child component to add new master data (Deignation or Organization). So here the Input component has input field which losses the focus when I try to enter in the input field. I know this is happening due the re-rendering of the component that's why it is lossing focus, but I want a solution to resolve it. Or the best solution used in this kind of case. I am providing below MasterLst component, ModalContainer, Input component.

MasterLst component:

const MasterList = () => {

  const dispatch = useDispatch();
  const navigate = useNavigate();

  const [addMasterData, setAddMasterData] = useState({
    designation: { value: '', error: '' },
    description: { value: '', error: '' },
    organization: { value: '', error: '' },
  });

  const [addDesignationData, setAddDesignationData] = useState({
    designation: { value: '', error: '' },
    description: { value: '', error: '' },
  });

  const [addOrganizationData, setAddOrganizationData] = useState({
    organization: { value: '', error: '' },
  });

  const [activeTab, setActiveTab] = useState('Designation');
  const [showModal, setShowModal] = useState(false);

  const { mastersByType, loading: masterLoading } = useSelector((state) => state.mastersByType);
  const { credentials } = useSelector((state) => state.login);

  const resetaddMasterData = useCallback(() => {
    setAddMasterData({
      designation: { value: '', error: '' },
      description: { value: '', error: '' },
      organization: { value: '', error: '' },
    });
  }, []);

  const resetAddDesignationData = useCallback(() => {
    setAddDesignationData({
      designation: { value: '', error: '' },
      description: { value: '', error: '' },
    });
  }, []);

  const resetAddOrganizationData = useCallback(() => {
    setAddOrganizationData({
      organization: { value: '', error: '' },
    });
  }, []);


  const onMasterDataChange = useCallback(
    (event) => {
      const { name, value } = event.target;
      setAddMasterData((prevData) => ({
        ...prevData,
        [name]: { value, error: '' },
      }));
    },
    [setAddMasterData]
  );

  const onDesignationChange = useCallback(
    (event) => {
      const { name, value } = event.target;
      setAddDesignationData((prevData) => ({
        ...prevData,
        [name]: { value, error: '' },
      }));
    },
    [setAddDesignationData]
  );

  const onOrganizationChange = useCallback(
    (event) => {
      const { name, value } = event.target;
      setAddOrganizationData((prevData) => ({
        ...prevData,
        [name]: { value, error: '' },
      }));
    },
    [setAddOrganizationData]
  );

  const handleTabClick = useCallback(
    (tab) => {
      setActiveTab(tab);
    },
    [setActiveTab]
  );

  const inputRef = useRef(null);

  useEffect(() => {
    if (showModal) {
      inputRef.current && inputRef.current.focus();
    }
  }, [showModal]);

  useEffect(() => {
    if (credentials) {
      const data = {
        masterType: activeTab === 'Designation' ? 'Designation' : 'Organization',
      };
      dispatch(getMastersByType(data));
    }
  }, [activeTab, dispatch, credentials]);


  return (
    <PageContainer>
      <div className={styles.topContainer}>
        <div className={styles.left}>
          <label>Master List</label>
        </div>
        <div className={styles.right}>
          <img src="./images/scenario.png" />
        </div>
      </div>

      <div className={styles.mainContainer}>
        <div className={styles.mainTopContainer}>
          <div className={styles.mainTopLeft}>
            <div className={styles.designationsContainer}>
              <div className={styles.designationsTop}
                onClick={() => handleTabClick('Designation')}
              >
                <label
                  style={{
                    color: activeTab === 'Designation' ?
                      'var(--primary)' :
                      'var(--input_label)'
                  }}
                >
                  Designations
                </label>
              </div>
              <div
                className={styles.designationsBottom}
                style={{
                  backgroundColor: activeTab === 'Designation' ?
                    'var(--primary)' :
                    'var(--input_label)'
                }}
              ></div>
            </div>
            <div className={styles.organizationsContainer}
              onClick={() => handleTabClick('Organization')}

            >
              <div className={styles.organizationsTop}>
                <label
                  style={{
                    color: activeTab === 'Organization' ?
                      'var(--primary)' :
                      'var(--input_label)'
                  }}
                >
                  Organizations
                </label>
              </div>
              <div
                className={styles.organizationsBottom}
                style={{
                  backgroundColor: activeTab === 'Organization' ?
                    'var(--primary)' :
                    'var(--input_label)'
                }}
              ></div>
            </div>
          </div>
          <div className={styles.mainTopRight}>
            <Button
              onClick={() => {
                setShowModal(true);
              }}
            >
              Add New
            </Button>
          </div>
        </div>
        <div className={styles.mainBottomContainer}>
          {/* Master List Table:: start */}
          <div className={styles.mainTableContainer}>
            <table className={styles.table_content}>
              <thead>
                {activeTab === 'Designation' ?
                  (
                    <tr>
                      <th></th>
                      <th>#</th>
                      <th>Designation</th>
                      <th>Description</th>
                      <th>Date Created</th>
                      <th>Scenario</th>
                      <th>Status</th>
                      <th></th>
                    </tr>
                  ) : (
                    <tr>
                      <th></th>
                      <th>#</th>
                      <th>Organization</th>
                      <th>Member Users</th>
                      <th>Date Created</th>
                      <th>Games Played</th>
                      <th>Status</th>
                      <th></th>
                    </tr>
                  )}

              </thead>
              <tbody>
                {activeTab === 'Designation' ?
                  (
                    mastersByType &&
                    mastersByType.success &&
                    mastersByType.data &&
                    JSON.parse(mastersByType.data)?.map((master, index) => (
                      <tr key={index}>
                        <td>
                          <Checkbox />
                        </td>
                        <td>{index + 1}</td>
                        <td>{master.MasterDisplayName}</td>
                        <td>Description</td>
                        <td>1 Jan 2024</td>
                        <td>5</td>
                        <td>Active</td>
                        <td>
                          <div className={styles.actions}>
                            <div className={styles.circleSvg}>
                              <svg>
                                <use xlinkHref="sprite.svg#edit_icon" />
                              </svg>
                            </div>
                            <div className={styles.circleSvg}>
                              <svg>
                                <use xlinkHref="sprite.svg#delete_icon" />
                              </svg>
                            </div>
                          </div>
                        </td>
                      </tr>
                    ))
                  ) : (
                    mastersByType &&
                    mastersByType.success &&
                    mastersByType.data &&
                    JSON.parse(mastersByType.data)?.map((master, index) => (
                      <tr key={index}>
                        <td>
                          <Checkbox />
                        </td>
                        <td>{index + 1}</td>
                        <td>{master.MasterDisplayName}</td>
                        <td>25</td>
                        <td>1 Jan 2024</td>
                        <td>5</td>
                        <td>Active</td>
                        <td>
                          <div className={styles.actions}>
                            <div className={styles.circleSvg}>
                              <svg>
                                <use xlinkHref="sprite.svg#edit_icon" />
                              </svg>
                            </div>
                            <div className={styles.circleSvg}>
                              <svg>
                                <use xlinkHref="sprite.svg#delete_icon" />
                              </svg>
                            </div>
                          </div>
                        </td>
                      </tr>
                    ))
                  )
                }
              </tbody>
            </table>
          </div>
          {/* Master List Table:: end */}
        </div>
      </div>

      {/* Modal Container :: start*/}

      {showModal && (
        <ModalContainer>
          <div className="modal_content">
            <div className="modal_header">
              <div>
                {activeTab === 'Designation' ? 'Add Designation' : 'Add Organization'}
              </div>
              <div>
                <svg
                  className="modal_crossIcon"
                  onClick={() => {
                    setShowModal(false);
                    // resetAddGroupData();
                  }}
                >
                  <use xlinkHref={"sprite.svg#crossIcon"} />
                </svg>
              </div>
            </div>
            <div className={styles.modalInputContainer}>
              {activeTab === 'Designation' ?
                (
                  <div>
                    <Input
                      type="text"
                      customStyle={{ marginTop: '1rem', }}
                      value={addDesignationData.designation.value}
                      name={"designation"}
                      placeholder="Designation Name"
                      onChange={onDesignationChange}
                    />
                    <Input
                      type="text"
                      customStyle={{ marginTop: '1rem', }}
                      value={addDesignationData.description.value}
                      name={"description"}
                      placeholder="Description"
                      textAreaStyleClass={styles.textAreaStyleClass}
                      onChange={onDesignationChange}
                      textArea
                    />
                  </div>
                ) : (
                  <div>
                    <Input
                      type="text"
                      customStyle={{ marginTop: '1rem', }}
                      value={addOrganizationData.organization.value}
                      name={"organization"}
                      placeholder="Organization Name"
                      onChange={onOrganizationChange}
                    />
                  </div>
                )
              }

            </div>

            <div className="modal_buttonContainer">
              <Button
                buttonType={"cancel"}
                onClick={() => {
                  setShowModal(false);
                  // resetAddGroupData();
                }}
              >
                Cancel
              </Button>
              <Button
                customStyle={{
                  marginLeft: "1rem",
                }}
              // onClick={onAddGroup}
              >
                Add
              </Button>
            </div>
          </div>
        </ModalContainer>
      )}

      {/* Modal Container :: end*/}
    </PageContainer>
  );
};

export default MasterList;

ModalContainer component:

import React, { useState } from "react";
import ReactDOM from "react-dom";
import styles from "./modal.module.css";

const ModalContainer = ({ children }) => {
  const modalRoot = document.getElementById("modal-root");
  const modalElement = document.createElement("div");

  modalRoot.appendChild(modalElement);

  return ReactDOM.createPortal(
    <div className={styles.container}>{children}</div>,
    modalElement
  );
};

export default ModalContainer;

Input Component :

import style from "./input.module.css";

const Input = ({
  textArea,
  type,
  customStyle,
  label,
  name,
  disabled = false,
  value = "",
  labelStyle = "",
  textAreaStyleClass,
  onChange = () => {},
  ref,
  ...props
}) => {
  return (
    <div style={customStyle} className={style.formGroup}>
      <label className={labelStyle}>{label}</label>
      {textArea ? (
        <textarea
          disabled={disabled}
          name={name}
          value={value}
          className={`${style.formControl} ${textAreaStyleClass}`}
          placeholder={label}
          {...props}
          onChange={onChange}
        />
      ) : (
        <input
          disabled={disabled}
          type={type}
          name={name}
          value={value}
          className={style.formControl}
          placeholder={label}
          ref={ref}
          {...props}
          onChange={onChange}
        />
      )}
    </div>
  );
};

export default Input;

I tried using autoFocus , useRef, but none of them worked. I am new to React and I don't know how to resolve this.

My one of the collegue suggested to create the container inside the modal in the same file as a component but without exporting and using in the same file i.e MasterList. Any suggestion, solutions would be helpfull.


Solution

  • You are creating new div and attaching it to #modal-root on each render. This makes createPortal to receive new element and that unmounts the previous content and mounts the new one. Something like this should solve this problem:

    const ModalContainer = ({ children }) => {
            const element = useRef()  
            // here the element is created only once
            if (!element.current) element.current = document.createElement('div')
        
            useLayoutEffect(() => {
                const target = document.getElementById('modal-root')
                // element is attached to the target only once
                target.appendChild(element.current)
                return () => {
                    // remove your created element on unmount
                    target.removeChild(element.current)
                }
            }, [])
        
            return createPortal(children, element.current)
         }