skip to Main Content

I have a JSON object which is deeply nested. I want to display input fields with respect to the JSON values. To traverse through the deeply nested JSON object I used recursion and collected all processed input fields to set it into a state to display in the main return of the module. However, it is throwing Too many re-renders to infinite loop. Uncaught run time errors.

I have tried various methods such as,

  1. Storing displayFrom in displayProperties in a separate const and used that const into setDisplayForm.

  2. Applied various conditions in the main return of App module such as typeof properties[ key ] === 'object' in the conditional rendering

  3. Used useMemo to wrap the displayProperties function from causing too many re-render issue.

I have implemented the same in stackblitz please take a look at my code and comment. https://stackblitz.com/edit/vitejs-vite-kjyz2y?file=src%2FApp.jsx&terminal=dev

There is an const input it makes the code looks bigger and complex but it is just a JSON nested object. The actual logic and code looks like below,

import { useState } from 'react';
import reactLogo from './assets/react.svg';
import viteLogo from '/vite.svg';
import './App.css';

function App() {
const [displayFrom, setDisplayForm] = useState();

const input = {} // a deeply nested JSON object

const displayProperties = (properties) => {
return (
  <>
    {Object.keys(properties).map((key, index) => (
      <div key={index}>
        <span>{key === 'label' ? properties[key] : null}</span>
        <span>
          {JSON.stringify(properties[key]).includes('properties') ||
          JSON.stringify(properties[key]).includes('label')
            ? displayProperties(properties[key])
            : null}
        </span>
      </div>
    ))}
  </>
);
};
const builtForm = () => displayProperties(input);
setDisplayForm(builtForm);

return <>{displayFrom}</>;
}

export default App;

2

Answers


  1. The problem is likely due to the fact that you can’t use an index as a key value.

    Try something like this:

    import { useState } from 'react';
    import reactLogo from './assets/react.svg';
    import viteLogo from '/vite.svg';
    import './App.css';
    
    function App() {
      const [displayFrom, setDisplayForm] = useState();
    
      const input = {}
    
      let id = 1;
    
      const displayProperties = (properties) => {
        return (
          <>
            {Object.keys(properties).map((key) => {
              id++;
              return (
                <div key={id}>
                  <span>{key === 'label' ? properties[key] : null}</span>
                  <span>{JSON.stringify(properties[key]).includes('properties') || JSON.stringify(properties[key]).includes('label') ? displayProperties(properties[key]) : null}</span>
                </div>
              );
            })}
          </>
        );
      };
    
      return <>{displayProperties(input)}</>;
    }
    
    export default App;
    
    Login or Signup to reply.
  2. Here is a preview of the app we will make. The user can supply any arbitrary object as a starting point. As the user changes values in the form, the deeply nested object is updated –

    preview
    preview

    an isomorphism first

    In another Q&A we go through the process of flattening and unflattening any nested object using a fromJS and toJS interface. The details of that process will not be covered in this post. We start by flattening the object –

    {
      users: [
        { name: "Alice", roles: ["a", "b"], phone: { mobile: 123 } },
        { name: "Bob", roles: [], phone: { home: 456, mobile: 789 } }
      ]
    }
    
    const flat = fromJS(example) // ✅
    
    [
      ["users", 0, "name", "Alice"],
      ["users", 0, "roles", 0, "a"],
      ["users", 0, "roles", 1, "b"],
      ["users", 0, "phone", "mobile", 123],
      ["users", 1, "name", "Bob"],
      ["users", 1, "phone", "home", 456],
      ["users", 1, "phone", "mobile", 789]
    ]
    

    And toJS can reverse the operation –

    const newExample = toJS(flat) // ✅
    
    {
      users: [
        { name: "Alice", roles: ["a", "b"], phone: { mobile: 123 } },
        { name: "Bob", roles: [], phone: { home: 456, mobile: 789 } }
      ]
    }
    

    react component

    Now we make a React Form component to operate on the flat state. Run the code example below and watch the nested object update –

    const example = {
      users: [
        { name: "Alice", roles: ["a", "b"], phone: { mobile: 123 } },
        { name: "Bob", roles: [], phone: { home: 456, mobile: 789 } }
      ]
    }
    
    function Form({ data }) {
      const [state, setState] = React.useState(() => fromJS(data))
      const updateState = index => event => {
        setState(prev => updateArr(prev, index, record =>
          [...record.slice(0, -1), event.target.value]
        ))
      }
      return <div>
        <pre>{JSON.stringify(toJS(state), null, 2)}</pre>
        <form>
          {state.map((record, index) => {
            const key = record.slice(0, -1).join(".")
            return <label key={key}>
              {key}
              <input value={record.at(-1)} onChange={updateState(index)} />
            </label>
          })} 
        </form>
      </div>
    }
    
    // dependencies
    function updateArr(arr, index, func) {
      return [
        ...arr.slice(0, index),
        func(arr[index]),
        ...arr.slice(index + 1),
      ]
    }
    
    // https://stackoverflow.com/a/65129261/633183
    function fromJS(v, r = []){
      switch (v && v.constructor) {
        case Object:
          return Object.entries(v)
            .flatMap(([ k, v ]) => fromJS(v, [ ...r, k ]))
        case Array:
          return v.flatMap((v, k) => fromJS(v, [ ...r, k ]))
        default:
          return [[ ...r, v ]]
      }
    }
    
    function toJS(t){
      return t.reduce((r, v) => merge(r, toJS1(v)), {})
    }
    
    function toJS1([ v, ...more ]) {
      if (more.length)
        return set(
          Number.isInteger(v) ? [] : {},
          [ v, toJS1(more) ]
        )
      else
        return v
    }
    
    function merge(l = {}, r = {}) {
      return Object
        .entries(r)
        .map(([ k, v ]) => isObject(v) && isObject(l[k]) ? [ k, merge(l[k], v) ] : [ k, v ])
        .reduce(set, l)
    }
    
    const isObject = t =>  Object(t) === t
    
    const set = (t, [ k, v ]) => (t[k] = v, t)
    
    // run
    ReactDOM.createRoot(document.querySelector("#app")).render(<Form data={example} />)
    div { display: flex; flex-direction: row; align-items: start;}
    label { display: block; width: 300px; }
    input { float: right; width: 100px; }
    label ~ label { margin-top: 0.5rem; }
    form { background: #ffc; padding: 1rem; }
    pre { width: 250px; }
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <div id="app"></div>

    remarks

    In ~20 lines of React, we made our first Formik clone. You can see how the Form component could be extended to render a more sophisticated HTML representation of the nested values, or to allow for the addition/removal of new fields. Once the input object is represented as flat state, the challenges of working with it in React are greatly reduced.

    To make the demo run online, all dependencies are included in this small code paste. Separate the generic functions into their respective modules to keep the React component focused on its task.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search