React-router

From bibbleWiki
Jump to navigation Jump to search

Introduction

This page is about routering with React.

Setup

Install package for Router

npm i react-router-dom

Add BrowserRouter to Index.js

...
import { BrowserRouter } from 'react-router-dom';
...
ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

Add Routes to App.js

We need to add routes to the the router we support. Note we can redirect from old routes within a switch (example logout below)

import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';

import Explore from './components/Explore';
import Home from './components/Home';
import Messages from './components/Messages';
import Profile from './components/Profile';

function App() {
  return (

    <Router>
        <div>
          <h2>Welcome to React Router Tutorial</h2>
          <nav className="navbar navbar-expand-lg navbar-light bg-light">
          <ul className="navbar-nav mr-auto">
            <li><Link to={'/'} className="nav-link"> Home </Link></li>
            <li><Link to={'/explore'} className="nav-link">Explore</Link></li>
            <li><Link to={'/messages'} className="nav-link">Messages</Link></li>
            <li><Link to={'/profile'} className="nav-link">Profile</Link></li>
          </ul>
          </nav>
          <hr />
          <Switch>
              <Route exact path='/' component={Home} />
              <Route path='/explore' component={Explore} />
              <Route path='/messages' component={Messages} />
              <Route path='/profile' component={Profile} />
              <Route path='/auth/logout' component={Logout} />
              <Redirect to="/auth/logout" from="/auth/logOut" />
 
          </Switch>
        </div>
      </Router>

  );
}

export default App;

Router Children Component

All router children components get the following props.

  • match Exact Match, Params, Path and matching Url
  • location Key, path name and search
  • history This is the browser history

NavLink Component

This allows us to work with the activeClassName and exact to highlight the list which is active.

  <Router>
    <Nav authenticated={!defaultToSignIn} />
    <hr />
    <Switch>
      <Route path="/" component={Home} exact />
      <Route path="/auth/login" component={Login} />
      <Route path="/auth/Register" component={Register} />
      <Route path="/auth/logout" component={Logout} />
    </Switch>
  </Router>

And for some of the NavLink routes

            <li className="nav-item">
              <NavLink
                to="/home"
                aria-current="page"
                className="nav-link"
                exact
              >
                Home
              </NavLink>
            </li>
        </ul>
        <ul className="navbar-nav ms-auto mb-2 mb-lg-0">
            <li className="nav-item">
              <NavLink to="/explore" aria-current="page" className="nav-link">
                Explore
              </NavLink>
            </li>
            <li className="nav-item">
              <NavLink to="/messages" aria-current="page" className="nav-link">
                Messages
              </NavLink>
            </li>

Prompt Component

We can prevent leaving of a page with the Prompt component. For example.

...
  return (
    <div className="Login">
      {error && <p intent="danger">{error}</p>}
      <Prompt
        when={!usernameValid || !passwordValid}
        message="Leaving page will loose your data"
      />
      <Form onSubmit={handleSubmit} className="row g-3 needs-validation">
        <div>
          <div className="col-md-4">
            <Form.Group size="lg" controlId="username">
              <Form.Label>Username</Form.Label>
              <Form.Control
...

Adding a 404 Page

To implement this we just create a route with no path and a component

  <Router>
    <Nav authenticated={!defaultToSignIn} />
    <hr />
    <Switch>
      <Route
        exact
        path="/"
        render={() =>
          defaultToSignIn ? (
            <Redirect to="/auth/login" />
          ) : (
            <Redirect to="/home" />
          )
        }
      />
      <ProtectedRoute path="/" component={Home} exact />
      <ProtectedRoute path="/explore" component={Explore} />
      <ProtectedRoute path="/messages" component={Messages} />
      <ProtectedRoute path="/profile" component={Profile} />
      <Route path="/auth/login" component={Login} />
      <Route path="/auth/Register" component={Register} />
      <Route path="/auth/logout" component={Logout} />
      <Route component={PageNotFound} />
    </Switch>
  </Router>

Rendering Component with Parameters

When you need to add props to the component to render we use the render property which takes a function as an argument and should return the Component for the page.

Router

  <Router>
    <Switch>
      <Route path="/auth/logout" component={Logout} />
      <Route path="/compwithprops" render={() => {
           return <CompWithProps prop1="#ff0000" prop2='Red'/>
      }) />
      <Route component={PageNotFound} />
    </Switch>

Component

For the component we need to wrap it with withRouter in a HOC.

import React from "react";

const CompWithProps = ({prop1,prop2}) => (
  <div>
..
  </div>
);

CompWithProps.proptypes = {
    prop1: PropTypes.string.isRequired,
    prop2: PropTypes.string.isRequired,
}

export default withRouter(CompWithProps);

Custom Route with Parameters

Here we create a custom route which takes a parameter :eid

...
<CustomRoute path={'/api/orders/:eid'} component={Home} />
...

I the custom route we now Here we can create a Route which pulls this data in the props. Like the example above we can render the component Home and the eid will be available in the props.match.params.eid

import React from "react";
import { Route } from "react-router-dom";

const CustomRoute = ({ component: ComponentToRender, ...rest }) => (
  <Route
    {...rest}
    render={(props) => {
      alert(`Cusom Route ${props.match.params.eid} `);
      return <ComponentToRender {...props} />;
    }}
  />
);
export default CustomRoute;

Protected Route

Authentication Service

We create an authService which emulates the following

import Cookies from "universal-cookie";

const cookies = new Cookies();
const authService = {

  async signIn(timeout, username, password) {
    const genericErrorMessage = "Unable to perform login.";

    const resource = `${process.env.REACT_APP_API_ENDPOINT}v1/login`;
    const options = { timeout };
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), timeout);
    try {
      const response = await fetch(resource, {
        options,
        signal: controller.signal,
        method: "POST",
        credentials: "include",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ username, password }),
      });
      clearTimeout(id);
      const responseAsJson = await response.json();
      if (responseAsJson.success) {
        return {
          error: false,
          status: response.status,
          token: responseAsJson.token,
        };
      }

      if (response.status === 400) {
        return {
          error: true,
          status: response.status,
          message: genericErrorMessage,
        };
      }
      if (response.status === 401) {
        return {
          error: true,
          status: response.status,
          message: responseAsJson.message,
        };
      }

      return {
        error: true,
        status: 400,
        message: responseAsJson.message,
      };
    } catch (error) {
      console.log(error);
      return {
        error: true,
        status: 500,
        message: "Unknown Error",
      };
    }
  },

  async signOut(token) {
    const genericErrorMessage = "Unable to perform logout.";

    const url = `${process.env.REACT_APP_API_ENDPOINT}v1/logout`;

    const response = await fetch(url, {
      method: "GET",
      credentials: "include",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
    });

    const responseAsJson = await response.json();
    if (!responseAsJson.success) {
      if (response.status === 400) {
        return {
          error: true,
          status: response.status,
          message: genericErrorMessage,
        };
      }
      if (response.status === 401) {
        return {
          error: true,
          status: response.status,
          message: responseAsJson.message,
        };
      }
      return {
        error: true,
        status: 400,
        message: responseAsJson.message,
      };
    }
    return {
      error: false,
      status: response.status,
    };
  },
};

export default authService;

Create Private Route

Introduction

This is more complicated than expected. What we are trying to achieve is

  • If unprotected just route
  • If not authenticated and route protected, prompt to login, and route
  • if authenticated route to protected route

We need to make sure that people who access /user/myprofile get prompted to sign in and then if successful proceed to /user/myprofile. To achieve this we need to

  • Change routes to use custom route
  • Create a custom Route to check for authentication
  • Store the original route in the Login so after login continue

Change routes to use custom route

We will make a custom route PrivateRoute which will perform checks on the protected routes.

  <Router>
    <Switch>
      <PrivateRoute path="/" component={Home} exact />
      <PrivateRoute path="/explore" component={Explore} />
      <Privateoute path="/messages" component={Messages} />
      <PrivateRoute path="/profile" component={Profile} />
      <Route path="/auth/login" component={Login} />
      <Route path="/auth/Register" component={Register} />
      <Route path="/auth/logout" component={Logout} />
      <Route component={PageNotFound} />
    </Switch>
  </Router>

Create a custom Route to check for authentication

We wrap the component to render which allows us to do some processing. In this example if authenticated, we render the component or we redirect to the /login route. Before doing so store the requested protected route.

import React from "react";
import { Redirect } from "react-router-dom";
import authService from "../service/authService";

const PrivateRoute = ({ component: Component, ...rest }) => (
  <Route {...rest}
    render={(props) => {
      authService.isAuthenticated() ? (
        <Component {...props} />
      ) : (
          <Redirect to={{
              pathname: '/login',
              state: { target: props.location }    
          }} />
      );
    }}
  />
);

export default PrivateRoute;

Store the original route in the Login so after login continue

Here we process the form but the main processing is highlighted.

We extract the state if set in the ProtectedRoute above. If it wasn't we default to the login, if it was we use that route.

If we are authenticated we either go to the default route or the route set in the ProtectedRoute.

...
import { UserContext } from "../context/UserContext";

const Login = (props) => {
...
  useEffect(() => {
    const updateFormValid = () => {
      if (usernameValid === isValid && passwordValid === isValid) {
        setFormValid(true);
      } else {
        setFormValid(false);
      }
    };
    updateFormValid();
  }, [usernameValid, passwordValid]);

  const { target } = props.location.state || {
    target: { pathname: "/auth/login" },
  };

  if (userContext.token) {
    if (target.pathname === "/auth/login") {
      target.pathname = "/";
    }
    return <Redirect to={target} />;
  }
...
  const handleSubmit = async (e) => {
    e.preventDefault();
...
  };
  return (
...
  );
};

export default Login;

Logout

Here is the example of the logout function. The main issue I came across was the rendering occurred twice which I was let to believe because of React.StrictMode. To overcome this I used the useEffect hook.

import React, { useEffect, useState, useContext } from "react";
import { Redirect } from "react-router-dom";
import { UserContext } from "../context/UserContext";
import authService from "../service/authService";

const Logout = () => {
  const [userContext, setUserContext] = useContext(UserContext);
  const [, setError] = useState();

  const genericErrorMessage = "Something went wrong! Please try again later.";

  useEffect(() => {
    const PerformLogout = async () => {

      const url = `${process.env.REACT_APP_API_ENDPOINT}v1/logout`;
      const response = await fetch(url, {
        method: "GET",
        credentials: "include",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${userContext.token}`,
        },
      });

      const responseAsJson = await response.json();

      if (!responseAsJson.success) {
        if (response.status === 400) {
          setError(genericErrorMessage);
        } else if (response.status === 401) {
          setError(responseAsJson.message);
        } else {
          setError(genericErrorMessage);
        }
      } else {
        setUserContext((oldValues) => ({
          ...oldValues,
          details: undefined,
          token: null,
        }));
      }
    };

    PerformLogout();

    // Anything in here is fired on component mount.
  }, []);

  return <Redirect to="/auth/login" />;
};

export default Logout;

Query Parameters

We can extract the query parameters in React with. The URLSearchParams code below is not well supported instead we need to use the npm package querystring

// const query = new URLSearchParams(props.location.search);
// const name = query.get('name')
// const occupation = query.get('occupation')

import * as querystring from "query-string";
const qsValues = querystring.parse(props.location.search);
const name = qsValues.name;
const occupation = qsValues.occupation

Routing and Redux

This did seem popular on the web so

Reminder of Redux

Here is a quick reminder of redux.

  • Views subscribe to changes in state in the store
  • State changes are passed to views as properties
  • Components dispatch action requests to action creators
  • Action creators return an action object
  • Actions are payloads sent to the reducer
  • The reducer is responsible for making the change in the store
  • The state is replaced as it is immutable

Redux reminder.png
To get the router to be used in redux was fairly straight forward

import {ConnectdRouter as Router, routerMiddleware} from "react-router-redux"
...
import createHistory from "history/createBrowserHistory"
import initialState from "./data/blahblah"

const history = createHistory()
const middleware = routerMiddleware(history)
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

...
<Provider store={store}>
    <Router history={history}>
        <App store={store} />
    </Router>
</Provide>

We need to add the route to the reducers to.

...
const rootReducer = combineReducers({
    app: appReducer,
    router: routeReducer,
})
...

Transitions

You need to provide a class prefix in this case trans.

import {TransitionGroup, CSSTransition} from 'react-transition-group'

<TransactionGroup>
  <CSSTransition key={location.key} classNames={'trans'} timeout={1000}>
    <Switch location={location}>
     <Route path={`${match.url}`} component={LoremNumber} exact/>
     <Route path={`${match.url}/:id`} component={LoremNumber} exact/>
    </Switch>
</TransactionGroup>

And the CSS

.trans-enter {
   opacity: 0;
   z-index: 1;
}

.trans-exit {
   display: none
}

.trans-enter.trans-enter-active {
   opacity: 1;
   transition: opacity 1000ms ease-in;
}