英文:
How to handle (bind to state) an array of inputs?
问题
我有一个在我的React应用程序中的表单。表单本身有一些字段,但也允许用户上传图片并对其进行评论。该表单会呈现已上传图片的列表(预览)以及每个图片的评论输入字段(ImageList
组件,它呈现多个 ImageItem
)。
我将已上传图片的数据存储在Redux(使用Toolkit)存储中(files
)。
files
是一个 IFile
数组:
interface IFile {
file: { name: string; url: string; size: number };
dto: { comment?: string };
}
组件大致如下:
// CreateForm.tsx
const { files } = useSelector((state: RootState) => state.createSpot);
return (
<form>
{/* 其他输入 */}
<ImageList files={files}/>
</form>
)
// ImageList.tsx
return (
<div>
{files.map((file, i) => (
<ImageItem
key={file.file.url}
file={file}
index={i}
/>
))}
</div>
)
ImageItem
只是一个 img
和一个文本 input
。
提交表单时,我还会提交图片和相应的评论。我希望以某种方式存储这些评论,或者至少在提交时接收它们并与表单一起提交。
我尝试将每个文件的输入明确绑定到Redux存储。我创建了一个reducer来通过其唯一的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;
},
我的 ImageItem
如下所示:
// 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,
})
);
}}
/>
</>
);
};
虽然它似乎按预期工作,但显然在每次键入字符时触发这样的操作是非常昂贵的。
所以有没有一种优雅和优化的方式来解决这个问题?我感觉我可能漏掉了一些明显的东西。
提前感谢您。
英文:
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.
答案1
得分: 1
我提议两件事:
- 规范化 redux 状态(参见 https://redux.js.org/usage/structuring-reducers/normalizing-state-shape#designing-a-normalized-state)以获得类似以下的结构:
{
images : {
byId : {
"image1" : {
id : "image1",
name : "",
url : "",
size: "",
comments : ["comment1", "comment2"]
},
}
},
comments : {
byId : {
"comment1" : {
id : "comment1",
comment : "",
},
"comment2" : {
id : "comment2",
comment : "",
},
},
}
}
- 当用户输入评论文本时,对 dispatch 进行防抖处理。
附言:如果每张图片只有一个评论,可能可以在状态中省去单独的 comments 部分。
英文:
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
答案2
得分: 1
你可能在寻找的解决方案是对textarea
元素的onChange
处理程序进行去抖,或者更准确地说,对dispatch
函数进行去抖,以便不会为每个单独的更改触发一个操作。
如果您不想从lodash或类似的库中导入去抖实用程序,这里是一个我使用的简单的去抖高阶函数。
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(fn, delay, ...args);
}
};
使用debounce
高阶函数装饰dispatch
函数。我还建议在第一次调度之前设置最小字符计数,例如至少3个字符。
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}
/>
</>
);
};
英文:
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}
/>
</>
);
};
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论