In intergalactic travels, it's important to track the route and have the ability to return to previous exploration points. In the world of React Router, this navigation system is the browser history - a mechanism that allows moving back and forth between visited pages. Let's see how to consciously manage history to increase the comfort of users' journeys through our application.
Browser history is a stack of URL addresses visited by the user. Browsers provide an API that allows:
React Router integrates with this API, providing hooks and components for managing history in a React-friendly way.
The
useNavigate hook is the primary tool for programmatic navigation in React Router:1import { useNavigate } from 'react-router-dom';
2
3function MissionControl() {
4 const navigate = useNavigate();
5
6 const goToExploration = () => {
7 navigate('/exploration');
8 };
9
10 const goBack = () => {
11 navigate(-1); // Go back one page
12 };
13
14 const goForward = () => {
15 navigate(1); // Go forward one page
16 };
17
18 const jumpToSpecificPoint = () => {
19 navigate(-3); // Go back three pages
20 };
21
22 return (
23 <div className="mission-control">
24 <h1>Mission Control Center</h1>
25
26 <div className="navigation-panel">
27 <button onClick={goToExploration}>Start Exploration</button>
28 <button onClick={goBack}>Return to previous location</button>
29 <button onClick={goForward}>Forward</button>
30 <button onClick={jumpToSpecificPoint}>Return to starting point</button>
31 </div>
32 </div>
33 );
34}When using
navigate, we can decide whether to add a new page to history or replace the current one:1// Add a new page to the history stack (default behavior)
2navigate('/new-location');
3
4// Replace the current page in history
5navigate('/new-location', { replace: true });When should you use
replace: true?Sometimes we need to prevent the user from leaving a page, for example when they have unsaved changes in a form. In React Router v6, there is no built-in navigation blocking mechanism, but we can implement one:
1import { useCallback, useEffect } from 'react';
2import { useNavigate, useLocation } from 'react-router-dom';
3
4function useBlockNavigation(blocker, when = true) {
5 const navigate = useNavigate();
6 const location = useLocation();
7
8 // Function presenting the user with a prompt
9 const handleBlockNavigation = useCallback((nextLocation) => {
10 if (
11 when &&
12 nextLocation.location.pathname !== location.pathname &&
13 !window.confirm('You have unsaved changes. Are you sure you want to leave this page?')
14 ) {
15 // User clicked "Cancel", prevent navigation
16 return false;
17 }
18 // User clicked "OK", allow navigation
19 return true;
20 }, [when, location]);
21
22 // Set up listening for the beforeunload event
23 useEffect(() => {
24 if (when) {
25 const handleBeforeUnload = (e) => {
26 e.preventDefault();
27 e.returnValue = '';
28 };
29
30 window.addEventListener('beforeunload', handleBeforeUnload);
31
32 return () => {
33 window.removeEventListener('beforeunload', handleBeforeUnload);
34 };
35 }
36 }, [when]);
37
38 // Set up custom blocking for React Router
39 useEffect(() => {
40 if (when) {
41 // Here we would use the blocker mechanism from React Router,
42 // but in v6 we need to use a workaround - e.g., window.history.pushState
43 const unblock = () => {
44 // Cleanup function
45 };
46
47 return unblock;
48 }
49 }, [when, handleBlockNavigation, navigate]);
50}
51
52// Usage:
53function SpaceshipConfigurator() {
54 const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
55 const [config, setConfig] = useState({});
56
57 useBlockNavigation(hasUnsavedChanges);
58
59 // Rest of the component...
60}In newer versions of React Router, you can use the official approach from the
@remix-run/router package.The
useLocation hook gives us access to the current location object, which contains information about the current URL:1import { useLocation } from 'react-router-dom';
2
3function LocationTracker() {
4 const location = useLocation();
5
6 return (
7 <div>
8 <h2>Current Cosmic Location</h2>
9 <p>Path: {location.pathname}</p>
10 <p>Query: {location.search}</p>
11 <p>Hash: {location.hash}</p>
12 <p>Key identifier: {location.key}</p>
13 <p>Navigation method: {location.state?.navigationMethod || 'Standard'}</p>
14 </div>
15 );
16}The
location.key field is particularly useful - it is a unique identifier for each entry in the history. We can use it to detect when the user returns to a page they have already visited.We can create our own hook for tracking navigation history:
1import { useState, useEffect } from 'react';
2import { useLocation } from 'react-router-dom';
3
4function useNavigationHistory(maxEntries = 10) {
5 const location = useLocation();
6 const [history, setHistory] = useState([]);
7
8 useEffect(() => {
9 setHistory(prev => {
10 // Add current location to history
11 const newHistory = [...prev, {
12 pathname: location.pathname,
13 search: location.search,
14 hash: location.hash,
15 key: location.key,
16 timestamp: Date.now()
17 }];
18
19 // Limit history size
20 return newHistory.slice(-maxEntries);
21 });
22 }, [location, maxEntries]);
23
24 return history;
25}
26
27// Usage:
28function NavigationLogger() {
29 const history = useNavigationHistory(5);
30
31 return (
32 <div className="navigation-log">
33 <h3>Travel Log</h3>
34 <ul>
35 {history.map((entry, index) => (
36 <li key={entry.key}>
37 {index === history.length - 1 ? '-> ' : ''}
38 {entry.pathname} (visited: {new Date(entry.timestamp).toLocaleTimeString()})
39 </li>
40 ))}
41 </ul>
42 </div>
43 );
44}Sometimes we want to change the default behavior of the browser's "Back" button. For example, if the user is on a page that can only be visited after logging in and clicks "Back", we can redirect them to a safe location:
1import { useEffect } from 'react';
2import { useNavigate, useLocation } from 'react-router-dom';
3
4function SecureZone() {
5 const navigate = useNavigate();
6 const location = useLocation();
7 const { isAuthenticated } = useAuth();
8
9 useEffect(() => {
10 // Check if the user is logged in on every location change
11 if (!isAuthenticated) {
12 navigate('/login', { replace: true });
13 }
14
15 // Modified "Back" button behavior
16 const handlePopState = () => {
17 if (!isAuthenticated) {
18 // If the user is not logged in and clicks "Back",
19 // redirect them to the home page instead of the previous page
20 navigate('/', { replace: true });
21 }
22 };
23
24 window.addEventListener('popstate', handlePopState);
25
26 return () => {
27 window.removeEventListener('popstate', handlePopState);
28 };
29 }, [isAuthenticated, navigate, location]);
30
31 return (
32 <div className="secure-zone">
33 <h1>Secured Zone</h1>
34 <p>Welcome to the Command Center. You have access to classified data.</p>
35 </div>
36 );
37}In more complex applications, we may want to synchronize browser history with global application state (e.g., Redux):
1import { useEffect } from 'react';
2import { useLocation } from 'react-router-dom';
3import { useDispatch } from 'react-redux';
4
5function HistorySync() {
6 const location = useLocation();
7 const dispatch = useDispatch();
8
9 useEffect(() => {
10 // Every time the location changes, we update the Redux state
11 dispatch({
12 type: 'navigation/locationChanged',
13 payload: {
14 pathname: location.pathname,
15 search: location.search,
16 hash: location.hash,
17 state: location.state
18 }
19 });
20 }, [location, dispatch]);
21
22 return null; // Component doesn't render anything
23}
24
25// Usage in App:
26function App() {
27 return (
28 <Router>
29 <HistorySync />
30 {/* Rest of the application */}
31 </Router>
32 );
33}We often need to analyze query parameters from the URL:
1import { useLocation } from 'react-router-dom';
2
3function useParsedQuery() {
4 const location = useLocation();
5 const queryParams = new URLSearchParams(location.search);
6
7 return {
8 get: (param) => queryParams.get(param),
9 has: (param) => queryParams.has(param),
10 getAll: (param) => queryParams.getAll(param),
11 toString: () => queryParams.toString(),
12 entries: () => Array.from(queryParams.entries()).reduce((acc, [key, value]) => {
13 acc[key] = value;
14 return acc;
15 }, {})
16 };
17}
18
19// Usage:
20function SpaceshipFilter() {
21 const query = useParsedQuery();
22 const typeFilter = query.get('type'); // e.g., 'exploration'
23 const minCrewSize = query.get('minCrew'); // e.g., '3'
24
25 return (
26 <div>
27 <h2>Spaceship Filtering</h2>
28 {typeFilter && <p>Filtering by type: {typeFilter}</p>}
29 {minCrewSize && <p>Minimum crew size: {minCrewSize}</p>}
30
31 {/* Rest of the component */}
32 </div>
33 );
34}Sometimes we want to check how deep in the browser history the user is, or perform an action after reaching a certain depth:
1import { useState, useEffect } from 'react';
2
3function useHistoryDepth() {
4 const [depth, setDepth] = useState(0);
5
6 useEffect(() => {
7 // Set initial depth
8 setDepth(window.history.length);
9
10 // Listen for changes
11 const handleNavigation = () => {
12 setDepth(window.history.length);
13 };
14
15 window.addEventListener('popstate', handleNavigation);
16
17 return () => {
18 window.removeEventListener('popstate', handleNavigation);
19 };
20 }, []);
21
22 return depth;
23}
24
25// Usage:
26function NavigationDepthIndicator() {
27 const depth = useHistoryDepth();
28
29 return (
30 <div className="depth-indicator">
31 <p>History depth: {depth}</p>
32 {depth > 10 && (
33 <button onClick={() => window.history.go(-5)}>
34 Go back 5 pages
35 </button>
36 )}
37 </div>
38 );
39}The standard browser behavior is to restore the scroll position when navigating back. In SPA applications, we often need to implement this manually:
1import { useEffect, useRef } from 'react';
2import { useLocation } from 'react-router-dom';
3
4function useScrollRestoration() {
5 const location = useLocation();
6 const scrollPositions = useRef({});
7
8 // Save scroll position on location change
9 useEffect(() => {
10 const saveScrollPosition = (key) => {
11 scrollPositions.current[key] = window.scrollY;
12 };
13
14 // Save current position when the user leaves the page
15 return () => {
16 saveScrollPosition(location.key);
17 };
18 }, [location]);
19
20 // Restore scroll position when the user returns to a page
21 useEffect(() => {
22 if (scrollPositions.current[location.key] !== undefined) {
23 // Restore saved position
24 window.scrollTo(0, scrollPositions.current[location.key]);
25 } else {
26 // New page, scroll to top
27 window.scrollTo(0, 0);
28 }
29 }, [location]);
30}
31
32// Usage:
33function ScrollRestoringPage() {
34 useScrollRestoration();
35
36 return (
37 <div className="long-content-page">
38 {/* Lots of content causing scrolling */}
39 </div>
40 );
41}Sometimes we need to know if specific URL parameters have changed:
1import { useEffect, useRef } from 'react';
2import { useLocation } from 'react-router-dom';
3
4function useQueryParamChange(paramName) {
5 const location = useLocation();
6 const previousParam = useRef(null);
7
8 // Get the current parameter value
9 const query = new URLSearchParams(location.search);
10 const currentValue = query.get(paramName);
11
12 // Check if the value has changed
13 const hasChanged = previousParam.current !== currentValue;
14
15 // Update the ref for the next render
16 useEffect(() => {
17 previousParam.current = currentValue;
18 }, [currentValue]);
19
20 return {
21 value: currentValue,
22 hasChanged,
23 previousValue: previousParam.current
24 };
25}
26
27// Usage:
28function GalaxyFilter() {
29 const { value: galaxyType, hasChanged } = useQueryParamChange('galaxy');
30
31 useEffect(() => {
32 if (hasChanged) {
33 // Perform action on galaxy type change
34 console.log(`Filter changed to: ${galaxyType}`);
35 // e.g., fetch new data
36 }
37 }, [galaxyType, hasChanged]);
38
39 return (
40 <div>
41 {/* Filtering components */}
42 </div>
43 );
44}Conscious management of browser history is like keeping a cosmic travel log - it allows us to:
Well-designed browser history handling makes the journey through our application intuitive and pleasant, just like a well-planned space mission.
Remember that browser history is not just a navigation mechanism, but also an important element of the user experience. Users expect the "Back" and "Forward" buttons to work intuitively, and your application should respect that.