How to Get a Component from Anywhere in an App Using React
Aug 18, 2020 • 15 Minute Read
Introduction
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.
Example App Overview and Setup
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.
Creating an Empty React Project
Create a new react project by running:
npx create-react-app react-router-context
Adding Bootstrap Styles
Use bootstrap by adding the following line inside index.html:
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
Installing React-Router
Run the following command inside the root directory to install react-router-dom:
npm i react-router-dom
Creating Routes
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:
import React from 'react';
import {Route,BrowserRouter,Switch} from 'react-router-dom';
import Home from '../components/Home';
import Signup from '../components/Signup';
import Login from '../components/Login';
const Routes=()=>{
return(
<>
<BrowserRouter>
<Switch>
<Route exact path="/" component={Home}/>
<Route exact path="/signup" component={Signup}/>
<Route exact path="/login" component={Login}/>
</Switch>
</BrowserRouter>
</>
);
}
export default Routes;
At any point, you can call a component from anywhere in the app by referencing its route.
Creating Components
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.
import React,{useState,useEffect} from 'react';
import {Link} from 'react-router-dom';
const Login=()=>{
const [loginDetails,setLoginDetails]=useState({
email:'',
password:''
})
const handleChange=(e)=>{
setLoginDetails({...loginDetails,[e.target.id]:e.target.value})
}
const handleSubmit=(e)=>{
e.preventDefault();
console.log(loginDetails)
}
return(
<>
<div className="container">
<div className="row">
<div className="col-4 mx-auto mt-5">
<div className="row my-3 justify-content-center">
<h4>Login</h4>
</div>
<div className="card">
<div className="card-body">
<form onChange={handleChange} onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">Email address</label>
<input type="email" className="form-control" id="email" aria-describedby="emailHelp" placeholder="Enter email" />
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input type="password" className="form-control" id="password" placeholder="Password" />
</div>
<div className="row justify-content-center my-3">
<Link to="/signup">Don't have an account? Signup</Link>
</div>
<div className="row justify-content-center">
<button type="submit" className="btn btn- primary">Submit</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</>
)
}
export default Login;
Similarly, create the Signup component as shown below:
import React,{useState,useContext} from 'react';
import {Link} from 'react-router-dom';
const Signup=()=>{
const [SignupDetails,setSignupDetails]=useState({
email:'',
password:'',
})
const handleChange=(e)=>{
setSignupDetails({...SignupDetails,[e.target.id]:e.target.value})
}
const handleSubmit=(e)=>{
e.preventDefault();
console.log(SignupDetails)
}
return(
<>
<div className="container">
<div className="row">
<div className="col-4 mx-auto mt-5">
<div className="row my-3 justify-content-center">
<h4>Signup</h4>
</div>
<div className="card">
<div className="card-body">
<form onChange={handleChange} onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">Email address</label>
<input type="email" className="form-control" id="email" aria-describedby="emailHelp" placeholder="Enter email" />
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input type="password" className="form-control" id="password" placeholder="Password" />
</div>
<div className="row justify-content-center my-3">
<Link to="/login">Already have an account? Login</Link>
</div>
<div className="row justify-content-center">
<button type="submit" className="btn btn-primary">Sign up</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</>
)
}
export default Signup;
Inside the Home, the component displays the authenticated user's email with a logout button.
import React fom 'react';
const Home=()=>{
return(
<>
<div className="container">
<div className="row">
<div className="col-4 mx-auto mt-5">
<div className="card" style={{width: '18rem'}}>
<div className="card-body">
<h6 className="card-subtitle mb-2 text-muted">Email</h6>
<button className="btn btn-info">Logout</button>
</div>
</div>
</div>
</div>
</div>
</>
)
}
export default Home;
Creating Global Context Store
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.
import React, {createContext, useEffect, useReducer} from 'react';
import {AuthReducer} from '../reducers/AuthReducer';
export const UserContext=createContext();
const initState={
isSignedUp:false,
isAuthenticated:false,
currentUser:null
}
export const UserContextProvider=(props)=>{
const [state, dispatch] = useReducer(AuthReducer, initState,()=>{
const data=localStorage.getItem('state');
return data?JSON.parse(data):initState
});
useEffect(()=>{
localStorage.setItem('state',JSON.stringify(state));
},[state])
return(
<UserContext.Provider value={{state,dispatch}}>
{props.children}
</UserContext.Provider>
)
}
Wrap your components inside UserContextProvider.
...
<UserContextProvider>
<Route exact path="/" component={Home}/>
<Route exact path="/signup" component={Signup}/>
<Route exact path="/login" component={Login}/>
</UserContextProvider>
...
Creating Reducer
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.
export const AuthReducer=(state,action)=>{
let allUsers=localStorage.getItem('users') ?
JSON.parse(localStorage.getItem('users')) : []
switch(action.type){
case "LOGIN": console.log('login')
if(allUsers.length==0) return {...state, msg:'User does not exist'};
let validLogin=false;
allUsers.forEach(user=>{
if(user.email===action.payload.user && user.password===action.payload.password){
validLogin=true;
return;
}
})
if(validLogin)
return{
...state,
isSignedUp:true,
isAuthenticated:true,
currentUser:{
email:action.payload.user,
password:action.payload.password
},
msg:'Login successful!'
}
else
return{...state}
case "SIGNUP": console.log('signup')
allUsers.push({email:action.payload.user, password:action.payload.password})
localStorage.setItem('users',JSON.stringify(allUsers))
return {
...state,
isSignedUp:true,
isAuthenticated:false,
currentUser:{
email:action.payload.user,
password:action.payload.password
},
msg:'Signed Up successfully! Login now.'
};
case "LOGOUT": console.log('logout');
return {...state,
isSignedUp:false,
isAuthenticated:false,
currentUser:null
};
default: return state;
}
}
Dispatching Actions
Import UserContext on top of every component and grab state and dispatch using the useContext hook as shown below:
import { UserContext } from '../contexts/UserContext';
const Login=()=>{
const {state,dispatch}=useContext(UserContext)
....
}
Inside your handleSubmit() method, dispatch the needful actions for all your components.
//dispatching action for Login
dispatch({
type:"LOGIN",
payload:{
user:loginDetails.email,
password:loginDetails.password
}
})
Remember to specify the type and payload object so your reducer can access these values and fire the correct action.
//dispatching action for Signup
dispatch({
type:"SIGNUP",
payload:{
user:SignupDetails.email,
password:SignupDetails.password
}
})
Finally, inside your Home component, dispatch the LOGOUT action inside the Logout() method.
//dispatching action for logout
const Logout=()=>{
dispatch({
type:'LOGOUT'
})
history.push('/login')
}
Also, update your Home component with data from your the context store.
...
<h6 className="card-subtitle mb-2 text-muted"> {state.currentUser.email}</h6>
...
Calling Components
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.
import {Link, useHistory} from 'react-router-dom';
...
const history=useHistory();
...
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.
if(state.isAuthenticated) history.push('/')
You can also protect your Home component's route using a condition on your global state.
if(!state.isAuthenticated && state.isSignedUp) history.push('/login')
Conclusion
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.