lavrent
lavrent

Reputation: 66

How to handle (bind to state) an array of inputs?

I have a form in my React App. The form has some fields itself, but also allows the user to upload images and comment them. The form renders a list of uploaded images (previews) and an input field for comment for each of them (ImageList component, which renders multiple ImageItems). I store data for the uploaded images in a Redux (with Toolkit) store (files).

files is an array of IFile:

interface IFile {
  file: { name: string; url: string; size: number };
  dto: { comment?: string };
}

Components look roughly like this:

// CreateForm.tsx

const { files } = useSelector((state: RootState) => state.createSpot);

return (
  <form>
    {/* other inputs */}
    <ImageList files={files}/>
  </form>
)
// ImageList.tsx

return (
  <div>
    {files.map((file, i) => (
      <ImageItem
        key={file.file.url}
        file={file}
        index={i}
      />
    ))}
  </div>
)

ImageItem is just an <img/> and a text <input/>.

Submitting the form I also submit the images and corresponding comments. I want to somehow store those comments or, at least, receive them on submit and submit with the form.

I tried explicitly binding the inputs of each file to Redux store. I created a reducer to update a file's comment by its unique url:

// createSpot.reducer.ts

updateFileComment(
  state,
  action: PayloadAction<{ url: IFile["file"]["url"]; value: string }>
) {
  const file = state.files.find((f) => f.file.url === action.payload.url);
  if (file) file.dto.comment = action.payload.value;
},

My ImageItem looked like this:

// ImageItem.tsx
const ImageItem: React.FC<ImageItemProps> = ({ file }: { file: IFile }) => {
  const dispatch = useAppDispatch();

  return (
    <>
      <img src={file.file.url} alt={file.file.name} />
      <textarea
        placeholder="Comment"
        value={file.dto.comment}
        onChange={(e) => {
          dispatch(
            updateFileComment({
              url: file.file.url,
              value: e.target.value,
            })
          );
        }}
      />
    </>
  );
};

While it seems to work as intended, it is obiously very expensive to dispatch such action on each character typed.

So is there some elegant and optimized way around this issue? I feel I'm missing something plain.

Thanks in advance.

Upvotes: 2

Views: 62

Answers (2)

Drew Reese
Drew Reese

Reputation: 203466

The solution you are likely looking for is to debounce the textarea element's onChange handler, or rather, the dispatch function, so you are not dispatching an action for each individual change.

If you don't want to import a debouncing utility from lodash or similar library, here's a simple debouncing Higher Order Function I use.

const debounce = (fn, delay) => {
  let timerId;
  return (...args) => {
    clearTimeout(timerId);
    timerId = setTimeout(fn, delay, ...args);
  }
};

Decorate the dispatch function with the debounce HOF. I might also suggest a minimum character count before the first dispatch, e.g. at least 3 characters.

const ImageItem: React.FC<ImageItemProps> = ({ file }: { file: IFile }) => {
  const dispatch = useAppDispatch();

  const debouncedDispatch = debounce(dispatch, 500);

  const onChangeHandler = e => {
    const { value } = e.target;
    
    if (value.length >= 3) {
      debouncedDispatch(updateFileComment({
        url: file.file.url,
        value,
      }));
    }
  };

  return (
    <>
      <img src={file.file.url} alt={file.file.name} />
      <textarea
        placeholder="Comment"
        value={file.dto.comment}
        onChange={onChangeHandler}
      />
    </>
  );
};

Upvotes: 1

Andrey Smolko
Andrey Smolko

Reputation: 1164

I would propose 2 things:

  1. Normalize the redux state (see https://redux.js.org/usage/structuring-reducers/normalizing-state-shape#designing-a-normalized-state) to have something like:

    {
     images : {
         byId : {
             "image1" : {
                 id : "image1",
                 name : "",
                 url : "",
                 size: "",
                 comments : ["comment1", "comment2"]
             },
         }
     },
     comments : {
         byId : {
             "comment1" : {
                 id : "comment1",
                 comment : "",
             },
             "comment2" : {
                 id : "comment2",
                 comment : "",
             },
         },
     }
    }
    
  2. Debounce dispatch when a user inputs text of comment.

P.S. if you have just one comment per image that may be ok to make state without separate comments slice

Upvotes: 1

Related Questions