0

I have a parent component that has an array in state. It maps over the array and passes items to child components.

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>
  );
}

The children invoke the callback to update the conversions.

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>
  );
}

I don't understand why this causes an infinite rendering loop. I understand that calling setConversions causes the parent to re-render and pass new props to the children. I guess that might cause all the children to be recreated from scratch. Feel free to provide a better explanation of what is happening.

Regardless, my main question is: how do I get around the infinite re-rendering?

Dominik
  • 6,078
  • 8
  • 37
  • 61
user1283776
  • 19,640
  • 49
  • 136
  • 276
  • I see in your `App` component that you call `setConversions` in `useEffect` without a condition, so it does put you in an infinite loop. You should add a boolean variable in your state which tells you if you called setConversions yet or not, and add this boolean to the second parameter of `useEffect`. – 4br3mm0rd Jun 29 '20 at 20:52
  • If `conversions` has more than one item then each item will try and re order it in a different order, this causes an infinite loop. – HMR Jun 29 '20 at 20:59
  • @HMR: That doesn't seem to be the cause. Even if I change the code to avoid the reordering, the problem persists. Thanks for the suggestion though. setConversion that doesn't reorder: function setConversion(filepath: string, partial: Partial) { const updated = conversions.map((conversion) => (conversion.filepath === filepath ? { ...conversion, ...partial } : conversion)); setConversions(updated); } – user1283776 Jun 30 '20 at 06:17
  • @4br3mm0rd: I do use a condition in useEffect, see the empty array passed as second argument https://stackoverflow.com/a/53121021/1283776 – user1283776 Jun 30 '20 at 06:20

1 Answers1

1

I tried to reproduce the error but was not able to. Even re ordering the conversions after async did not infinitely re render but did put the conversions in random order.

I changed some of your code to optimize like not randomizing conversions and making Conversion a pure component because it will render whenever other conversions change which will make it render more times the larger conversions array get (maybe up to the point where it errors but didn't try).

The comments are where I made the changes.

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>
HMR
  • 37,593
  • 24
  • 91
  • 160
  • I abandoned my OP approach and am currently trying to solve the problem using useReducer insteead. It seems recommended for state that can have multiple sub-values, which fits my use case. Thank you regardless! I learned from reading your answer. – user1283776 Jun 30 '20 at 08:44
  • 1
    @user1283776 I don't think the shape of the state warrants the use of a reducer and the complexity of your requirements comes from multiple asynchronous individual updates. Hope you can solve these issues. – HMR Jun 30 '20 at 09:08