Reputation: 66
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 ImageItem
s).
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
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
Reputation: 1164
I would propose 2 things:
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 : "",
},
},
}
}
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