Routing is essential to most web applications. You cannot cover all of the features of your application in just one page. It would be overloaded, and your user would find it difficult to understand. Sharing links to pictures, profiles, or posts is also very important for a social network such as Graphbook. It is also crucial to split content into different pages, due to search engine optimization (SEO).
In this article, we will learn how to do client-side routing in a React application. We will cover the installation of React Router, implement routes, create user profiles with GraphQL backend, and handle manual navigation.
We will first start by installing and configuring React Router 4 by running npm:
npm install --save react-router-dom
From the package name, you might assume that this is not the main package for React. The reason for this is that React Router is a multi-package library. That comes in handy when using the same tool for multiple platforms. The core package is called react-router.
There are two further packages. The first one is the react-router-dom package, which we installed in the preceding code, and the second one is the react-router-native package. If at some point, you plan to build a React Native app, you can use the same routing, instead of using the browser's DOM for a real mobile app.
The first step that we will take introduces a simple router to get our current application working, including different paths for all of the screens. There is one thing that we have to prepare before continuing. For development, we are using the webpack development server. To get the routing working out of the box, we will add two parameters to the webpack.client.config.js file. The devServer field should look as follows:
devServer: { port: 3000, open: true, historyApiFallback: true, },
The historyApiFallback field tells the devServer to serve the index.html file, not only for the root path, http://localhost:3000/ but also when it would typically receive a 404 error. That happens when the path does not match a file or folder that is normal when implementing routing.
The output field at the top of the config file must have a publicPath property, as follows:
output: { path: path.join(__dirname, buildDirectory), filename: 'bundle.js', publicPath: '/', },
The publicPath property tells webpack to prefix the bundle URL to an absolute path, instead of a relative path. When this property is not included, the browser cannot download the bundle when visiting the sub-directories of our application, as we are implementing client-side routing.
Before implementing the routing, we will clean up the App.js file. Create a Main.js file next to the App.js file in the client folder. Insert the following code:
import React, { Component } from 'react'; import Feed from './Feed'; import Chats from './Chats'; import Bar from './components/bar'; import CurrentUserQuery from './components/queries/currentUser';
export default class Main extends Component { render() { return ( <CurrentUserQuery> <Bar changeLoginState={this.props.changeLoginState}/> <Feed /> <Chats /> </CurrentUserQuery> ); }}
As you might have noticed, the preceding code is pretty much the same as the logged in condition inside the App.js file. The only change is that the changeLoginState function is taken from the properties, and is not directly a method of the component itself. That is because we split this part out of the App.js and put it into a separate file. This improves reusability for other components that we are going to implement.
Now, open and replace the render method of the App component to reflect those changes, as follows:
render() { return ( <div> <Helmet> <title>Graphbook - Feed</title> <meta name="description" content="Newsfeed of all your friends on Graphbook" /> </Helmet> <Router loggedIn={this.state.loggedIn} changeLoginState= {this.changeLoginState}/> </div> ) }
If you compare the preceding method with the old one, you can see that we have inserted a Router component, instead of directly rendering either the posts feed or the login form. The original components of the App.js file are now in the previously created Main.js file. Here, we pass the loggedIn state variable and the changeLoginState function to the Router component. Remove the dependencies at the top, such as the Chats and Feed components, because we won't use them any more thanks to the new Main component. Add the following line to the dependencies of our App.js file:
import Router from './router';
To get this code working, we have to implement our custom Router component first. Generally, it is easy to get the routing running with React Router, and you are not required to separate the routing functionality into a separate file, but, that makes it more readable. To do this, create a new router.js file in the client folder, next to the App.js file, with the following content:
import React, { Component } from 'react'; import LoginRegisterForm from './components/loginregister'; import Main from './Main'; import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom';
export default class Routing extends Component { render() { return ( <Router> <Switch> <Route path="/app" component={() => <Main changeLoginState= {this.props.changeLoginState}/>}/> </Switch> </Router> ) }}
At the top, we import all of the dependencies. They include the new Main component and the react-router package. The problem with the preceding code is that we are only listening for one route, which is /app. If you are not logged in, there will be many errors that are not covered. The best thing to do would be to redirect the user to the root path, where they can log in.
The primary goal of this article is to build a profile page, similar to Facebook, for your users. We need a separate page to show all of the content that a single user has entered or created.
We have prepared most of the work required to add a new user route. Open up the router.js file again. Add the new route, as follows:
<PrivateRoute path="/user/:username" component={props => <User {...props} changeLoginState={this.props.changeLoginState}/>} loggedIn={this.props.loggedIn}/>
Those are all of the changes that we need to accept parameterized paths in React Router. We read out the value inside of the new user page component. Before implementing it, we import the dependency at the top of router.js to get the preceding route working:
import User from './User';
Create the preceding User.js file next to the Main.js file. Like the Main component, we are collecting all of the components that we render on this page. You should stay with this layout, as you can directly see which main parts each page consists of. The User.js file should look as follows:
import React, { Component } from 'react'; import UserProfile from './components/user'; import Chats from './Chats'; import Bar from './components/bar'; import CurrentUserQuery from './components/queries/currentUser';
export default class User extends Component { render() { return ( <CurrentUserQuery> <Bar changeLoginState={this.props.changeLoginState}/> <UserProfile username={this.props.match.params.username}/> <Chats /> </CurrentUserQuery> ); }}
We use the CurrentUserQuery component as a wrapper for the Bar component and the Chats component. If a user visits the profile of a friend, they see the common application bar at the top. They can access their chats on the right-hand side, like in Facebook.
We removed the Feed component and replaced it with a new UserProfile component. Importantly, the UserProfile receives the username property. Its value is taken from the properties of the User component. These properties were passed over by React Router. If you have a parameter, such as a username, in the routing path, the value is stored in the match.params.username property of the child component. The match object generally contains all matching information of React Router.
From this point on, you can implement any custom logic that you want with this value. We will now continue with implementing the profile page.
Follow these steps to build the user's profile page:
import React, { Component } from 'react'; import PostsQuery from '../queries/postsFeed'; import FeedList from '../post/feedlist'; import UserHeader from './header'; import UserQuery from '../queries/userQuery';
The first three lines should look familiar. The last two imported files, however, do not exist at the moment, but we are going to change that shortly. The first new file is UserHeader, which takes care of rendering the avatar image, the name, and information about the user. Logically, we request the data that we will display in this header through a new Apollo query, called UserQuery.
export default class UserProfile extends Component { render() { const query_variables = { page: 0, limit: 10, username: this.props.username }; return ( <div className="user"> <div className="inner"> <UserQuery variables={{username: this.props.username}}> <UserHeader/> </UserQuery> </div> <div className="container"> <PostsQuery variables={query_variables}> <FeedList/> </PostsQuery> </div> </div> ) } }
The UserProfile class is not complex. We are running two Apollo queries simultaneously. Both have the variables property set. The PostQuery receives the regular pagination fields, page and limit, but also the username, which initially came from React Router. This property is also handed over to the UserQuery, inside of a variables object.
To use the username as input to the GraphQL query we first have to change the query string from the GET_POSTS variable. Change the first two lines to match the following code:
query postsFeed($page: Int, $limit: Int, $username: String) { postsFeed(page: $page, limit: $limit, username: $username) {
Add a new line to the getVariables method, above the return statement:
if(typeof variables.username !== typeof undefined) { query_variables.username = variables.username; }
If the custom query component receives a username property, it is included in the GraphQL request. It is used to filter posts by the specific user that we are viewing.
import React, { Component } from 'react'; import { Query } from 'react-apollo'; import Loading from '../loading'; import Error from '../error'; import gql from 'graphql-tag';
const GET_USER = gql` query user($username: String!) { user(username: $username) { id email username avatar } }`;
The preceding query is nearly the same as the currentUser query. We are going to implement the corresponding user query later, in our GraphQL API.
export default class UserQuery extends Component { getVariables() { const { variables } = this.props; var query_variables = {}; if(typeof variables.username !== typeof undefined) { query_variables.username = variables.username; } return query_variables; } render() { const { children } = this.props; const variables = this.getVariables(); return( <Query query={GET_USER} variables={variables}> {({ loading, error, data }) => { if (loading) return <Loading />; if (error) return <Error><p>{error.message}</p></Error>; const { user } = data; return React.Children.map(children, function(child){ return React.cloneElement(child, { user }); }) }} </Query> ) } }
We set the query property and the parameters that are collected by the getVariables method to the GraphQL Query component. The rest is the same as any other query component that we have written. All child components receive a new property, called user, which holds all the information about the user, such as their name, their email, and their avatar image.
import React, { Component } from 'react';export default class UserProfileHeader extends Component { render() { const { avatar, email, username } = this.props.user; return ( <div className="profileHeader"> <div className="avatar"> <img src={avatar}/> </div> <div className="information"> <p> {username} </p> <p> {email} </p> <p>You can provide further information here and build your really personal header component for your users.</p> </div> </div> ) }}
We have finished the new front end components, but the UserProfile component is still not working. The queries that we are using here either do not accept the username parameter or have not yet been implemented.
With the new profile page, we have to update our back end accordingly. Let's take a look at what needs to be done, as follows:
We will begin with the postsFeed query.
Edit the postsFeed query in the RootQuery type of the schema.js file to match the following code:
postsFeed(page: Int, limit: Int, username: String): PostFeed @auth
Here, I have added the username as an optional parameter.
Now, head over to the resolvers.js file, and take a look at the corresponding resolver function. Replace the signature of the function to extract the username from the variables, as follows:
postsFeed(root, { page, limit, username }, context) {
To make use of the new parameter, add the following lines of code above the return statement:
if(typeof username !== typeof undefined) { query.include = [{model: User}]; query.where = { '$User.username$': username }; }
In the preceding code, we fill the include field of the query object with the Sequelize model that we want to join. This allows us to filter the associated Chats model in the next step.
Then, we create a normal where object, in which we write the filter condition. If you want to filter the posts by an associated table of users, you can wrap the model and field names that you want to filter by with dollar signs. In our case, we wrap User.username with dollar signs, which tells Sequelize to query the User model's table and filter by the value of the username column.
No adjustments are required for the pagination part. The GraphQL query is now ready. The great thing about the small changes that we have made is that we have just one API function that accepts several parameters, either to display posts on a single user profile, or to display a list of posts like a news feed.
Let's move on and implement the new user query. Add the following line to the RootQuery in your GraphQL schema:
user(username: String!): User @auth
This query only accepts a username, but this time it is a required parameter in the new query. Otherwise, the query would make no sense, since we only use it when visiting a user's profile through their username. In the resolvers.js file, we will now implement the resolver function using Sequelize:
user(root, { username }, context) { return User.findOne({ where: { username: username } }); },
In the preceding code, we use the findOne method of the User model by Sequelize, and search for exactly one user with the username that we provided in the parameter.
We also want to display the email of the user on the user's profile page. Add the email as a valid field on the User type in your GraphQL schema with the following line of code:
email: String
With this step, our back end code and the user page are ready.
This article walked you through the installation process of React Router and how to implement a route in React. Then we moved on to more advanced stuff by implementing a user profile, similar to Facebook, with a GraphQL backend.
If you found this post useful, do check out the book, Hands-on Full-Stack Web Development with GraphQL and React. This book teaches you how to build scalable full-stack applications while learning to solve complex problems with GraphQL.
How to build a Relay React App [Tutorial]
React vs. Vue: JavaScript framework wars