UI components in React can be seen as a bundle of HTML elements. Web developers often wonder whether it's possible to treat an entire component as a DOM element and reference it using id
or className
. Certainly React isn't built for that, but it does allow you to fetch components using their routes. This guide offers a concrete example that combines the power of React-Router and Context API to call components from anywhere in the app.
This example app allows a user to sign up and log in using the browser's localStorage
API. You'll learn how components are identified using routes and see the ease offered by the Context API in triggering explicit calls to these components.
Create a new react project by running:
1npx create-react-app react-router-context
Use bootstrap by adding the following line inside index.html:
1<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
Run the following command inside the root directory to install react-router-dom
:
1npm i react-router-dom
The app contains three routes for three different pages: Login
, Signup
, and Home
. The Home
component is what the user sees after successfully logging in. Consider the following code that sets up the routes for the app and renders one route at a time:
1import React from 'react';
2import {Route,BrowserRouter,Switch} from 'react-router-dom';
3import Home from '../components/Home';
4import Signup from '../components/Signup';
5import Login from '../components/Login';
6
7const Routes=()=>{
8 return(
9 <>
10 <BrowserRouter>
11 <Switch>
12 <Route exact path="/" component={Home}/>
13 <Route exact path="/signup" component={Signup}/>
14 <Route exact path="/login" component={Login}/>
15 </Switch>
16 </BrowserRouter>
17 </>
18 );
19}
20
21export default Routes;
At any point, you can call a component from anywhere in the app by referencing its route.
The Login
component contains a bootstrap form with two fields for the user's email and password and state
to store them. It also contains a handleChange()
method that sets the state
and a handleSubmit()
method that logs out this state
on the console.
1import React,{useState,useEffect} from 'react';
2import {Link} from 'react-router-dom';
3
4const Login=()=>{
5 const [loginDetails,setLoginDetails]=useState({
6 email:'',
7 password:''
8 })
9
10 const handleChange=(e)=>{
11 setLoginDetails({...loginDetails,[e.target.id]:e.target.value})
12 }
13
14 const handleSubmit=(e)=>{
15 e.preventDefault();
16 console.log(loginDetails)
17 }
18
19 return(
20 <>
21 <div className="container">
22 <div className="row">
23
24 <div className="col-4 mx-auto mt-5">
25 <div className="row my-3 justify-content-center">
26 <h4>Login</h4>
27 </div>
28 <div className="card">
29 <div className="card-body">
30 <form onChange={handleChange} onSubmit={handleSubmit}>
31 <div className="form-group">
32 <label htmlFor="email">Email address</label>
33 <input type="email" className="form-control" id="email" aria-describedby="emailHelp" placeholder="Enter email" />
34 </div>
35 <div className="form-group">
36 <label htmlFor="password">Password</label>
37 <input type="password" className="form-control" id="password" placeholder="Password" />
38 </div>
39 <div className="row justify-content-center my-3">
40 <Link to="/signup">Don't have an account? Signup</Link>
41 </div>
42 <div className="row justify-content-center">
43 <button type="submit" className="btn btn- primary">Submit</button>
44 </div>
45 </form>
46
47 </div>
48 </div>
49 </div>
50 </div>
51
52 </div>
53 </>
54 )
55}
56
57export default Login;
Similarly, create the Signup
component as shown below:
1import React,{useState,useContext} from 'react';
2import {Link} from 'react-router-dom';
3
4const Signup=()=>{
5 const [SignupDetails,setSignupDetails]=useState({
6 email:'',
7 password:'',
8 })
9
10 const handleChange=(e)=>{
11 setSignupDetails({...SignupDetails,[e.target.id]:e.target.value})
12 }
13
14 const handleSubmit=(e)=>{
15 e.preventDefault();
16 console.log(SignupDetails)
17 }
18
19 return(
20 <>
21 <div className="container">
22 <div className="row">
23
24 <div className="col-4 mx-auto mt-5">
25 <div className="row my-3 justify-content-center">
26 <h4>Signup</h4>
27 </div>
28 <div className="card">
29 <div className="card-body">
30 <form onChange={handleChange} onSubmit={handleSubmit}>
31
32 <div className="form-group">
33 <label htmlFor="email">Email address</label>
34 <input type="email" className="form-control" id="email" aria-describedby="emailHelp" placeholder="Enter email" />
35 </div>
36 <div className="form-group">
37 <label htmlFor="password">Password</label>
38 <input type="password" className="form-control" id="password" placeholder="Password" />
39 </div>
40 <div className="row justify-content-center my-3">
41 <Link to="/login">Already have an account? Login</Link>
42 </div>
43 <div className="row justify-content-center">
44 <button type="submit" className="btn btn-primary">Sign up</button>
45 </div>
46 </form>
47 </div>
48 </div>
49 </div>
50 </div>
51 </div>
52 </>
53 )
54}
55
56export default Signup;
Inside the Home
, the component displays the authenticated user's email with a logout button.
1import React fom 'react';
2
3const Home=()=>{
4 return(
5 <>
6 <div className="container">
7 <div className="row">
8 <div className="col-4 mx-auto mt-5">
9 <div className="card" style={{width: '18rem'}}>
10 <div className="card-body">
11 <h6 className="card-subtitle mb-2 text-muted">Email</h6>
12 <button className="btn btn-info">Logout</button>
13 </div>
14 </div>
15 </div>
16 </div>
17 </div>
18 </>
19 )
20}
21
22export default Home;
The context store communicates with your localStorage
and serves as a global state for your app. Components rendered as children using its provider have access to this global state.
To begin, create a context using the createContext()
hook and set up an initial state. Then create a provider, a function component that acts as a wrapper for all the components using the global state. Next, extract state
and dispatch
from the useReducer()
hook and pass your reducer as the first argument and your initial state initState
as the second argument. Finally, check your localStorage
. If it is empty, assign initState
to your global state; otherwise, push the existing data to it.
1import React, {createContext, useEffect, useReducer} from 'react';
2import {AuthReducer} from '../reducers/AuthReducer';
3
4export const UserContext=createContext();
5
6const initState={
7 isSignedUp:false,
8 isAuthenticated:false,
9 currentUser:null
10}
11
12 export const UserContextProvider=(props)=>{
13 const [state, dispatch] = useReducer(AuthReducer, initState,()=>{
14 const data=localStorage.getItem('state');
15 return data?JSON.parse(data):initState
16 });
17 useEffect(()=>{
18 localStorage.setItem('state',JSON.stringify(state));
19 },[state])
20 return(
21 <UserContext.Provider value={{state,dispatch}}>
22 {props.children}
23 </UserContext.Provider>
24 )
25}
Wrap your components inside UserContextProvider
.
1...
2<UserContextProvider>
3 <Route exact path="/" component={Home}/>
4 <Route exact path="/signup" component={Signup}/>
5 <Route exact path="/login" component={Login}/>
6</UserContextProvider>
7...
Actions inside the reducer work similar to actions in Redux. You can create cases here to determine the app flow. For instance, on submitting the sign-up form, you want to fire your SIGNUP
action to add the newly created user to your database (here, the browser's localStorage
). Inside the LOGIN
action, write down the logic to authenticate the user. And finally, in the LOGOUT
action, expire the user's session. Your reducer updates the global state based on which component you decide to render at what time.
1export const AuthReducer=(state,action)=>{
2 let allUsers=localStorage.getItem('users') ?
3 JSON.parse(localStorage.getItem('users')) : []
4 switch(action.type){
5 case "LOGIN": console.log('login')
6 if(allUsers.length==0) return {...state, msg:'User does not exist'};
7 let validLogin=false;
8 allUsers.forEach(user=>{
9 if(user.email===action.payload.user && user.password===action.payload.password){
10 validLogin=true;
11 return;
12 }
13 })
14 if(validLogin)
15 return{
16 ...state,
17 isSignedUp:true,
18 isAuthenticated:true,
19 currentUser:{
20 email:action.payload.user,
21 password:action.payload.password
22 },
23 msg:'Login successful!'
24 }
25 else
26 return{...state}
27
28 case "SIGNUP": console.log('signup')
29 allUsers.push({email:action.payload.user, password:action.payload.password})
30 localStorage.setItem('users',JSON.stringify(allUsers))
31 return {
32 ...state,
33 isSignedUp:true,
34 isAuthenticated:false,
35 currentUser:{
36 email:action.payload.user,
37 password:action.payload.password
38 },
39 msg:'Signed Up successfully! Login now.'
40 };
41
42 case "LOGOUT": console.log('logout');
43 return {...state,
44 isSignedUp:false,
45 isAuthenticated:false,
46 currentUser:null
47 };
48 default: return state;
49 }
50}
Import UserContext
on top of every component and grab state
and dispatch
using the useContext
hook as shown below:
1import { UserContext } from '../contexts/UserContext';
2
3const Login=()=>{
4 const {state,dispatch}=useContext(UserContext)
5 ....
6}
Inside your handleSubmit()
method, dispatch the needful actions for all your components.
1//dispatching action for Login
2dispatch({
3 type:"LOGIN",
4 payload:{
5 user:loginDetails.email,
6 password:loginDetails.password
7 }
8})
9
Remember to specify the type
and payload
object so your reducer can access these values and fire the correct action.
1//dispatching action for Signup
2dispatch({
3 type:"SIGNUP",
4 payload:{
5 user:SignupDetails.email,
6 password:SignupDetails.password
7 }
8})
Finally, inside your Home
component, dispatch the LOGOUT
action inside the Logout()
method.
1//dispatching action for logout
2
3const Logout=()=>{
4 dispatch({
5 type:'LOGOUT'
6 })
7 history.push('/login')
8}
Also, update your Home
component with data from your the context store.
1...
2 <h6 className="card-subtitle mb-2 text-muted"> {state.currentUser.email}</h6>
3...
Based on where the user is in your app flow, using the global state, you can explicitly re-route to a component. To change the current route, import the useHistory()
hook from react-router-dom
inside all your components.
1import {Link, useHistory} from 'react-router-dom';
2...
3 const history=useHistory();
4...
Now any component can be called by passing its route to the history.push()
method. You can get your Home
component for an authenticated user from anywhere in the app using the following check.
1if(state.isAuthenticated) history.push('/')
You can also protect your Home
component's route using a condition on your global state.
1if(!state.isAuthenticated && state.isSignedUp) history.push('/login')
Context allows you to easily manage your state, which you might need at any point in your app, without having to manually pass it down at each level as props. You can connect your routes to your global state and conditionally call a component from anywhere in the app, as demonstrated in this guide.