Evanss
Evanss

Reputation: 23593

Use URL on a component page to identify and pass data in state from parent with React Router?

Im using React Router. In my state I have a number of TV shows identifiable by an ID. On the shows page I need to load the data for the specific show, matching the show ID to the end of the URL.

My state looks something like this but with more shows:

shows: [
  {
    id: 1318913
    name: "Countdown"
    time: "14:10"
  },
  {
    id: 1318914
    name: "The News"
    time: "15:00"
  }
]

In App.js:

<BrowserRouter>
  <Switch>
    <Route
      exact
      path="/"
      render={() => {
        return <Table shows={this.state.shows} />;
      }}
    />
    <Route
      path="/show/:showID"
      render={() => {
        return <Show />;
      }}
    />
    <Route component={FourOhFour} />
  </Switch>
</BrowserRouter>;

React Router makes the :showID part of the URL available on the shows page . So I could pass my entire show state to the Show component and then find the correct show from the url. However I imagine this is inefficient?

I was wondering if I should look up the show via the ID in App.js and only pass the correct show to the Show component. Is this better practice / more efficient?

Here is my Table component:

class Table extends Component {
  render(props) {
    return (
      <table className="table table-striped table-bordered table-hover">
        <tbody>
        {
          this.props.shows.map((item)=>{
            return <TableRow
              name={item.name}
              time={item.time}
              key={item.eppisodeId}
              eppisodeId={item.eppisodeId}
              img={item.img}
            />
          })
        }
        </tbody>
      </table>
    )
  }
}

Here is my TableRow component:

class TableRow extends Component {
  render(props) {
    const image = this.props.img !== null ?
      <img src={this.props.img} alt={this.props.name} />
      : null;
    const showUrl = '/show/' + this.props.eppisodeId;
    return (
      <tr className="tableRow">
        <td className="tableRow--imgTd">{image}</td>
        <td><Link to={showUrl}>{this.props.name}</Link></td>
        <td className="tableRow--timeTd">{this.props.time}</td>
      </tr>
    )
  }
}

Upvotes: 5

Views: 173

Answers (2)

Shubham Khatri
Shubham Khatri

Reputation: 281726

As I see in your question, you are using Link to navigate. Considering you want to also send the data with the Link, you can pass an object to it instead of the string path as mentioned in the React-router docs, So you can pass the show information with the state object in TableRow component like

const {name, time, episodeId, img} = this.props;
<Link to={{pathname:showUrl, state: {show: {name, time, episodeId, img }}}}>{this.props.name}</Link>

Now you can retrieve this information in Show component as

this.props.location.state && this.props.location.state.name

The other thing that you need to take care of is, that you need to pass the props to the render function

 <Switch>
       <Route
      exact
      path="/"
      render={(props) => {
        return <Table shows={this.state.shows} {...props}/>;
      }}
    />
    <Route
      path="/show/:showID"
      render={(props) => {
        return <Show {...props}/>;
      }}
    />
    <Route component={FourOhFour} />
  </Switch>

Now the above solution will work only if you navigate from within the App, but if you change the URL it won't work, if you want to make it more robust, you would need to pass the data to the show component and then optimise in the lifecycle functions

You would . do it like

<Switch>
       <Route
      exact
      path="/"
      render={(props) => {
        return <Table shows={this.state.shows} {...props}/>;
      }}
    />
    <Route
      path="/show/:showID"
      render={(props) => {
        return <Show shows={this.state.shows} {...props}/>;
      }}
    />
    <Route component={FourOhFour} />
</Switch>

And then in the Show component

class Show extends React.Component {
    componentDidMount() {
        const {shows, match} = this.props;
        const {params} = match;
        // get the data from shows which matches params.id here using `find` or `filter` or whatever you feel appropriate

    }
    componentWillReceiveProps(nextProps) {
        const {match} = this.props;
        const {shows: nextShows, match: nextMatch} = nextProps;
        const {params} = match;
        const {params: nextParams} = nextMatch;
        if( nextParams.showId !== params.showId) {
            // get the data from nextProps.showId and nextShows here 
        }

    }
}

However this is where libraries like redux and reselect are useful. with Redux your data shows will be in one common store and you can access it in any component and reselect gives you an option to create a selector that is memoized to get the appropriate show data from shows, so that the same calculation is not repeated again. Please explore about these and check if you find them a good fit for your project.

Upvotes: 2

Evanss
Evanss

Reputation: 23593

The component parameter can accept a function. If you pass props then with props.match.params.id you can find the ID from the url. As we're in a function we're able to find the show from our state and pass it to the Show component.

<BrowserRouter>
  <Switch>
    <Route
      exact
      path="/"
      render={() => {
        return <Table shows={this.state.shows} />;
      }}
    />

    <Route path="/show/:id" component={(props)=>{
      const foundShow = this.state.shows.find((show)=>{
      const urlId = props.match.params.id;
         return show.eppisodeId === parseInt(urlId);
       });
       // {...props} is optional, it will pass the other properties from React Router
       return <Show show={foundShow} {...props} />
     }} />

    <Route component={FourOhFour} />
  </Switch>
</BrowserRouter>;

There is however some weird behaviour. If I console.log(this.props); in the Show component then I can see its being rendered twice. The first time this.props.show is empty and the second time it has the data that I passed. This is a real pain as I have to check for the presence of each variable before I render it, which makes me thing I must done something wrong...

UPDATE: The following solves the above problem but I don't know if this is a good practice:

class Show extends Component {
  render(props) {
    if (this.props.foundShow) {
      const {name, network, time} = this.props.foundShow;
      return (
        <div>
          <h2>
            {name} on {network} at {time}
          </h2>
        </div>
      )
    } else {
      return (
        <h2>Loading....</h2>
      )
    }
  }
}

Upvotes: 0

Related Questions