Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Do I *have to* flatten all hierarchical component declarations in React.JS?

I'm wondering what the canonical method is for creating a dynamic select component in React. Do I have to create a separate component that returns options as per the code below in order to be able to customize the value through props for each entry, and then include these into a separate select component?

class ListEntry extends React.Component {
  render = () => {
    return(
      <option value={this.props.ovalue}>{this.props.ovalue}</option>
    )
  }
}


class SomeList extends React.Component {
  genstuff = (n) => {
    var theEntries = [];
    for(var i = 0; i < n; i++) {
      theEntries.push(<ListEntry ovalue={Math.random().toString()} />)
    };
    return(theEntries)
  }

  render = () => {
    var entries = this.genstuff(10);
    return(
      <select>
        {entries}
        <ListEntry ovalue="a basic entry"/>
        <ListEntry ovalue="another"/>
      </select>
    )
  }
}

Is there another "accepted" way to do this within the SomeList component itself? If so, what are the advantages, disadvantages?

I have also used this technique (successfully) for svg elements so the above question applies too:

class SmallCircle extends React.Component {
  render = () => {
    return(
      <circle cx={this.props.scx} cy={this.props.scy} r="5" 
        stroke="blue" fill="dodgerblue" strokeWidth="2"/>)
  }
}


class SomeSvg extends React.Component {

  randomCoords = (n, domain) => {
    let xs = [...Array(n).keys()].map(i=> {
      return {
        "i": i,
        "x": Math.floor(Math.random() * domain + 1).toString(),
        "y": Math.floor(Math.random() * domain + 1).toString()
      }
    })
    return(xs)
  }

  render = () => {
    var svgcoords = this.randomCoords(5000, 700);
    var thecircles = svgcoords.map(xy => <SmallCircle key={xy.i.toString() + xy.x.toString() + xy.y.toString()} 
        scx={xy.x} scy={xy.y} />);
    return(
      <svg width="700" height="700">
        {thecircles}
        <circle cx="1" cy="1" r="100" stroke="orange" strokeWidth="7" fill="grey" />
        <circle cx="700" cy="700" r="100" stroke="green" strokeWidth="7" fill="grey" />
        <circle cx="1" cy="700" r="100" stroke="red" strokeWidth="7" fill="grey" />
        <circle cx="700" cy="1" r="100" stroke="blue" strokeWidth="7" fill="grey" />
        <SmallCircle scx="200" scy="200" />
      </svg>
    )
  }
}

So basically my question is, does React require you to "unwrap" (ie flatten) hierarchical component structures into multiple granular top-level components all the time?

Here is the output, btw:

enter image description here

EDIT: I do realise I haven't included a "key" for each select entry and it's complaining in the console, but the question isn't affected by this.

like image 497
Thomas Browne Avatar asked Aug 15 '17 13:08

Thomas Browne


People also ask

What is the correct way to update property of React state?

Approach 1: We can create a dummy object to perform operations on it (update properties that we want) then replace the component's state with the updated object. Approach 2: We can pass the old nested object using the spread operator and then override the particular properties of the nested object.

Which is the best place to declare state in a React component?

When the component class is created, the constructor is the first method called, so it's the right place to initialize everything – state included. The class instance has already been created in memory, so you can use this to set properties on it. This is the one place where it is acceptable to have this.

How do you avoid React componentDidMount being called multiple times?

How to avoid React componentDidMount being called multiple times. Are you seeing your React component calling the componentDidMount lifecycle multiple times? The answer to this problem is to avoid conditional rendering, and avoid adding the React prop key to the component.


2 Answers

No, React does not require you to do that. If you wanted to, you could put all your React code in 1 giant component.

So, why not do that? There's many reasons actually: readability, reusability, testability, maintainability, etc.

Meaning: you create seperate components when you need certain logic to stand on its own. In your question, you have ListEntry as a seperate component. Why? If you dont use that component anywhere else in your code, you can just put that logic inside SomeList if you wanted to.

I think the key to your question is this: you dont need to inherit from React.Component if you dont use any of its logic. Basic 'stateless components' can be written as a function:

function MyComponent(props) {
    (...some logic...)
    return <option>{ props.value }</option>;
}

Or, ES6 equivalent:

const MyComponent = props => { 
    (...some logic...)
    return <option>{ props.value }</option>;
}

or, if you dont need any logic in the rendering at all:

const MyComponent = props => <option>{ props.value }</option>;

So, you could just define this function in your components render (or in the function body for stateless components), like so:

class SomeList extends Component {
    render() {
        const ListEntry = props => <option>{ props.value }</option>;
        return (
            <select>
                { [...Array(10).keys()].map(x => (
                    <ListEntry key={x} value={Math.random()} />
                ))}
                <ListEntry value="basic" />
                <ListEntry value="another" />
            </select>
        );
    }
}

Here, the code for our ListEntry component is defined in the SomeList components' render. (disclaimer: I don't think render is the best place for this tho, it's just to illustrate the point). Though, I don't really see the point in doing so. Since ListEntry doesnt contain any logic, why not use <option> directly instead of ListEntry? It could be a simple as:

const SomeList = () => {
    return (
        <select>
            { [...Array(10).keys()].map(index => (
                <option key={index}>{ Math.random() }</option>
            ))}
            <option>Basic</option>
            <option>Another</option>
        </select>
    );
};

As for your SVG example: if you take the above and create functions for the components you need, it could be written like this:

const SomeSvg = () => {
    const SmallCircle = props => (
        <circle {...props} r="5" stroke="blue" fill="dodgerblue" strokeWidth="2" />
    );

    const BigCircle = props => (
        <circle {...props} r="100" strokeWidth="7" fill="grey" />
    );

    const randomCoords = (n, domain) => [...Array(n).keys()].map(() => ({
        x: Math.floor(Math.random() * domain + 1),
        y: Math.floor(Math.random() * domain + 1)
    }));

    return (
        <svg width="700" height="700">
            { randomCoords(5000, 700).map((xy, i) => <SmallCircle key={i} cx={xy.x} cy={xy.y} />) }
            <BigCircle cx="1" cy="1" stroke="orange" />
            <BigCircle cx="700" cy="700" stroke="green" />
            <BigCircle cx="1" cy="700" stroke="red" />
            <BigCircle cx="700" cy="1" stroke="blue" />
            <SmallCircle cx="200" cy="200" />
        </svg>
    );
};
like image 95
0xRm Avatar answered Oct 19 '22 18:10

0xRm


React doesn't have any opinion on how much granular your component structures should be. It might be one component with some props or multiple components.

So Select component can be something like follows.

class Select extends Component {
  render() {
    return (
      <select>
        <option value="">Select...</option>
        {this.props.options.map(option =>
          <option key={option.value} value={option.value}>
            {option.text}
          </option>
        )}
      </select>
    );
  }
}

You can simply provide a JavaScript array as option and it will work nicely.

var options = [
  { value: 'one', text: 'One' },
  { value: 'two', text: 'Two' }
];

<Select options={options} />

Now let's say we got a new requirement to highlight some options of one Select component of your application based on some criteria. We got few options here.

The simplest option would be writing another similar component with highlighting logic. But this will duplicate most of our code and make it hard to maintain.

Another option is to pass an additional prop and conditionally enable highlighting logic based on the prop. Again, this isn't scalable. When we get several new requirements like this it will be hard to maintain our component's code and understand the interface of it.

However, if we make our component based on multiple composable components it will be easy to customize your component based on different requirements. In this case, we can introduce a highlight prop to Option component and simply conditionally pass the prop based on the criteria.

class Option extends Component {
  render() {
    return (
      <option
        value={this.props.value}
        className={this.prop.highlight ? "highlight" : ""}
      >
        {this.props.text}
      </option>
    );
  }
}

class Select extends Component {
  render() {
    return (
      <select className="my-select">
        {this.props.children} // render children of Select tag
      </select>
    );
  }
}

class HighLightingSelect extends Component {
  render() {
    return (
      <Select>
        {this.props.options((option, index) =>
          <Option key={option.value} value={option.value} highlight={index % 2}>
            {option.text}
          </Option>
        )}
      </Select>
    );
  }
}

The idea here is it's totally up to you to decide how much granularity you want in your component structures. It can be one component or a dozen of components which need be composed together to get the expected result.

If you make it more granular, it will be easy to customize and reuse your code in different situations. Especially, React promote composition over inheritance for code reuse. So granularity is key to reusability in React.

But having multiple components will make it harder to understand and use your component in simple scenarios.

like image 21
Tharaka Wijebandara Avatar answered Oct 19 '22 19:10

Tharaka Wijebandara