如何在没有无限渲染循环的情况下调用 setter 函数的子项使用具有数组状态的 useState 钩子?



我有一个状态为数组的父组件。它映射数组并将项传递给子组件。

import React, { useState, useEffect } from "react";
import { Channel } from "../../../constants";
import { CommandLineArguments } from "../../../main/ipcHandlers";
import { Conversion, Converter } from "../Converter/Converter";
export function App() {
const [commandLineArguments, setCommandLineArguments] = useState<null | CommandLineArguments>(null);
const [conversions, setConversions] = useState<Conversion[]>([]);
function setConversion(filepath: string, partial: Partial<Conversion>) {
const updated = conversions
.filter((conversion) => conversion.filepath === filepath)
.map((conversion) => ({ ...conversion, ...partial }));
const rest = conversions.filter((conversion) => conversion.filepath !== filepath);
setConversions([...rest, ...updated]);
}
useEffect(function getCommandLineArgumentsEffect() {
async function asyncReadSvgFile() {
const args = await window.bridgeToMainProcessApi.invoke(Channel.GetCommandLineArguments);
const s = (args.input || []).map((path) => {
return { filepath: path };
});
setConversions(s);
}
asyncReadSvgFile();
}, []);
return (
<div>
{conversions.map((c) => (
<Converter
proxy=""
setConversion={setConversion}
key={c.filepath}
filepath={c.filepath}
svg={c.svg}
processedSvg={c.processedSvg}
tgml={c.tgml}
/>
))}
</div>
);
}

子项调用回调以更新转换。

import React, { useEffect } from "react";
import compose from "lodash/fp/compose";
import { XmlView, XmlType, ViewType } from "../XmlView";
import { Channel, defaultProxy } from "../../../constants";
import { prepareSvg, convertSvg } from "../../../process";
import { filenameWithoutExtension, filenameFromPath } from "../App/files";
export type Conversion = {
filepath: string;
svg?: string;
processedSvg?: string;
tgml?: string;
};
type Props = Conversion & {
proxy: string;
setConversion(filepath: string, conversion: Partial<Conversion>): void;
};
export function Converter(props: Props) {
const { filepath, svg, processedSvg, tgml, proxy, setConversion } = props;
useEffect(
function readSvgFileEffect() {
console.log("read1");
async function asyncReadSvgFile() {
console.log("read2");
const files = await window.bridgeToMainProcessApi.invoke(Channel.ReadFiles, [filepath]);
const svg = files[0].content;
setConversion(filepath, { svg });
}
asyncReadSvgFile();
},
[filepath]
);
useEffect(
function prepareSvgEffect() {
async function asyncprepareSvg() {
if (!svg) {
return;
}
const processedSvg = await prepareSvg(svg, defaultProxy ? defaultProxy : proxy);
setConversion(filepath, { processedSvg });
}
asyncprepareSvg();
},
[svg]
);
useEffect(
function convertSvgEffect() {
async function asyncConvertSvg() {
if (!processedSvg) {
return;
}
const tgml = await convertSvg(processedSvg, compose(filenameWithoutExtension, filenameFromPath)(filepath));
setConversion(filepath, { tgml });
}
asyncConvertSvg();
},
[processedSvg]
);
return (
<div>
{svg && <XmlView serialized={svg} xmlType={XmlType.Svg} viewType={ViewType.Image} />}
{processedSvg && <XmlView serialized={processedSvg} xmlType={XmlType.ProcessedSvg} viewType={ViewType.Image} />}
{tgml && <XmlView serialized={tgml} xmlType={XmlType.Tgml} viewType={ViewType.Image} />}
{svg && <XmlView serialized={svg} xmlType={XmlType.Svg} viewType={ViewType.Code} />}
{processedSvg && <XmlView serialized={processedSvg} xmlType={XmlType.ProcessedSvg} viewType={ViewType.Code} />}
{tgml && <XmlView serialized={tgml} xmlType={XmlType.Tgml} viewType={ViewType.Code} />}
</div>
);
}

我不明白为什么这会导致无限渲染循环。我知道调用setConversions会导致父级重新渲染并将新道具传递给孩子。我想这可能会导致所有的孩子从头开始重新创建。随意对正在发生的事情提供更好的解释。

无论如何,我的主要问题是:如何绕过无限重新渲染?

我试图重现错误,但无法重现。即使在异步后重新排序转换也不会无限地重新渲染,而是以随机顺序进行转换。

我更改了您的一些代码以进行优化,例如不随机化转换并使转换成为纯组件,因为它会在其他转换更改时呈现,这将使它呈现比更大的转换数组获得更多倍(也许直到它出错但没有尝试(。

评论是我进行更改的地方。

const later = (value) =>
new Promise((resolve) =>
setTimeout(() => resolve(value), Math.random() * 100)
);
//using memo so it will only re render if props change
const Converter = React.memo(function Converter(props) {
const {
filepath,
setConversion,
svg,
processedSvg,
} = props;
React.useEffect(
function readSvgFileEffect() {
later({ svg: { val: 'svg' } }).then((resolve) =>
setConversion(filepath, resolve)
);
},
//added dependencies
[filepath, setConversion]
);
React.useEffect(
function prepareSvgEffect() {
if (!svg) {
return;
}
later({
processedSvg: { val: 'processed' },
}).then((resolve) =>
setConversion(filepath, resolve)
);
},
//added dependencies
[filepath, setConversion, svg]
);
React.useEffect(
function convertSvgEffect() {
if (!processedSvg) {
return;
}
later({
tgml: { val: 'tgml' },
}).then((resolve) =>
setConversion(filepath, resolve)
);
},
//added dependencies
[filepath, processedSvg, setConversion]
);
return <pre>{JSON.stringify(props, null, 2)}</pre>;
});
function App() {
const [conversions, setConversions] = React.useState([]);
React.useEffect(function getCommandLineArgumentsEffect() {
later().then(() =>
setConversions([
{ filepath: '1' },
{ filepath: '2' },
{ filepath: '3' },
])
);
}, []);
//use useCallback so setConversion doesn't change
const setConversion = React.useCallback(
function setConversion(filepath, partial) {
//pass callback to set state function so conversions
//  is not a dependency of useCallback
setConversions((conversions) =>
//do not re order conversions
conversions.map((conversion) =>
conversion.filepath === filepath
? {
...conversion,
...partial,
}
: conversion
)
);
},
[]
);
return (
<div>
{conversions.map((c) => (
<Converter
setConversion={setConversion}
key={c.filepath}
filepath={c.filepath}
svg={c.svg}
processedSvg={c.processedSvg}
tgml={c.tgml}
/>
))}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

<div id="root"></div>

最新更新