Howard Wang
Howard Wang

Reputation: 472

Sharing state between React components

I have a table component (parent) and in each row of the table, there's another component that's basically an image button (child). When clicked, the image button switches from its default three vertical dots (https://png.icons8.com/windows/1600/menu-2.png) to a download icon. The three vertical dots image has an onClick listener that switches it to the download icon and the download icon has an onClick listener that downloads the file in the table row. Each image button has it's own state (based on which image to display). In the table component, I have a div that wraps the entire screen. When that div is clicked (so basically if you click anywhere outside the images), I want to be able to reset all the images in the table back to the three dots. I'm not sure how to accomplish this since there's many child components, each with it's own state (therefore I don't think redux would work). Is there a way to share the state of each image button with the table component?

Here's some of the table code

<div className='shield' onClick={() => this.resetDownloads()}></div>
<table className='table table-striped'>
  <tbody>
    this.props.songs.data.map((song, index) => 
      <tr key={index}>
       <td>{song.name}</td>
       <td>{song.artist}</td>
       <td>{song.duration}</td>
       <td><ActionButton song={song}/></td>
      </tr>
  </tbody>
 </table>

Here's some of the ActionButton component

class ActionButton extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            download: false
        };
        this.toggle = this.toggle.bind(this);
    }

    toggle(e) {
        e.stopPropagation();
        this.setState({
            download: !this.state.download
        });
    }

    render() {
      return this.state.download ? (
        <div>
          <img src={Download} width='15%' onClick={(e) => this.props.downloadSong(this.props.song, e)}></img>
        </div>
      )
      : (
        <div>
          <img src={Dots} width='15%' onClick={(e) => this.toggle(e)} className='image'></img>
        </div>
      )
    }
}

Upvotes: 0

Views: 3295

Answers (1)

devserkan
devserkan

Reputation: 17598

This is where you lift your state up. Actually, in the first place, you don't need a state in your ActionButton component. It should be a stateless component. You can keep all your data in the parent component.

Let's assume there is an id property in the song data. You can track a downloadState in the parent component and add this song's id to this state object. Then you can pass this value to your ActionComponent and use it. Also, you can keep all your functions in your parent component.

const songs = [
  { id: "1", name: "Always Blue", artist: "Chet Baker", duration: "07:33" },
  { id: "2", name: "Feeling Good", artist: "Nina Simone", duration: "02:58" },
  { id: "3", name: "So What", artist: "Miles Davis", duration: "09:23" },
]

class App extends React.Component {
  state = {
    downloadState: {},
  }
  
  toggle = ( e, id ) => {
    e.stopPropagation();
    this.setState( prevState => ({
      downloadState: { ...prevState.downloadState, [id]: !prevState.downloadState[id]}
    }))
  }

  downloadSong = ( e, song ) => {
    e.stopPropagation();
    alert( song.name );
  }

  resetDownloads = () => this.setState({ downloadState: {}});
  

  render() {
    return (
      <div onClick={this.resetDownloads}>
      <table>
        <tbody>
        {
          songs.map((song, index) => (
          <tr key={index}>
            <td>{song.name}</td>
            <td>{song.artist}</td>
            <td>{song.duration}</td>
            <td>
            <ActionButton
              toggle={this.toggle}
              song={song}
              downloadState={this.state.downloadState}
              downloadSong={this.downloadSong}
            />
            </td>
          </tr>
          ))
        }
        </tbody>
      </table>
      </div>
    )
  }
}

const ActionButton = props => {
  const { downloadState, downloadSong, song, toggle } = props;
  
  const handleToggle = e => toggle(e, song.id);
  const handleDownload = e => downloadSong( e, song );
  
  const renderImages = () => {
    let images = "";
    if ( downloadState[song.id] ) {
      images = <p onClick={handleDownload}>Download</p>;
    } else {
      images = <p onClick={handleToggle}>Dots</p>;
    }
    return images;
  }

  return (
    <div>{renderImages()}</div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
table, th, td {
    border: 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

If there isn't any id property then you can set up the same logic with indexes but I think every data should have an id :) Maybe instead of using indexes same logic can be used with song names since they are almost unique. Who knows :)

Upvotes: 2

Related Questions