How to handle multiple Contexts using React Context API? - reactjs

I have a question about React's Context API. My coding level with React is beginner.
I am building an application that has 8 contexts and they may multiply in the future of the project. They are basic CRUD contexts for the different elements of my application without much complexity.
As I am writing my application I notice that a nested context hell is created in my App.js
To give more information I will explain a portion of the app. I have a Context for CRUD actions for Coaches, Athletes, Courts etc.
In my folder structure under /src directory I have a /context directory and inside I have a separate folder for each entity. Let's take Coaches as an example. In the /src/context/coach directory I have 3 files. A coachContext.js, a coachReducer.js and a CoachState.js
Contents of coachContext.js file:
import { createContext } from "react";
const coachContext = createContext();
export default coachContext;
Contents of coachReducer.js file:
const coachReducer = (state, action) => {
switch (action.type) {
case "GET_COACHES":
return {
...state,
coaches: action.payload,
};
case "SET_CURRENT_COACH":
return {
...state,
coach: action.payload,
loading: false,
};
default:
return state;
}
};
export default coachReducer;
Contents of CoachState.js file:
import { useReducer } from "react";
import coachContext from "./coachContext";
import coachReducer from "./coachReducer";
const CoachState = (props) => {
const initialState = {
coaches: [],
coach: [],
loading: false,
};
const [state, dispatch] = useReducer(coachReducer, initialState);
// Function to Add coach
// Function to Delete coach
// Function to Set current coach
// Function to clear current coach
// Function to Update coach
return (
<coachContext.Provider
value={{
coaches: state.coaches,
coach: state.coach,
loading: state.loading,
}}
>
{props.children}
</coachContext.Provider>
);
};
export default CoachState;
The same goes for Athletes context, Courts context and all other elements of my application.
Finally, in my App.js I have:
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Home from "./pages/Home";
import Coaches from "./pages/Coaches";
import Athletes from "./pages/Athletes";
import Courts from "./pages/Courts";
import CoachState from "./context/coach/CoachState";
import AthleteState from "./context/athlete/AthleteState";
import CourtState from "./context/court/CourtState";
function App() {
return (
<CourtState>
<AthleteState>
<CoachState>
<Router>
<Switch>
<Route exact path="/" component={Home}></Route>
<Route exact path="/coaches" component={Coaches}></Route>
<Route exact path="/athletes" component={Athletes}></Route>
<Route exact path="/courts" component={Courts}></Route>
</Switch>
</Router>
</CoachState>
</AthleteState>
</CourtState>
);
}
export default App;
When I finish writing my other Contexts as you can understand they will wrap the Router as all current states do. So there is going to be a big nesting "problem".
I would like any advice as to how could I resolve this nested contexts issue? Did I make the correct decision of developing my app using Context API instead of Redux?

Instead of using multiple context providers then useContext to get each value from each context provider, you can add all of the needed values within one context provider then use a custom hook to fetch the data or function that you need
this decreases the amount of context providers used, it doesn't decrease them to 1 provider since not all logic is going to be shared or common within one provider and another
I have used Kent C. Dodds' blog post "How to use React Context effectively" as a reference to write context providers efficiently.
example: (basic counter example but I'll explain the workflow)
const MainContext = createContext(null);
const MyComponent = (props) => {
const [counter, updateCounter] = useState(0);
const increment = () => {
updateCounter(counter + 1);
}
const decrement = () => {
updateCounter(counter - 1);
}
return(
<MainContext.Provider value={{counter, increment, decrement}}>
{children}
</MainContext.Provider>
)
}
const useCountNumber = () => {
const context = useContext(MainContext);
if(context === undefined || context === null) {
throw new Error('useCounter is not within MainContext scope');
}
else {
return context.counter;
}
}
const useIncrementCount = () => {
const context = useContext(MainContext);
if(context === undefined || context === null) {
throw new Error('useIncrementCount is not within MainContext scope');
}
else {
return context.increment;
}
}
const useDecrementCount = () => {
const context = useContext(MainContext);
if(context === undefined || context === null) {
throw new Error('useDecrementCount is not within MainContext scope');
}
else {
return context.decrement;
}
}
// in component you wish to use those values
const MyCounter = () => {
const count = useCountNumber();
const increment = useIncrementCount();
const decrement = useDecrementCount();
return(
<div>
{count}
<button onClick={increment}> +1 </button>
<button onClick={decrement}> -1 </button>
</div>
);
}
I have used this in production, use one context provider and you put values inside of that single provider. This is manageable for a small set of functions but as it gets bigger then I would recommend to use something like redux or another state management library
Also consider using useMemo for memoizing some state elements and useReducer to utilize a function to optimize performance of your context if it is triggering deep updates

You could use this npm package react-pipeline-component
Your code would be like this:
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Home from "./pages/Home";
import Coaches from "./pages/Coaches";
import Athletes from "./pages/Athletes";
import Courts from "./pages/Courts";
import CoachState from "./context/coach/CoachState";
import AthleteState from "./context/athlete/AthleteState";
import CourtState from "./context/court/CourtState";
import {Pipeline, Pipe} from 'react-pipeline-component'
function App() {
return (
<Pipeline components={[
<CourtState children={<Pipe />} />,
<AthleteState children={<Pipe />} />,
<CoachState children={<Pipe />} />,
<Router children={<Pipe />} />,
<Switch children={<Pipe />} />,
<>
<Route exact path="/" component={Home}></Route>
<Route exact path="/coaches" component={Coaches}></Route>
<Route exact path="/athletes" component={Athletes}></Route>
<Route exact path="/courts" component={Courts}></Route>
</>
]}/>
);
}
export default App;

Related

Problem with separating ContextProvider from app.jsx

I just started using createContext/useContext and after successfuly implementing it I would like now to put it in seperate file. The problem is that i am getting error of too many re-renders and I don't really know whats the problem. Could you give me a clue what might be wrong? Below I have put context code,app.js and example of component where context is actually used.
***context***
import React, { createContext, useState } from "react";
export const DarkModeContext = createContext({
isDarkMode: false,
toggleIsDarkMode: () => {},
});
export const DarkModeContextProvider = ({ children }) => {
const [isDarkMode, setIsDarkMode] = useState(false);
const toggleIsDarkMode = setIsDarkMode((prev) => !prev);
const value = {
isDarkMode,
toggleIsDarkMode,
};
return (
<DarkModeContext.Provider value={value}>
{children}
</DarkModeContext.Provider>
);
};
*** App.js***
import { useContext } from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Navbar from "../components/Navbar";
import MainSection from "../components/MainSection/MainSection";
import CountryPage from "../components/CountryPage";
import { Wrapper } from "./App.styles";
import { DarkModeContext, DarkModeContextProvider } from "./DarkModeContext";
function App() {
const { isDarkMode } = useContext(DarkModeContext);
return (
<DarkModeContextProvider>
<Router>
<Wrapper dark={isDarkMode}>
<Navbar />
<Routes>
<Route path="/" element={<MainSection />}></Route>
<Route
path="/country/:countryCode"
element={<CountryPage />}
></Route>
</Routes>
</Wrapper>
</Router>
</DarkModeContextProvider>
);
}
***part of component***
import {useContext} from "react"
import { DarkModeContext } from "../../App/DarkModeContext";
export default function CountryPage() {
const { isDarkMode } = useContext(DarkModeContext);
return (
<CountryPageWrapper dark={isDarkMode}>
<StyledLink dark={isDarkMode} to="/">
What I'm noticing is that in DarkModeContextProvider you are calling setIsDarkMode on every render rather than wrapping that method in another function (you should also be passing the boolean value and not a function updating it). Try updating that line to:
const toggleIsDarkMode = () => { setIsDarkMode(!isDarkMode) };
Assuming you meant the toggler to be a function, this should prevent your re-rendering loop as the setter is only getting called when the toggle is called into action.
You can't create a context provider and consume its value in the same component.
For example in App right now you have this:
// ...
const { isDarkMode } = useContext(DarkModeContext);
return (
<DarkModeContextProvider>
<Router>
<Wrapper dark={isDarkMode}>
// ...
This doesn't work, because where is useContext(DarkModeContext) getting the value of its context from? It hasn't been rendered yet, until you render <DarkModeContextProvider> which happens after trying to get the value of isDarkMode from the context. This is backwards.
One easy way to to fix this could be to simply move your <DarkModeContextProvider> out of App and into where you render <App />. I assume you're using React DOM somewhere to render your app, so where you are using that try something like this:
ReactDOM.render((
<DarkModeContextProvider>
<App />
</DarkModeContextProvider>
), document.getElementById('root'));

NextJS: Context values undefined in production (works fine in development)

A "dark mode" feature has been implemented on my Next.js application using React's Context api.
Everything works fine during development, however, Context provider-related problems have arisen on the built version — global states show as undefined and cannot be handled.
_app.tsx is wrapped with the ThemeProvider as such:
// React & Next hooks
import React, { useEffect } from "react";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
// Irrelevant imports
// Global state management
import { Provider } from "react-redux";
import store from "../redux/store";
import { AuthProvider } from "../context/UserContext";
import { ThemeProvider } from "../context/ThemeContext";
// Components
import Layout from "../components/Layout/Layout";
import Footer from "../components/Footer/Footer";
// Irrelevant code
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter();
// Applying different layouts depending on page
switch (Component.name) {
case "HomePage":
return (
<Provider store={store}>
<ThemeProvider>
<AuthProvider>
<Component {...pageProps} />
<Footer color="fff" />
</AuthProvider>
</ThemeProvider>
</Provider>
);
case "PageNotFound":
return (
<>
<Component {...pageProps} />
<Footer color="#f2f2f5" />
</>
);
default:
// Irrelevant code
}
}
export default MyApp;
The ThemeContext correctly exports both its Provider and Context:
import { createContext, ReactNode, useState, useEffect } from "react";
type themeContextType = {
darkMode: boolean | null;
toggleDarkMode: () => void;
};
type Props = {
children: ReactNode;
};
// Checks for user's preference.
const getPrefColorScheme = () => {
return !window.matchMedia
? null
: window.matchMedia("(prefers-color-scheme: dark)").matches;
};
// Gets previously stored theme if it exists.
const getInitialMode = () => {
const isReturningUser = "dark-mode" in localStorage; // Returns true if user already used the website.
const savedMode = localStorage.getItem("dark-mode") === "true" ? true : false;
const userPrefersDark = getPrefColorScheme(); // Gets user's colour preference.
// If mode was saved ► return saved mode else get users general preference.
return isReturningUser ? savedMode : userPrefersDark ? true : false;
};
export const ThemeContext = createContext<themeContextType>(
{} as themeContextType
);
export const ThemeProvider = ({ children }: Props) => {
// localStorage only exists on the browser (window), not on the server
const [darkMode, setDarkMode] = useState<boolean | null>(null);
// Getting theme from local storage upon first render
useEffect(() => {
setDarkMode(getInitialMode);
}, []);
// Prefered theme stored in local storage
useEffect(() => {
localStorage.setItem("dark-mode", JSON.stringify(darkMode));
}, [darkMode]);
const toggleDarkMode = () => {
setDarkMode(!darkMode);
};
return (
<ThemeContext.Provider value={{ darkMode, toggleDarkMode }}>
{children}
</ThemeContext.Provider>
);
};
The ThemeToggler responsible for updating the darkMode state operates properly during development (theme toggled and correct value console.loged upon clicking), however it doesn't do anything during production (console.logs an undefined state):
import React, { FC, useContext } from "react";
import { ThemeContext } from "../../context/ThemeContext";
const ThemeToggler: FC = () => {
const { darkMode, toggleDarkMode } = useContext(ThemeContext);
const toggleTheme = () => {
console.log(darkMode) // <--- darkMode is undefined during production
toggleDarkMode();
};
return (
<div className="theme-toggler">
<i
className={`fas ${darkMode ? "fa-sun" : "fa-moon"}`}
data-testid="dark-mode"
onClick={toggleTheme}
></i>
</div>
);
};
export default ThemeToggler;
The solutions/suggestions I've looked up before posting this were to no avail.
React Context API undefined in production — react and react-dom are on the same version.
Thanks in advance.
P.S. For those wondering why I am using both Redux and Context for global state management:
Context is best suited for low-frequency and simple state updates such as themes and authentication.
Redux is better for high-frequency and complex state updates in addition to providing a better debugging tool — Redux DevTools.
P.S.2 Yes, it is better – performance-wise – to install FontAwesome's dependencies rather than use a CDN.
Thanks for sharing the code. It's well written. By reading it i don't see any problem. Based on your component topology, as long as your ThemeToggler is defined under any page component, your darkMode can't be undefined.
Here's your topology of the site
<MyApp>
<Provider>
// A. will not work
<ThemeProvider>
<HomePage>
// B. should work
</HomePage>
</ThemeProvider>
// C. will not work
</Provider>
</MyApp>
Although your ThemeProvider is a custom provider, inside ThemeContext.Provider is defined with value {{ darkMode, toggleDarkMode }}. So in theory you can't get undefined unless your component ThemeToggler is not under a HomePage component. I marked two non working locations, any component put under location A or C will give you undefined.
Since you have a condition for HomePage, you can run into this problem if you are on other pages. So in general you should wrap the ThemeProvider on top of your router.
<ThemeProvider>
<AuthProvider>
{Component.name != "PageNotFound" && (
<Component {...pageProps} />
)}
</AuthProvider>
</ThemeProvider>
You get the point, you want to first go through a layer that theme always exist before you fire up a router.
You can confirm if this is the case by doing the following test.
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</ThemeProvider>
)
}
If this works in production, then it confirms it. To be honest, this problem also exists in the dev, however maybe due to your routes change too quickly, it normally hides these issues.

Update site immediately after setting Local Storage

I am making a mern stack application and currently I am trying to switch between the login route and main page depending if you are logged in or not. However, this only works once I refresh the page, is there any way I can make it work without having to refresh the page?
App.js
{!localStorage.getItem('token') ? (
<Redirect exact from='/' to='/login' />
):
<>
<Navbar />
<Redirect to='/' />
</>
}
Reacting to changes in local storage is -at best- a weird approach. In practice, the only way for a component to re-render, is by the props that it receives to change, or by using component state via useState.
I'll write this imaginary piece of code to illustrate my point:
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom'
// ...
const LoginPage = _props {
const [token, setToken] = useState(localStorage.getItem('token'))
if (token) {
return <Redirect to='/' />
}
// I have no idea how you login your users
return (
<div>
<LoginForm onToken={setToken} />
</div>
)
}
If you need component A to react to changes done by component B, where neither of them is a direct child of the other, you will need global state.
Global state is similar to component state in that changes on it should trigger a re-render on the component that depends on it. But it is global, not local to a particular component.
To achieve this, there are complex solutions like redux, but you can implement a very simple version of it using a React Context:
// src/providers/GlobalStateProvider.js
import React, { createContext, useContext, useState } from 'react'
const Context = createContext()
const GlobalStateProvider = ({ children }) => {
const [token, doSetToken] = useState(localStorage.getItem('token'))
const setToken = t => {
doSetToken(t)
localStorage.setItem('token', token)
}
return (
<Context.Provider value={{ token, setToken }}>
{children}
</Context>
)
}
export { Context }
export default GlobalStateProvider
// src/App.js
import GlobalStateProvider from './providers/GlobalStateProvider'
// ...
const App = _props => {
return (
{/* Any component that is descendant of this one can access the context values, an will re-render if they change */}
<GlobalStateProvider>
{/* ... the rest of your components */}
</GlobalStateProvider>
)
}
// ...
// your particular component
import React, { useContext } from 'react'
import { Context } from 'path/to/GlobalStateProvider'
const SomeComponent = _props => {
// Component will re-render if token changes
// you can change token from wherever by using `setToken`
const { token, setToken } = useContext(Context)
if (token) {
// do this
} else {
// do that
}
}

Share API data with other components by using React Hook, NO REDUX

I'm experimenting with Hooks without any state management tool (such as Redux), to get the same kind of behavior/structure I could have by using a traditional structure of classes + redux.
Usually, with a class base code I would:
ComponentDidMount dispatch to Call the API
Use actions and reducers to store the data in Redux
Share the data to any component I want by using mapStateToProps
And here where the problem is by using Hooks without Redux: 'Share the DATA with any component'.
The following example is the way I have found to share states between components by Hooks:
//app.js
import React, { useReducer } from 'react'
import { BrowserRouter } from 'react-router-dom'
import Routes from '../../routes'
import Header from '../Shared/Header'
import Footer from '../Shared/Footer'
export const AppContext = React.createContext();
// Set up Initial State
const initialState = {
userType: '',
};
function reducer(state, action) {
switch (action.type) {
case 'USER_PROFILE_TYPE':
return {
userType: action.data === 'Student' ? true : false
};
default:
return initialState;
}
}
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<BrowserRouter>
<AppContext.Provider value={{ state, dispatch }}>
<Header userType={state.userType} />
<Routes />
<Footer />
</AppContext.Provider>
</BrowserRouter>
)
}
export default App
// profile.js
import React, { useEffect, useState, useContext} from 'react'
import { URLS } from '../../../constants'
import ProfileDeleteButton from './ProfileDeleteButton'
import DialogDelete from './DialogDelete'
import api from '../../../helpers/API';
// Import Context
import { AppContext } from '../../Core'
const Profile = props => {
// Share userType State
const {state, dispatch} = useContext(AppContext);
const userType = type => {
dispatch({ type: 'USER_PROFILE_TYPE', data: type }); <--- Here the action to call the reducer in the App.js file
};
// Profile API call
const [ profileData, setProfileData ] = useState({});
useEffect(() => {
fetchUserProfile()
}, [])
const fetchUserProfile = async () => {
try {
const data = await api
.get(URLS.PROFILE);
const userAttributes = data.data.data.attributes;
userType(userAttributes.type) <--- here I am passing the api response
}
catch ({ response }) {
console.log('THIS IS THE RESPONSE ==> ', response.data.errors);
}
}
etc.... not important what's happening after this...
now, the only way for me to see the value of userType is to pass it as a prop to the <Header /> component.
// app.js
<BrowserRouter>
<AppContext.Provider value={{ state, dispatch }}>
<Header userType={state.userType} /> <--passing here the userType as prop
<Routes />
<Footer />
</AppContext.Provider>
</BrowserRouter>
Let's say that I want to pass that userType value to children of <Routes />.
Here an example:
<AppContext.Provider value={{ state, dispatch }}>
<Routes userType={state.userType} />
</AppContext.Provider>
and then, inside <Routes /> ...
const Routes = () =>
<Switch>
<PrivateRoute exact path="/courses" component={Courses} userType={state.userType} />
</Switch>
I don't like it. It's not clean, sustainable or scalable.
Any suggestions on how to make the codebase better?
Many thanks
Joe
You don't need to pass the state as a prop to every component. Using context you can gain access to state properity in your reducer inside every child component of parent Provider. Like you have already done in the Profile.js
const {state, dispatch} = useContext(AppContext);
State property here contains state property in the reducer. So you can gain access to it by state.userType
Everything you need is within your context.
The only changes I would make is spread the data instead of trying to access it one at a time something like this
<AppContext.Provider value={{ ....state, dispatch }}>
then use const context = useContext(AppContext) within the component you need to access the data.
context.userType

Detect Route Change with react-router

I have to implement some business logic depending on browsing history.
What I want to do is something like this:
reactRouter.onUrlChange(url => {
this.history.push(url);
});
Is there any way to receive a callback from react-router when the URL gets updated?
You can make use of history.listen() function when trying to detect the route change. Considering you are using react-router v4, wrap your component with withRouter HOC to get access to the history prop.
history.listen() returns an unlisten function. You'd use this to unregister from listening.
You can configure your routes like
index.js
ReactDOM.render(
<BrowserRouter>
<AppContainer>
<Route exact path="/" Component={...} />
<Route exact path="/Home" Component={...} />
</AppContainer>
</BrowserRouter>,
document.getElementById('root')
);
and then in AppContainer.js
class App extends Component {
componentWillMount() {
this.unlisten = this.props.history.listen((location, action) => {
console.log("on route change");
});
}
componentWillUnmount() {
this.unlisten();
}
render() {
return (
<div>{this.props.children}</div>
);
}
}
export default withRouter(App);
From the history docs:
You can listen for changes to the current location using
history.listen:
history.listen((location, action) => {
console.log(`The current URL is ${location.pathname}${location.search}${location.hash}`)
console.log(`The last navigation action was ${action}`)
})
The location object implements a subset of the window.location
interface, including:
**location.pathname** - The path of the URL
**location.search** - The URL query string
**location.hash** - The URL hash fragment
Locations may also have the following properties:
location.state - Some extra state for this location that does not reside in the URL (supported in createBrowserHistory and
createMemoryHistory)
location.key - A unique string representing this location (supported
in createBrowserHistory and createMemoryHistory)
The action is one of PUSH, REPLACE, or POP depending on how the user
got to the current URL.
When you are using react-router v3 you can make use of history.listen() from history package as mentioned above or you can also make use browserHistory.listen()
You can configure and use your routes like
import {browserHistory} from 'react-router';
class App extends React.Component {
componentDidMount() {
this.unlisten = browserHistory.listen( location => {
console.log('route changes');
});
}
componentWillUnmount() {
this.unlisten();
}
render() {
return (
<Route path="/" onChange={yourHandler} component={AppContainer}>
<IndexRoute component={StaticContainer} />
<Route path="/a" component={ContainerA} />
<Route path="/b" component={ContainerB} />
</Route>
)
}
}
Update for React Router 5.1+.
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function SomeComponent() {
const location = useLocation();
useEffect(() => {
console.log('Location changed');
}, [location]);
...
}
react-router v6
In react-router v6, this can be done by combining the useLocation and useEffect hooks
import { useLocation } from 'react-router-dom';
const MyComponent = () => {
const location = useLocation()
React.useEffect(() => {
// runs on location, i.e. route, change
console.log('handle route change here', location)
}, [location])
...
}
For convenient reuse, you can do this in a custom useLocationChange hook
// runs action(location) on location, i.e. route, change
const useLocationChange = (action) => {
const location = useLocation()
React.useEffect(() => { action(location) }, [location])
}
const MyComponent1 = () => {
useLocationChange((location) => {
console.log('handle route change here', location)
})
...
}
const MyComponent2 = () => {
useLocationChange((location) => {
console.log('and also here', location)
})
...
}
If you also need to see the previous route on change, you can combine with a usePrevious hook
const usePrevious = (value) => {
const ref = React.useRef()
React.useEffect(() => { ref.current = value })
return ref.current
}
const useLocationChange = (action) => {
const location = useLocation()
const prevLocation = usePrevious(location)
React.useEffect(() => {
action(location, prevLocation)
}, [location])
}
const MyComponent1 = () => {
useLocationChange((location, prevLocation) => {
console.log('changed from', prevLocation, 'to', location)
})
...
}
It's important to note that all the above fire on the first client route being mounted, as well as subsequent changes. If that's a problem, use the latter example and check that a prevLocation exists before doing anything.
If you want to listen to the history object globally, you'll have to create it yourself and pass it to the Router. Then you can listen to it with its listen() method:
// Use Router from react-router, not BrowserRouter.
import { Router } from 'react-router';
// Create history object.
import createHistory from 'history/createBrowserHistory';
const history = createHistory();
// Listen to history changes.
// You can unlisten by calling the constant (`unlisten()`).
const unlisten = history.listen((location, action) => {
console.log(action, location.pathname, location.state);
});
// Pass history to Router.
<Router history={history}>
...
</Router>
Even better if you create the history object as a module, so you can easily import it anywhere you may need it (e.g. import history from './history';
This is an old question and I don't quite understand the business need of listening for route changes to push a route change; seems roundabout.
BUT if you ended up here because all you wanted was to update the 'page_path' on a react-router route change for google analytics / global site tag / something similar, here's a hook you can now use. I wrote it based on the accepted answer:
useTracking.js
import { useEffect } from 'react'
import { useHistory } from 'react-router-dom'
export const useTracking = (trackingId) => {
const { listen } = useHistory()
useEffect(() => {
const unlisten = listen((location) => {
// if you pasted the google snippet on your index.html
// you've declared this function in the global
if (!window.gtag) return
window.gtag('config', trackingId, { page_path: location.pathname })
})
// remember, hooks that add listeners
// should have cleanup to remove them
return unlisten
}, [trackingId, listen])
}
You should use this hook once in your app, somewhere near the top but still inside a router. I have it on an App.js that looks like this:
App.js
import * as React from 'react'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import Home from './Home/Home'
import About from './About/About'
// this is the file above
import { useTracking } from './useTracking'
export const App = () => {
useTracking('UA-USE-YOURS-HERE')
return (
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
)
}
// I find it handy to have a named export of the App
// and then the default export which wraps it with
// all the providers I need.
// Mostly for testing purposes, but in this case,
// it allows us to use the hook above,
// since you may only use it when inside a Router
export default () => (
<BrowserRouter>
<App />
</BrowserRouter>
)
I came across this question as I was attempting to focus the ChromeVox screen reader to the top of the "screen" after navigating to a new screen in a React single page app. Basically trying to emulate what would happen if this page was loaded by following a link to a new server-rendered web page.
This solution doesn't require any listeners, it uses withRouter() and the componentDidUpdate() lifecycle method to trigger a click to focus ChromeVox on the desired element when navigating to a new url path.
Implementation
I created a "Screen" component which is wrapped around the react-router switch tag which contains all the apps screens.
<Screen>
<Switch>
... add <Route> for each screen here...
</Switch>
</Screen>
Screen.tsx Component
Note: This component uses React + TypeScript
import React from 'react'
import { RouteComponentProps, withRouter } from 'react-router'
class Screen extends React.Component<RouteComponentProps> {
public screen = React.createRef<HTMLDivElement>()
public componentDidUpdate = (prevProps: RouteComponentProps) => {
if (this.props.location.pathname !== prevProps.location.pathname) {
// Hack: setTimeout delays click until end of current
// event loop to ensure new screen has mounted.
window.setTimeout(() => {
this.screen.current!.click()
}, 0)
}
}
public render() {
return <div ref={this.screen}>{this.props.children}</div>
}
}
export default withRouter(Screen)
I had tried using focus() instead of click(), but click causes ChromeVox to stop reading whatever it is currently reading and start again where I tell it to start.
Advanced note: In this solution, the navigation <nav> which inside the Screen component and rendered after the <main> content is visually positioned above the main using css order: -1;. So in pseudo code:
<Screen style={{ display: 'flex' }}>
<main>
<nav style={{ order: -1 }}>
<Screen>
If you have any thoughts, comments, or tips about this solution, please add a comment.
import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import Sidebar from './Sidebar';
import Chat from './Chat';
<Router>
<Sidebar />
<Switch>
<Route path="/rooms/:roomId" component={Chat}>
</Route>
</Switch>
</Router>
import { useHistory } from 'react-router-dom';
function SidebarChat(props) {
**const history = useHistory();**
var openChat = function (id) {
**//To navigate**
history.push("/rooms/" + id);
}
}
**//To Detect the navigation change or param change**
import { useParams } from 'react-router-dom';
function Chat(props) {
var { roomId } = useParams();
var roomId = props.match.params.roomId;
useEffect(() => {
//Detect the paramter change
}, [roomId])
useEffect(() => {
//Detect the location/url change
}, [location])
}
Use the useLocation() Hook to detect the URL change and put it in dependency array in useEffect() this trick worked for me
const App = () => {
const location = useLocation();
useEffect(() => {
window.scroll(0,0);
}, [location]);
return (
<React.Fragment>
<Routes>
<Route path={"/"} element={<Template/>} >
<Route index={true} element={<Home/>} />
<Route path={"cart"} element={<Cart/>} />
<Route path={"signin"} element={<Signin/>} />
<Route path={"signup"} element={<Signup/>} />
<Route path={"product/:slug"} element={<Product/>} />
<Route path={"category/:category"} element={<ProductList/>} />
</Route>
</Routes>
</React.Fragment>
);
}
export default App;
You can use the useLocation with componentDidUpdate for getting the route change for class component and useEffect for functional component
In Class component
import { useLocation } from "react-router";
class MainApp extends React.Component {
constructor(props) {
super(props);
}
async componentDidUpdate(prevProps) {
if(this.props.location.pathname !== prevProps.location.pathname)
{
//route has been changed. do something here
}
}
}
function App() {
const location = useLocation()
return <MainApp location={location} />
}
In functional component
function App() {
const location = useLocation()
useEffect(() => {
//route change detected. do something here
}, [location]) //add location in dependency. It detects the location change
return <Routes>
<Route path={"/"} element={<Home/>} >
<Route path={"login"} element={<Login/>} />
</Routes>
}
React Router V5
If you want the pathName as a string ('/' or 'users'), you can use the following:
// React Hooks: React Router DOM
let history = useHistory();
const location = useLocation();
const pathName = location.pathname;

Resources