Viewing a record on its own route

Linked Projects:

Calreact
 
Lesson code - https://github.com/learnetto/calreact/tree/m7-react-router

Now let’s add our second route - a route for viewing an individual appointment, or the equivalent of a Rails show page.

So let’s define that route now:

<Route path = "/appointments/:id" component={Appointment} />

Just like in Rails, we can specify a route with a URL parameter like this.

And what we want to render there is the Appointment component.

We need a way to easily navigate to that route as well, instead of having to manually type the URL in the browser.

So on our homepage - in our appointments list - let’s add links to each individual appointment, so that we can go its route and view it.

In our Appointment component, when we render the title, we want it to be a link.

We’re going to use the Link component from React Router.

Let’s import Link:

import { Link } from 'react-router-dom';

And then use it in relation to appointments:

<Link to={`/appointments/${appointment.id}`}>

We also need to import the Appointment component in AppRouter:

import { Appointment } from './Appointment';

When we try to load the page, something seems broken. Our page is not loading fully.

[screenshot: 1:27]

We have an error here saying: 

A <Router> may have only one child element.

We need to enclose all of our routes in a <div>.

Now our page should load. And we have links on the appointment titles.

[screenshot: 1:40]

If I click on a link, you’ll see that the URL changes, but the page doesn’t seem to have changed.

[screenshot 1:41-42]

It’s just showing the same things as the homepage.

The reason that it is happening is that React Router matches the path to all  the routes that contain it.

Because the slash (/) matches all the routes, whatever we render on the home route will show up on all our routes.

We can avoid that by passing the exact prop:

<Route exact path="/" render={routeProps => (
...

Now if I navigate to an individual appointment link, only the titles at the top show up, but the other stuff from the homepage doesn’t.

[screenshot 2:23]

The titles are coming from the index file. 

The appointment data is not showing because we’re not passing the appointment prop, as per this prop type warning.

We could do it in the route using the render prop like we did for the home route. But then we’d have to fetch the data for individual appointments here in the router. We don’t really want to do that.

What we’re going to do is - when the user hits the appointment route, that’s when we’ll fetch its data.

We’ll do that in the Appointment component.

First I’m going to change this from a function component to a class.

I’m just going to paste in the code for that.

export default class Appointment extends React.Component {
 constructor (props) {
  super(props)
 }
 static propType = {
  appointment: PropTypes.object.isRequired
 }
 
 render () {
  return (
   <div className='appointment'>
    <h2>Appointment</h2>
    <Link to={`/appointments/${this.props.appointment.id}`} >
     <h3>{this.props.appointment.title}</h3>
    </Link>
    <p>{formatDate(this.props.appointment.appt_time)}</p>
   </div>
  )
 }
}

I’ll need to change the imports for this component in other components from 

import { Appointment } from './Appointment';

to

import Appointment from './Appointment';

Ok, now let’s fix the prop type warning by defining a default value for the appointment prop.

[screenshot 3:12]

We can define a static property called defaultProps.

Let’s set appointment to an empty object.

static defaultProps = {
 appointment: {}
}

And now we won’t get a failed prop type warning.

[screenshot 3:27]

This is a way to define a default value for a prop in case you're using the component you don't pass a prop value.

And we can see a title Appointment and datetime on the page.

[screenshot 3:37]

The datetime shouldn’t really appear because we haven’t got any appointment data yet.

I think that means there’s a bug in our formatDate utility method.

When you pass a blank value to moment, it just uses the current datetime, so let’s add a check and return an empty string if no datetime is passed to it:

return(d ? moment(d).format('MMMM DD YYYY, h:mm:ss a') : '');

Now the datetime won’t show unless the appointment data is loaded.

Back in the Appointment component, let’s remove the <h2> tag. We don’t really need it.

Now let’s initialize the state for this component and store the appointment data in there.

We’ll set it to props.appointment.

export default class Appointment extends React.Component {
 constructor (props) {
  super(props)
  this.state = {
    appointment: props.appointment
  }
}

And then use that state value down here instead of the prop.

render () {
 return () {
  <div className='appointmet'>
   <Link to={`/appointments/${this.state.appointment.id}`} >
    <h3>{this.state.appointment.title}</h3>
   </Link>
   <p>{formatDate(this.state.appointment.appt_time)}</p>
  </div>
 )
}

Now we’ll fetch the appointment data in the componentDidMount method.

The componentDidMount hook is a React component lifecycle hook which runs after the component output has been rendered to the DOM.

We’ll make an AJAX request to get the appointment data.

To do that, we add:

componentDidMount () {
 $.ajax({
  type: "GET",
  url: `/appointments/${this.props.match.id}`
  
}

Here match is a prop passed by React Router.

And the dataType:

componentDidMount () {
 $.ajax({
  type: "GET",
  url: `/appointments/${this.props.match.id}`
  dataType: "JSON"
 }).done((data) => {
  this.setState({appointment: data});
 })
}

Now we need to define a show action on the appointments controller to send the data to the client.

def show
  @appointment = Appointment.find(params[:id]
  render json: @appointment
end

Now reload the homepage, then click through to an appointment, and the data now loads.

[screenshot 5:47]

But notice when we go to the homepage, we now get an error in the console.

Cannot find property 'params' of undefined
[screenshot 5:52]

That’s happening because when the Appointment component is rendered on the homepage, it doesn’t get a prop called match.

That only gets passed by react router, when we go to an individual appointment route.

So we should make this AJAX call only when the match prop exists.

Let’s add a check:

componentDidMount () {
 if(this.props.match) {
  $.ajax({
   type: "GET",
   url: `/appointments/${this.props.match.id}`
   dataType: "JSON"
  }).done((data) => {
   this.setState({appointment: data});
  })
 }
}

And that check fixes the error.

And so our show route works.

Liked this tutorial? Get more like this in your inbox