Formik - State and initial values gets overwritten

Viewed 906

I want to implement an edit dialog for some metadata that I get from the backend. For this, I'm using Formik. When the user changes one metadata field then an icon is shown, which indicates that the field was changed. On submit, only the updated values should be sent to the backend. I read in other posts that you should compare the current field values with the initial values provided to the Formik form. This works perfectly fine for single values, like a changed title. However, the form I need to implement has also multi-value fields like creators. I implemented a custom field for that where the user can choose/provide multiple values for one field. The values of this field are saved as an array in the Formik form. The problem is that Formik also changes the initialValue of this field to the currently saved array and therefore I'm no longer able to check if the field is updated or not. Furthermore, I save the metadata fields provided by the backend in a state value because the response contains some further information needed for further implementation. This state value also contains the current value (before update) that a metadata field has and is used as initial values for the Formik form. Strangely, the multi-field component not only overwrites the initialValue of the field of the Formik form but also the value of the field in the state which is only read and never directly updated.

My dialog for editing metadata looks as follows:

const EditMetadataEventsModal = ({ close, selectedRows, updateBulkMetadata }) => {
  const { t } = useTranslation();

  const [selectedEvents, setSelectedEvents] = useState(selectedRows);
  const [metadataFields, setMetadataFields] = useState({});
  const [fetchedValues, setFetchedValues] = useState(null);

  useEffect(() => {
    async function fetchData() {
      let eventIds = [];
      selectedEvents.forEach((event) => eventIds.push(event.id));

      // metadata for chosen events is fetched from backend and saved in state
      const responseMetadataFields = await fetchEditMetadata(eventIds);
      let initialValues = getInitialValues(responseMetadataFields);
      setFetchedValues(initialValues);
      setMetadataFields(responseMetadataFields);
    }
    fetchData();
  }, []);

  const handleSubmit = (values) => {
    const response = updateBulkMetadata(metadataFields, values);
    close();
  };

  return (
    <>
      <div className="modal-animation modal-overlay" />
      <section className="modal wizard modal-animation">
        <header>
          <a className="fa fa-times close-modal" onClick={() => close()} />
          <h2>{t('BULK_ACTIONS.EDIT_EVENTS_METADATA.CAPTION')}</h2>
        </header>

        <MuiPickersUtilsProvider utils={DateFnsUtils} locale={currentLanguage.dateLocale}>
          <Formik initialValues={fetchedValues} onSubmit={(values) => handleSubmit(values)}>
            {(formik) => (
              <>
                <div className="modal-content">
                  <div className="modal-body">
                    <div className="full-col">
                      <div className="obj header-description">
                        <span>{t('EDIT.DESCRIPTION')}</span>
                      </div>
                      <div className="obj tbl-details">
                        <header>
                          <span>{t('EDIT.TABLE.CAPTION')}</span>
                        </header>
                        <div className="obj-container">
                          <table className="main-tbl">
                            <tbody>
                              {metadataFields.mergedMetadata.map(
                                (metadata, key) =>
                                  !metadata.readOnly && (
                                    <tr key={key} className={cn({ info: metadata.differentValues })}>
                                      <td>
                                        <span>{t(metadata.label)}</span>
                                        {metadata.required && <i className="required">*</i>}
                                      </td>
                                      <td className="editable ng-isolated-scope">
                                        {/* Render single value or multi value input */}
                                        {console.log('field value')}
                                        {console.log(fetchedValues[metadata.id])}
                                        {metadata.type === 'mixed_text' &&
                                        !!metadata.collection &&
                                        metadata.collection.length !== 0 ? (
                                          <Field name={metadata.id} fieldInfo={metadata} component={RenderMultiField} />
                                        ) : (
                                          <Field
                                            name={metadata.id}
                                            metadataField={metadata}
                                            showCheck
                                            component={RenderField}
                                          />
                                        )}
                                      </td>
                                    </tr>
                                  ),
                              )}
                            </tbody>
                          </table>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>

                {/* Buttons for cancel and submit */}
                <footer>
                  <button
                    type="submit"
                    onClick={() => formik.handleSubmit()}
                    disabled={!(formik.dirty && formik.isValid)}
                    className={cn('submit', {
                      active: formik.dirty && formik.isValid,
                      inactive: !(formik.dirty && formik.isValid),
                    })}
                  >
                    {t('UPDATE')}
                  </button>
                  <button onClick={() => close()} className="cancel">
                    {t('CLOSE')}
                  </button>
                </footer>

                <div className="btm-spacer" />
              </>
            )}
          </Formik>
        </MuiPickersUtilsProvider>
      </section>
    </>
  );
};

const getInitialValues = (metadataFields) => {
  // Transform metadata fields provided by backend
  let initialValues = {};
  metadataFields.mergedMetadata.forEach((field) => {
    initialValues[field.id] = field.value;
  });

  return initialValues;
};

Here is RenderMultiField:

const childRef = React.createRef();

const RenderMultiField = ({ fieldInfo, field, form }) => {
  // Indicator if currently edit mode is activated
  const [editMode, setEditMode] = useState(false);
  // Temporary storage for value user currently types in
  const [inputValue, setInputValue] = useState('');

  useEffect(() => {
    // Handle click outside the field and leave edit mode
    const handleClickOutside = (e) => {
      if (childRef.current && !childRef.current.contains(e.target)) {
        setEditMode(false);
      }
    };

    // Focus current field
    if (childRef && childRef.current && editMode === true) {
      childRef.current.focus();
    }

    // Adding event listener for detecting click outside
    window.addEventListener('mousedown', handleClickOutside);

    return () => {
      window.removeEventListener('mousedown', handleClickOutside);
    };
  }, []);

  // Handle change of value user currently types in
  const handleChange = (e) => {
    const itemValue = e.target.value;
    setInputValue(itemValue);
  };

  const handleKeyDown = (event) => {
    // Check if pressed key is Enter
    if (event.keyCode === 13 && inputValue !== '') {
      event.preventDefault();

      // add input to formik field value if not already added
      if (!field.value.find((e) => e === inputValue)) {
        field.value[field.value.length] = inputValue;
        form.setFieldValue(field.name, field.value);
      }

      // reset inputValue
      setInputValue('');
    }
  };

  // Remove item/value from inserted field values
  const removeItem = (key) => {
    field.value.splice(key, 1);
    form.setFieldValue(field.name, field.value);
  };

  return (
    // Render editable field for multiple values depending on type of metadata field
    editMode ? (
      <>
        {fieldInfo.type === 'mixed_text' && !!fieldInfo.collection && (
          <EditMultiSelect
            collection={fieldInfo.collection}
            field={field}
            setEditMode={setEditMode}
            inputValue={inputValue}
            removeItem={removeItem}
            handleChange={handleChange}
            handleKeyDown={handleKeyDown}
          />
        )}
      </>
    ) : (
      <ShowValue setEditMode={setEditMode} field={field} form={form} />
    )
  );
};

// Renders multi select
const EditMultiSelect = ({ collection, setEditMode, handleKeyDown, handleChange, inputValue, removeItem, field }) => {
  const { t } = useTranslation();

  return (
    <>
      <div ref={childRef}>
        <div onBlur={() => setEditMode(false)}>
          <input
            type="text"
            name={field.name}
            value={inputValue}
            onKeyDown={(e) => handleKeyDown(e)}
            onChange={(e) => handleChange(e)}
            placeholder={t('EDITABLE.MULTI.PLACEHOLDER')}
            list="data-list"
          />
          {/* Display possible options for values as dropdown */}
          <datalist id="data-list">
            {collection.map((item, key) => (
              <option key={key}>{item.value}</option>
            ))}
          </datalist>
        </div>
        {/* Render blue label for all values already in field array */}
        {field.value instanceof Array &&
          field.value.length !== 0 &&
          field.value.map((item, key) => (
            <span className="ng-multi-value" key={key}>
              {item}
              <a onClick={() => removeItem(key)}>
                <i className="fa fa-times" />
              </a>
            </span>
          ))}
      </div>
    </>
  );
};

// Shows the values of the array in non-edit mode
const ShowValue = ({ setEditMode, form: { initialValues }, field }) => {
  return (
    <div onClick={() => setEditMode(true)}>
      {field.value instanceof Array && field.value.length !== 0 ? (
        <ul>
          {field.value.map((item, key) => (
            <li key={key}>
              <span>{item}</span>
            </li>
          ))}
        </ul>
      ) : (
        <span className="editable preserve-newlines">{''}</span>
      )}
      <i className="edit fa fa-pencil-square" />

      <i className={cn('saved fa fa-check', { active: initialValues[field.name] !== field.value })} />
    </div>
  );
};

export default RenderMultiField;

This is initialValues before a change and after: InitialValues before change InitialValues after change

This is the state of MetadataFields and FetchedValues before and after change: State before change State after change

0 Answers
Related