Handling route parameters

The URLs that we've looked at so far in this chapter have all been static. Most applications will use both static and dynamic routes. In this section, you'll learn how to pass dynamic URL segments into your components, how to make these segments optional, and how to get query string parameters.

Resource IDs in routes

A common use case is to make the ID of a backend resource part of the URL. This makes it easy for our code to grab the ID, then make an API call that fetches the relevant resource data. Let's implement a route that renders a user detail page. This will require a route that includes the user ID, which then needs to somehow be passed to the component so that it can fetch the user.

We'll start with the route:

import React from 'react'; 
import { 
  Router, 
  Route, 
  browserHistory, 
} from 'react-router'; 
 
import UserContainer from './UserContainer'; 
 
export default ( 
  <Router history={browserHistory}> 
    { /* Note the ":" before "id". This denotes 
         a dynamic URL segment and the value will 
         be available in the "params" property of 
         the rendered component. */ } 
    <Route path="/users/:id" component={UserContainer} /> 
  </Router> 
); 

The : syntax marks the beginning of a URL variable. So in this case, the id variable will be passed to the UserContainer component, which we'll now implement:

import React, { Component, PropTypes } from 'react'; 
import { fromJS } from 'immutable'; 
 
import User from './User'; 
import { fetchUser } from './api'; 
 
export default class UserContainer extends Component { 
  state = { 
    data: fromJS({ 
      error: null, 
      first: null, 
      last: null, 
      age: null, 
    }), 
  } 
 
  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 
 
  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 
 
  componentWillMount() { 
    // The dynamic URL segment we're interested in, "id", 
    // is stored in the "params" property. 
    const { params: { id } } = this.props; 
 
    // Fetches a user based on the "id". Note that it's 
    // converted to a number first. 
    fetchUser(+id).then( 
      // If the user was successfully fetched, then 
      // merge the user properties into the state. Also, 
      // make sure that "error" is cleared. 
      (user) => { 
        this.data = this.data 
          .merge(user, { error: null }); 
      }, 
 
      // If the user fetch failed, set the "error" state 
      // to the resolved error value. Also, make sure the 
      // other user properties are restored to their defaults 
      // since the component is now in an error state. 
      (error) => { 
        this.data = this.data 
          .merge({ 
            error, 
            first: null, 
            last: null, 
            age: null, 
          }); 
      } 
    ); 
  } 
 
  render() { 
    return ( 
      <User {...this.data.toJS()} /> 
    ); 
  } 
} 
 
// Params should always be there... 
UserContainer.propTypes = { 
  params: PropTypes.object.isRequired, 
}; 

The params property contains any dynamic parts of the URL. In this case, we're interested in the id parameter. Then, we pass the integer version of this value to the fetchUser() API call. If the URL is missing the segment completely, then this code won't run at all; the router will just complain about not being able to find a matching route. However, there's no type-checking done at the route level, which means it's up to you to design the failure mode.

In this example, the type cast will simply fail, if the user passes the string "one" for example, and a 500 error will happen. You could write a function that type-checks parameters and, instead of failing with an exception, responds with a 404 error. In any case, it's up to the application, not the react-route library, to provide a meaningful failure mode.

Now let's take a look at the API function we're using to respond with the user data:

// Mock data... 
const users = [ 
  { first: 'First 1', last: 'Last 1', age: 1 }, 
  { first: 'First 2', last: 'Last 2', age: 2 }, 
]; 
 
// Returns a promise that resolves to a 
// user from the "users" array, using the 
// given "id" index. If nothing is found, 
// the promise is rejected. 
export function fetchUser(id) { 
  return new Promise((resolve, reject) => { 
    const user = users[id]; 
 
    if (user === undefined) { 
      reject(`User ${id} not found`); 
    } else { 
      resolve(user); 
    } 
  }); 
} 

Either the user is found in the users array of mock data or the promise is rejected. If rejected, the error-handling behavior of the UsersContainer component is invoked. Last but not least, we have the component responsible for actually rendering the user details:

import React, { PropTypes } from 'react'; 
import { Map as ImmutableMap } from 'immutable'; 
 
// Renders "error" text, unless "error" is 
// null - then nothing is rendered. 
const Error = ({ error }) => 
  ImmutableMap() 
    .set(null, null) 
    .get(error, (<p><strong>{error}</strong></p>)); 
 
// Renders "children" text, unless "children" 
// is null - then nothing is rendered. 
const Text = ({ children }) => 
  ImmutableMap() 
    .set(null, null) 
    .get(children, (<p>{children}</p>)); 
 
const User = ({ 
  error, 
  first, 
  last, 
  age, 
}) => ( 
  <section> 
    { /* If there's an API error, display it. */ } 
    <Error error={error} /> 
 
    { /* If there's a first, last, or age value, 
         display it. */ } 
    <Text>{first}</Text> 
    <Text>{last}</Text> 
    <Text>{age}</Text> 
  </section> 
); 
 
// Every property is optional, since we might 
// have have to render them. 
User.propTypes = { 
  error: PropTypes.string, 
  first: PropTypes.string, 
  last: PropTypes.string, 
  age: PropTypes.number, 
}; 
 
export default User; 

Now, if you run the example, and navigate to /users/0, this is what the rendered page looks like:

Resource IDs in routes

And if you navigate to a user that doesn't exist, /users/2, here's what you'll see:

Resource IDs in routes

Optional parameters

Sometimes, we need to specify that certain aspects of a feature be enabled. The best way to specify this in many cases is to encode the option as part of the URL or provide it as a query string. URLs work best for simple options, and query strings work best if there are many optional values the component can use.

Let's implement a user list component that renders a list of users. Optionally, we want to be able to sort the list in descending order. Let's make this an optional path segment in the route definition for this page:

import React from 'react'; 
import { 
  Router, 
  Route, 
  browserHistory, 
} from 'react-router'; 
 
import UsersContainer from './UsersContainer'; 
 
export default ( 
  <Router history={browserHistory}> 
    { /* The ":desc" parameter is optional, and so 
         is the "/" that precedes it. The "()" syntax 
         marks anything that's optional. */ } 
    <Route 
      path="/users(/:desc)" 
      component={UsersContainer} 
    /> 
  </Router> 
); 

It's the () syntax that marks anything in between as optional. In this case, it's the / and the—desc parameter. This means that the user can provide anything they want after /users/. This also means that the component needs to make sure that the string desc is provided, and that everything else is ignored.

It's also up to the component to handle any query strings provided to it. So while the route declaration doesn't provide any mechanism to define accepted query strings, the router will still process them and hand them to the component in a predictable way. Let's take a look at the user list container component now:

import React, { Component, PropTypes } from 'react'; 
import { fromJS } from 'immutable'; 
 
import Users from './Users'; 
import { fetchUsers } from './api'; 
 
export default class UsersContainer extends Component { 
  // The "users" state is an empty immutable list 
  // by default. 
  state = { 
    data: fromJS({ 
      users: [], 
    }), 
  } 
 
  componentWillMount() { 
    // The URL and query string data we need... 
    const { 
      props: { 
        params, 
        location: { 
          query, 
        }, 
      }, 
    } = this; 
 
    // If the "params.desc" value is "desc", it means that 
    // "desc" is a URL segment. If "query.desc" is true, it 
    // means "desc" was provided as a query parameter. 
    const desc = params.desc === 'desc' || !!(query.desc); 
 
    // Tell the "fetchUsers()" API to sort in descending 
    // order if the "desc" value is true. 
    fetchUsers(desc).then((users) => { 
      this.data = this.data 
        .set('users', users); 
    }); 
  } 
 
  render() { 
    return ( 
      <Users {...this.data.toJS()} /> 
    ); 
  } 
} 
 
UsersContainer.propTypes = { 
  params: PropTypes.object.isRequired, 
  location: PropTypes.object.isRequired, 
}; 

The key aspect of this container is that it looks for either params.desc or query.desc. It uses this as an argument to the fetchUsers() API, to determine the sort order. So, here's what's rendered when you navigate to /users:

Optional parameters

And if we include the descending parameter by navigating to /users/desc, here's what we get:

Optional parameters

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.47.221