3
votes

I am using the react-hook-form library to create a multi-step dynamic large form (over 70 fields). Most of my form fields are dynamic (user can add/delete more fields by clicking + button). In order to achieve this, I use react-hooks and add a hook for each dynamic field. But I feel like my code is not abstract enough and there might be a smarter way to do this. Below there is an example screenshot of my form:

enter image description here

Respective code for the hooks and form fields are here:

         // hooks
         const [sizes, setSizes] = React.useState([0]);
         const [formats, setFormats] = React.useState([0]);

         const [counter, setCounter] = React.useState(1);

         // buttons
         const addSizesFieldset = () => {
         setSizes(prevIndexes => [...prevIndexes, counter]);
         setCounter(prevCounter => prevCounter + 1);
         };

         const removeSizesFieldset = index => () => {
         setSizes(prevIndexes => [
         ...prevIndexes.filter(item => item !== index)
         ]);
         setCounter(prevCounter => prevCounter - 1);
         };


         const addFormatsFieldset = () => {
         setFormats(prevIndexes => [...prevIndexes, counter]);
         setCounter(prevCounter => prevCounter + 1);
         };

         const removeFormatsFieldset = index => () => {
         setFormats(prevIndexes => [
         ...prevIndexes.filter(item => item !== index)
         ]);
         setCounter(prevCounter => prevCounter - 1);
         };


        // form field arrays
        {sizes.map(index => {
            const fieldName = `sizes[${index}]`;
            return (
                <fieldset name={fieldName} key={fieldName} >
                    <h4>Sizes:</h4>
                    <label>
                        Size:
                        <input
                            type="text"
                            placeholder="Input"
                            name={`${fieldName}.size`}
                            ref={register}

                        />
                    </label>

                    <button type="button" onClick={addSizesFieldset}>
                        +
                    </button>
                    <button type="button" onClick={removeSizesFieldset(index)}>
                        -
                    </button>
                </fieldset>
            );
        })}
            {formats.map(index => {
                const fieldName = `formats[${index}]`;
                return (
                    <fieldset name={fieldName} key={fieldName} >
                        <h4>Formats:</h4>
                        <label>
                            Format:
                            <input
                                type="text"
                                placeholder="Input"
                                name={`${fieldName}.format`}
                                ref={register}

                            />
                        </label>

                        <button type="button" onClick={addFormatsFieldset}>
                            +
                        </button>
                        <button type="button" onClick={removeFormatsFieldset(index)}>
                            -
                        </button>
                    </fieldset>
                );
            })} ´´

Imagine you are doing these hook instants and const add/remove...Fieldset functions for seventy fields. It seemed too much duplication to me but I couldn't find any solution or a new idea other than "React can handle multiple hooks so no worries" comments.

If you want to try your ideas on codesandbox, I prepared below code as well:

https://codesandbox.io/s/react-hook-form-wizard-form-yzno5

2

2 Answers

3
votes

Maybe if you use useReducer, it would be better if you have multiple states

Exemple

const initialState = {
  counter: 1,
  sizes: [0],
  formats: [0],
};

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return {
        ...state,
        counter: state.counter + 1,
        [action.typeObject]: [...state.sizes, state.counter + 1]
      };
    case 'remove':
      return {
        ...state,
        counter: state.counter - 1,
        [action.typeObject]: [...state.sizes.filter(item => item !== action.index)]
      };
    default:
      return state;
  }
}

/***/

const [stateReducer, dispatch] = React.useReducer(reducer, initialState);

/***/

const addSizesFieldset = () => {
  dispatch({ type: 'add', typeObject: 'sizes' })
};

const removeSizesFieldset = index => () => {
  dispatch({ type: 'remove', typeObject: 'sizes', index })
};

const addFormatsFieldset = () => {
 dispatch({ type: 'add', typeObject: 'formats' })
};

const removeFormatsFieldset = index => () => {
  dispatch({ type: 'remove', typeObject: 'formats', index })
};

Test it

Here is my simple implementation by forking your code: https://codesandbox.io/s/react-hook-form-wizard-form-1h4yq


Edit:

You can even remove 2 functions by adding "typeObject" in param:

const addFieldset = field => () => {
  dispatch({ type: 'add', typeObject: field })
};

const removeFieldset = (index, field) => () => {
  dispatch({ type: 'remove', typeObject: field, index })
};

2
votes

have you consider to use our new custom hook useFieldArray? maybe it's an alternative solution for those dynamic fields. I haven't tested with dynamic append, it's worth to give it a try tho. feedback welcome.

you can follow up questions in our spectrum channel as well: https://spectrum.chat/react-hook-form?tab=posts

https://react-hook-form.com/api#useFieldArray

function App() {
  const { register, control, handleSubmit } = useForm({
    // defaultValues: {}; you can populate the fields by this attribute 
  });
  const { fields, append, prepend, remove } = useFieldArray({ control, name: "test" });

  return (
    <form onSubmit={handleSubmit(data => console.log("data", data))}>
      <ul>
        {fields.map((item, index) => (
          <li key={item.id}>
            {/* for empty validation register make sure pass empty object  */}
            <input name={`test[${index}].name`} ref={register({})} /> 
            <button onClick={() => remove(index)}>Delete</button>
          </li>
        )}
      </ul>
      <section>
        <button type="button" onClick={() => append({ name: "test" })} >
          append
        </button>
        <button type="button" onClick={() => prepend({ name: "test1" })}>
          prepend
        </button>
    </form>
  );
}