React & separating concerns/dumb vs smart components

I’m relative new to React and I am attempting to take a sloppily written component and make it more semantic by separating logic from the UI i.e. a dumb component and a smart component rather than both jumbled together.

Here is the sloppy component I’m working with:

import React, { Component } from 'react';
import './App.css';
import axios from "axios";

class CoinList extends Component {
  constructor(props) {
    super(props);

    this.state = {
      coinList: []
    };
  }



  componentDidMount() {
    axios.get(`https://min-api.cryptocompare.com/data/all/coinlist`)
    .then(res => {
      const coins = res.data;
      this.setState({ coinList: coins});
    });
  }



// Object.keys is used to map through the data. Can't map through the data without this because the data is not an array. Map can only be used on arrays.
render() {
  const data = this.state.coinList.Data;
  if (data == null) return null;

  return (
    <div className="App">
      {Object.keys(data).map((key) => (
        <div className="container">
          <table className="table table-striped">
          <thead>
            <tr>
              <th className="col-md-2">Coin</th>
              <th className="col-md-2">Symbol</th>
              <th className="col-md-2">Algorithm</th>
              <th className="col-md-2">#</th>
            </tr>
           </thead>
            <tbody>
              <tr>
                <td className="col-md-2">{data[key].CoinName}</td>
                <td className="col-md-2">{key}</td>
                <td className="col-md-2">{data[key].Algorithm}</td>
                <td className="col-md-2">{data[key].SortOrder}</td>
              </tr>
            </tbody>
          </table>
        </div>
      ))}
    </div>
  );
 }
}


export default CoinList;

I have attempted to separate concerns by taking this code and breaking it up into two separate components. Here is what I have thus far:

import React, { Component } from 'react';
import CoinListItem from './coinListItem.js';


export default class NewCoinList extends Component {

  renderList() {
    const data = this.state.coinList.Data;
    if (data == null) return null;

      return Object.keys(data).map((key) => (
          <CoinListItem
            key={key}
          />

    ))
  }


  render() {
    return (
      <table className='table table-striped'>
        <thead>
          <tr>
            <th className="col-md-2">Coin</th>
            <th className="col-md-2">Symbol</th>
            <th className="col-md-2">Algorithm</th>
            <th className="col-md-2">#</th>
          </tr>
        </thead>
        <tbody>
          {this.renderList()}
        </tbody>
      </table>
    )
  }


}

And this:

import React from 'react';


const CoinListItem = (props) => {
  const data = this.state.coinList.Data;
  if (data == null) return null;

  return (
    <tr>
      <td className="col-md-2">{props.data[key].CoinName}</td>
      <td className="col-md-2">{props.key}</td>
      <td className="col-md-2">{props.data[key].Algorithm}</td>
      <td className="col-md-2">{props.data[key].SortOrder}</td>
    </tr>

  );

};


export default CoinListItem;

I am getting an error in CoinListItem that says “Key is not defined”. Can anyone tell me what the problem is and how to properly separate this component into two components that are more semantic?

Correct me if I am wrong, however, isn’t key not supposed to be used for render data? I thought it’s specialization is for indexing virtual dom elements to help with re-renders for components that change?

Honestly I’m confused about this as well. I was trying to look at other projects I’ve worked along with in tutorials and emulate how they separate logic from UI in my project. I don’t fully understand it.

When you map through an array to make DOM elements, React requires you to give them a key. This is an invisible helper for it to know when and what to rerender. You are not meant to use that key inside the element, it is for React only. If you want to use it, you can also pass the same number in as another prop.

Here is an explanation.

1 Like

What does the data you get back look like? You’re saying coinList is an array, then you try to iterate through coinList.Data, which suggests it isn’t an array but an object with an array called Data as one of the fields.

Anyway, make the request, set the state in componentDidMount. You’ve got that. But then you need to pass that data as a prop to the child. Like

import React, {Component} from 'react';
import axios from "axios";

class CoinListWrapper extends Component {
  state = [];

  componentDidMount() {
    axios.get(`https://min-api.cryptocompare.com/data/all/coinlist`)
    .then(res => this.setState({ coinList: res.data });
  }

  render() {
    return (
      <div className="App">
        <table className="table table-striped">
          <thead>
            <tr>
              <th>Coin</th>
              <th>Symbol</th>
              <th>Algorithm</th>
              <th>#</th>
            </tr>
           </thead>
           <CoinList coins={ this.state.coinList } />
         </table>
      </div>
    )
  }
}
  
function CoinList({coins}) {
  return (
    <tbody>
      {coins.map((coin, i) => (
        <tr key={i}>
          <td>{coin.CoinName}</td>
          <td>{coin.NotSureWhatThisIsSupposedToBe}</td>
          <td>{coin.Algorithm}</td>
          <td>{coin.SortOrder}</td>
        </tr>
      ))}
    </tbody>
  );
}

It looks like this: https://min-api.cryptocompare.com/data/all/coinlist

I tried it your way and I’m still geting errors.

When you map through an array to make DOM elements, React requires you to give them a key. This is an invisible helper for it to know when and what to rerender. You are not meant to use that key inside the element, it is for React only. If you want to use it, you can also pass the same number in as another prop.

This is what I was trying to say. Key isn’t really a prop that you can use. It’s more metadata just for React to help with virtual dom changes.

1 Like

Ah, it must be the coin symbol. Yes?

Yeah.

Here’s what it looks like.

@Nicknyr — have you changed your CoinListItem component at all and what error messages are you getting (if any)? I noticed that the component in question isn’t quite right (see comments below):

const CoinListItem = (props) => {
  const data = this.state.coinList.Data;
  // This component doesn't have its own state, this would actually, if
  // I'm not mistaken, throw an error and stops things from being rendered
  if (data == null) return null;

  return (
    <tr>
      <td className="col-md-2">{props.data[key].CoinName}</td>
      <td className="col-md-2">{props.key}</td>
      <td className="col-md-2">{props.data[key].Algorithm}</td>
      <td className="col-md-2">{props.data[key].SortOrder}</td>
    </tr>

  );

};


export default CoinListItem;

A better way to structure this (in my opinion) is to simply pass in the data for each coin into CoinListItem:

  renderList() {
    const data = this.state.coinList.Data;

    if (data == null) return null;

    return Object.keys(data).map((symbol) => (
      <CoinListItem key={symbol} data={data[symbol]} />
    ));
  }

And return the corresponding JSX for the table row in your CoinListItem component:

const CoinListItem = ({data}) => {
  return (
    <tr>
      <td className="col-md-2">{data.CoinName}</td>
      <td className="col-md-2">{data.Symbol}</td>
      <td className="col-md-2">{data.Algorithm}</td>
      <td className="col-md-2">{data.SortOrder}</td>
    </tr>
  );
};

Here’s the whole thing, it was tested on CodePen:

class NewCoinList extends React.Component {
  constructor(props) {
    super(props);
    
    this.state = {
      coinList: []
    }
  }

  componentDidMount() {
    fetch(`https://min-api.cryptocompare.com/data/all/coinlist`)
      .then(res => res.json())
      .then(json => this.setState({ coinList: json }));
  }

  renderList() {
    const data = this.state.coinList.Data;

    if (data == null) return null;

    return Object.keys(data).map((symbol) => (
      <CoinListItem data={data[symbol]} />

    ))
  }

  render() {
    return (
      <table className='table table-striped'>
        <thead>
          <tr>
            <th className="col-md-2">Coin</th>
            <th className="col-md-2">Symbol</th>
            <th className="col-md-2">Algorithm</th>
            <th className="col-md-2">#</th>
          </tr>
        </thead>
        <tbody>
          {this.renderList()}
        </tbody>
      </table>
    )
  }
}
  
const CoinListItem = ({data}) => {
  return (
    <tr>
      <td className="col-md-2">{data.CoinName}</td>
      <td className="col-md-2">{data.Symbol}</td>
      <td className="col-md-2">{data.Algorithm}</td>
      <td className="col-md-2">{data.SortOrder}</td>
    </tr>
  );
};

ReactDOM.render(
  <NewCoinList />,
  document.getElementById('container')
);

I hope that helps. :slight_smile:

I didn’t check if it worked, sorry, I was just using it as a rough example. But the point stands: that’s how you do it - you pass via props. You have to do that, that’s the core concept of React. I think you’re also getting confused over the actual Data key and the concept of a key when iterating over arrays in React. This should work, not tested but correct data structure now:

  componentDidMount() {
    fetch(`https://min-api.cryptocompare.com/data/all/coinlist`)
    .then(res => res.json())
    .then(res => this.setState({coins: Object.values(res.Data) });

So have a parent component that has state (array of coins). Use componentDidMount to make request and load. Use Object.values (or iterate using keys, values is much easier) to get the get the values in Data in your array of objects. Pass that array as a prop to a child (the actual rows containing the data). map over that array in the subcomponent. Use some value that you know will be unique for each mapped component as the key prop otherwise you’ll get a key warning in the console

This puts the core state in one place; try not to use it more than that, then you can just use props and stateless components.