79354815

Date: 2025-01-14 11:14:29
Score: 0.5
Natty:
Report link

Short answer

As we know, the key is with respect to the container. Therefore even if there is no change in keys, a change in container will lead to recreating the same component.

Detailed answer

The above point puts the emphasis on the container. On the other side, a recursive rendering as the below code does, has a significant impact on its resulting containers.

export default function Group({ group }: Props) {
   ...
   else return <Group key={ele.id} group={ele} />
   ...
   console.log(elements);
   return <div>{elements}</div>;
}

The console log in the code will help us to know that with the given data, this component is rendered twice with two separate data. It means the below single declaration rendered in two separate times. It is true since there is a recursive call for the nested group with id 2 in addition to the initial call for the id 0.

<Group group={group} key="0" />

Let us view the console log generated:

// Array 1
// 0:{$$typeof: Symbol(react.element), key: '1', ...
// 1:{$$typeof: Symbol(react.element), key: '2', ...
// 2:{$$typeof: Symbol(react.element), key: '5', ...

// Array 2
// 0:{$$typeof: Symbol(react.element), key: '3' ...
// 1:{$$typeof: Symbol(react.element), key: '4' ...

Observation

These are the two distinct arrays React has created for us while rendering the component. In this case, the two arrays containing the items 1,2,5 and items 3,4 respectively.

Whenever there is a change in the data resulting a change in the containing array, react will remove the component from the container it has been changed from, and will add the component to the container it has been changed to. This is the reason for the issue we have been facing in this post while moving an object from one group to another.

Coming back to the point again, we face this issue since internally there are separate arrays for each nested group.

One possible solution

One solution may be to render in a way that it does not produce separate containers with respect to each group. However, this approach will necessitate a review on the recursive render. We need to find a way to render it so that the entire items contained in a single array, so that we can move items as we will. And react will not either remove or add components.

The following sample code demoes two things:

a. The existing render and the issue we now face.

b. Render without recursion, so that the issue may be addressed.

App.js

import { useState } from 'react';

export default function App() {
  const [someData, setSomeData] = useState(getInitialData());

  return (
    <>
      Existing declaration
      <Group group={someData} key="0" />
      <br />
      proposed declaration List
      <br />
      <List obj={someData} key="00" />
      <br />
      <button
        onClick={() => {
          setSomeData(moveTextFromGroup2toGroup0());
        }}
      >
        move text 3 from Group 2 to Group 0
      </button>
      <br />
      <button
        onClick={() => {
          setSomeData(moveTextWithinGroup2());
        }}
      >
        move text 3 withing Group 2
      </button>
      <br />
      <button
        onClick={() => {
          setSomeData(getInitialData());
        }}
      >
        Reset
      </button>
    </>
  );
}

function List({ obj }) {
  let items = [];
  let stack = [];

  stack.push(obj);

  while (stack.length) {
    const o = stack[0]; // o for object
    if (o.type === 'group') {
      // if group type, then push into stack
      // to process in the next iteration
      for (let i = 0; i < o.groups.length; i++) {
        stack.push({ ...o.groups[i], groupId: o.id });
      }
    } else {
      // if not group type, keep to render
      items.push(<A key={o.id} label={'Group ' + o.groupId + ':' + o.text} />);
    }
    stack.shift(); // remove the processed object
  }
  return items;
}

function Group({ group }) {
  const elements = group.groups.map((ele) => {
    if (ele.type === 'other')
      return <A key={ele.id} label={'Group ' + group.id + ':' + ele.text} />;
    else return <Group key={ele.id} group={ele} />;
  });
  console.log(elements);
  return <div>{elements}</div>;
}

function A({ label }) {
  const [SomeInput, setSomeInput] = useState('');
  return (
    <>
      <label>{label}</label>
      <input
        value={SomeInput}
        onChange={(e) => setSomeInput(e.target.value)}
      ></input>
      <br />
    </>
  );
}

function getInitialData() {
  return {
    id: 0,
    type: 'group',
    groups: [
      {
        id: 1,
        type: 'other',
        text: 'text 1',
      },
      {
        id: 2,
        type: 'group',
        groups: [
          {
            id: 3,
            type: 'other',
            text: 'text 3',
          },
          {
            id: 4,
            type: 'other',
            text: 'text 4',
          },
        ],
      },
      {
        id: 5,
        type: 'other',
        text: 'text 5',
      },
    ],
  };
}

function moveTextWithinGroup2() {
  return {
    id: 0,
    type: 'group',
    groups: [
      {
        id: 1,
        type: 'other',
        text: 'text 1',
      },
      {
        id: 2,
        type: 'group',
        groups: [
          {
            id: 4,
            type: 'other',
            text: 'text 3',
          },
          {
            id: 3,
            type: 'other',
            text: 'text 4',
          },
        ],
      },
      {
        id: 5,
        type: 'other',
        text: 'text 5',
      },
    ],
  };
}

function moveTextFromGroup2toGroup0() {
  return {
    id: 0,
    type: 'group',
    groups: [
      {
        id: 1,
        type: 'other',
        text: 'text 1',
      },
      {
        id: 3,
        type: 'other',
        text: 'text 3',
      },
      {
        id: 2,
        type: 'group',
        groups: [
          {
            id: 4,
            type: 'other',
            text: 'text 4',
          },
        ],
      },
      {
        id: 5,
        type: 'other',
        text: 'text 5',
      },
    ],
  };
}

Test run

On loading the app

enter image description here

Test to move the component Text 3 in Group 2 to Group 0 - using the recursive rendering

enter image description here

After clicking the button "move text 3 from Group 2 to Group 0".

enter image description here

Observation

The component has been moved from Group 2 to Group 0 as we can verify from the labels, however, the input has been lost. It means, React has removed the component from Group 2 and newly added it to Group 0.

We shall do the same test with rendering without recursion

enter image description here

After clicking the button "move text 3 from Group 2 to Group 0".

enter image description here

Observation

The component has been moved by retaining the input. It means, React has neither removed nor added it.

Therefore the point to take note may this:

Components with keys will retain retain states as long as its container is not changing.

Aside : The component without keys will also retain states as longs its position in the container is not changing.

Note:

The sole objective of the proposed solution is not to say do not use recursive rendering and use imperative way as the sample code does. The sole objective here is to make it clear that - Container has great significance in retaining states.

Citations:

Is it possible to traverse object in JavaScript in non-recursive way?

Option 2: Resetting state with a key

Reasons:
  • Blacklisted phrase (1): Is it possible to
  • RegEx Blacklisted phrase (1): help us
  • Long answer (-1):
  • Has code block (-0.5):
Posted by: WeDoTheBest4You