ReactJs: Rendering Similar React Components from a list without using same states for each of the element

Viewed 72

I have a CustomReactComponent class which holds a list of similar React Components, I then render a single element Component from the list based on some condition. This Component has <input/> tag. When I try to render it all the <input/> tags have the same value when User changes only one of them.
How can I prevent this from happening? and have the inputs store differnt values for each instance

Minimal Reproducible Code:

import {Component} from "react";

export default class CustomReactComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {selected: 0};
        this.list = [<div><input/></div>, <div><input/></div>, <div><input/></div>, <div><input/></div>];
    }

    onSelect = (e) => {
        this.setState({selected: e.target.value});
    }

    render() {
        let Component = this.list[this.state.selected];
        return (
            <div>
                <select onChange={this.onSelect}>
                    <option value="0">0</option>
                    <option value="1">1</option>
                    <option value="2">2</option>
                    <option value="3">3</option>
                </select>
                <div>
                    State:{this.state.selected}: 
                    {Component}
                </div>
            </div>
        );
    }
}



Edit 1:
As @asynts pointed out we could use key for each component, but keys's check for siblings during rendering, and since here i am rendering only a single component, key would be of no use, it re-initializes the component every time the state selected changes. But this is a start, it atleast dosent treat all the components in this.list as the same thing

Edit 2:
After some tinkering and experimentation I found a work around for this, we can have a Wrap Component which is given a unique id uid as a prop this can then be used during rendering if state selected points to uid of a Wrap we style it to display: initial else to display: none, this is where key's comes handy we can always render all the siblings and give it a key same as uid

Code:

import {Component} from "react";

function Wrap({uid}) {
    return <div>uid = {uid} : <input/></div>
}

export default class CustomReactComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {selected: 0};
        this.list = [
            <Wrap uid={0}/>,
            <Wrap uid={1}/>,
            <Wrap uid={2}/>,
            <Wrap uid={3}/>
        ];
    }

    onSelect = (e) => {
        this.setState({selected: parseInt(e.target.value)});
    }

    render() {
        return (
            <div>
                State:
                <select onChange={this.onSelect}>
                    <option value="0">0</option>
                    <option value="1">1</option>
                    <option value="2">2</option>
                    <option value="3">3</option>
                </select>
                <div>
                    {this.list.map((component) =>
                        <div key={component.props.uid}
                             style={{display: component.props.uid !== this.state.selected ? 'none' : 'initial'}}>
                            {component}
                        </div>)}
                </div>
            </div>
        );
    }
}
2 Answers

This is actually a much more interesting problem than I originally thought. It seems that React simply can't tell that you are rendering a different element.

Ultimately, the JSX syntax is just syntactic sugar for React.createElement which returns an object:

// jsx:
<input />

// js:
React.createElement("input");

// result object:
{
    type: "input",

    // This is the default.
    key: null,

    // There is some other stuff in here too.
}

In other words, all the entries in your array are identical as far as React is concerned.


You can resolve this problem by specifying a key attribute in each array entry:

this.list = [
    <div key="1"><input/></div>,
    <div key="2"><input/></div>,
    <div key="3"><input/></div>,
    <div key="4"><input/></div>,
];

There is some documentation about this but it's a bit vague.

Here is an in-depth explanation about why keys are necessary if you’re interested in learning more.

TL;DR: React will create a tree of these objects I mentioned and then computes the difference to the previous tree and updates the DOM. In your case, it is not able to find a difference and therefore needs the key attribute to help.

We could use key for each component, but key's check for siblings during rendering, and here only a single component is being rendered, thus making key not so useful, it re-initializes the component every time the state selected changes.
But we can have a Wrap Component as a work around, Wrap is given a unique id uid as a prop this can then be used during rendering if state selected points to uid of a Wrap we style it to display: initial else to display: none, this is where key's comes handy we can always render all the siblings and give it a key same as uid

Code:

import {Component} from "react";

function Wrap({uid}) {
    return <div>uid = {uid} : <input/></div>
}

export default class CustomReactComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {selected: 0};
        this.list = [
            <Wrap uid={0}/>,
            <Wrap uid={1}/>,
            <Wrap uid={2}/>,
            <Wrap uid={3}/>
        ];
    }

    onSelect = (e) => {
        this.setState({selected: parseInt(e.target.value)});
    }

    render() {
        return (
            <div>
                State:
                <select onChange={this.onSelect}>
                    <option value="0">0</option>
                    <option value="1">1</option>
                    <option value="2">2</option>
                    <option value="3">3</option>
                </select>
                <div>
                    {this.list.map((component) =>
                        <div key={component.props.uid}
                             style={{display: component.props.uid !== this.state.selected ? 'none' : 'initial'}}>
                            {component}
                        </div>)}
                </div>
            </div>
        );
    }
}
Related