0

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>
      )
    }
}
Howard Wang
  • 472
  • 6
  • 18
  • https://stackoverflow.com/questions/38901106/how-to-make-a-shared-state-between-two-react-components – Ramesh Aug 24 '18 at 19:06
  • 3
    Can you share your code. – Anas Aug 24 '18 at 19:06
  • @Anas added some code! – Howard Wang Aug 24 '18 at 19:18
  • @Ramesh I've looked at that question and it doesn't really apply to me because I have many child components and can't use redux to manage the state of all of them as each child component is independent of each other. – Howard Wang Aug 24 '18 at 19:19
  • The images in the table cells can only have either download state or 3 dots state right? And are the number of rows in the table known before hand? – Vijay Venugopal Menon Aug 24 '18 at 19:24
  • Redux is specifically helpful in this cases, you can manage all your component states in it. – Anas Aug 24 '18 at 19:25
  • What I was trying to get to is that if you know the number of rows in the table beforehand, maintain an array in the table component, something like [{row1:'download'},{row2:'threedots'},{row3:"download"}] . Then pass these as props to the rows and then when the rows are clicked call a parent function in table component to update the value in the array for that index and change the value from 'download' to 'threedots' or viceversa. Then when we click outside, update the array and set all values to 'threedots'. You can even use redux, just to do this part. – Vijay Venugopal Menon Aug 24 '18 at 19:31
  • This way, the other state properties of the rows remain as it is. You are just dealing with the image icon on the rows, which can only have either one of 2 states. – Vijay Venugopal Menon Aug 24 '18 at 19:32
  • @VijayMenon Gotcha! That makes a lot of sense. Thanks! – Howard Wang Aug 24 '18 at 19:41

1 Answers1

2

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 :)

devserkan
  • 16,870
  • 4
  • 31
  • 47