In the spaceship command center, we sometimes need to navigate between different locations not only using buttons and links, but also programmatically - based on specific conditions, events, or application logic. React Router provides advanced tools for such programmatic navigation, allowing you to control the user's route from the code level.
Programmatic navigation is a technique for changing the URL address and rendered view using JavaScript code, without direct user interaction with links or buttons. It is an essential element of many application scenarios:
In React Router, the main tool for programmatic navigation is the
useNavigate hook.The
useNavigate hook introduced in React Router v6 provides a function that allows programmatically navigating to other paths in the application:1import { useNavigate } from 'react-router-dom';
2
3function LaunchControlPanel() {
4 const navigate = useNavigate();
5
6 const handleMissionStart = () => {
7 // Mission start logic...
8 console.log('Mission started!');
9
10 // After starting the mission, redirect to the monitoring panel
11 navigate('/mission/monitoring');
12 };
13
14 return (
15 <div className="launch-control">
16 <h2>Launch Control Panel</h2>
17 <button
18 className="launch-button"
19 onClick={handleMissionStart}
20 >
21 Start Mission
22 </button>
23 </div>
24 );
25}The
navigate function can accept different arguments:1// Navigate to a specific path
2navigate('/planets/mars');
3
4// Relative navigation (one level up)
5navigate('..');
6
7// Relative navigation (to a sub-path)
8navigate('systems');1// Go back one page (like the browser's "back" button)
2navigate(-1);
3
4// Go forward one page (like the browser's "forward" button)
5navigate(1);
6
7// Go back two pages
8navigate(-2);1// Navigation with options
2navigate('/planets/mars', {
3 replace: true, // Replaces the current history entry instead of adding a new one
4 state: { prevLocation: '/dashboard' } // State data to pass
5});
6
7// Navigation with search parameters
8navigate({
9 pathname: '/planets',
10 search: '?type=rocky&habitable=true'
11});By default,
navigate adds a new entry to the browser history. This means the user can return to the previous page using the "back" button. Sometimes, however, we want to replace the current entry so the user can't go back (e.g., after logging out):1function LogoutButton() {
2 const navigate = useNavigate();
3
4 const handleLogout = () => {
5 // Log out the user
6 logout();
7
8 // Redirect to the login page, replacing the current entry in history
9 navigate('/login', { replace: true });
10 };
11
12 return <button onClick={handleLogout}>Logout</button>;
13}We can pass state data to a new location using the
state option. This data is not visible in the URL, but is available in the target component:1// Source component
2function PlanetsList() {
3 const navigate = useNavigate();
4
5 const viewPlanetDetails = (planet) => {
6 navigate(`/planets/${planet.id}`, {
7 state: {
8 planetData: planet,
9 viewedFrom: 'planetsList',
10 timestamp: Date.now()
11 }
12 });
13 };
14
15 return (
16 <div>
17 {planets.map(planet => (
18 <div key={planet.id} onClick={() => viewPlanetDetails(planet)}>
19 {planet.name}
20 </div>
21 ))}
22 </div>
23 );
24}
25
26// Target component
27function PlanetDetails() {
28 const { state } = useLocation();
29 const { planetId } = useParams();
30
31 useEffect(() => {
32 if (state?.viewedFrom) {
33 console.log(`Redirected from: ${state.viewedFrom}`);
34 console.log(`Planet data from navigation: ${state.planetData}`);
35 }
36 }, [state]);
37
38 return (
39 <div>
40 <h1>Planet details: {planetId}</h1>
41 {/* Rest of the component */}
42 </div>
43 );
44}Using the
useNavigate hook, we can build advanced navigation mechanisms that handle complex user flows:1function ResourceLoader({ resourceId, resourceType }) {
2 const navigate = useNavigate();
3 const [resource, setResource] = useState(null);
4 const [error, setError] = useState(null);
5
6 useEffect(() => {
7 fetchResource(resourceId, resourceType)
8 .then(data => {
9 setResource(data);
10
11 // Conditionally redirect based on resource type
12 if (resourceType === 'mission' && data.status === 'in-progress') {
13 navigate(`/mission-control/${resourceId}`);
14 } else if (resourceType === 'planet' && data.isRestricted) {
15 navigate('/access-denied', {
16 replace: true,
17 state: { reason: 'restricted-planet' }
18 });
19 }
20 })
21 .catch(err => {
22 setError(err);
23
24 // Redirect to error page in case of a problem
25 navigate('/error', {
26 state: {
27 error: err.message,
28 resourceId,
29 resourceType
30 }
31 });
32 });
33 }, [resourceId, resourceType, navigate]);
34
35 if (error) return <p>Loading...</p>;
36 if (!resource) return <p>Loading...</p>;
37
38 return <ResourceViewer resource={resource} />;
39}1function MissionPlanner() {
2 const navigate = useNavigate();
3 const [currentStep, setCurrentStep] = useState(1);
4 const [missionData, setMissionData] = useState({});
5
6 const handleStepComplete = (stepData) => {
7 // Update mission data with data from the current step
8 const updatedData = { ...missionData, ...stepData };
9 setMissionData(updatedData);
10
11 if (currentStep < 3) {
12 // Move to the next step
13 setCurrentStep(currentStep + 1);
14 } else {
15 // All steps completed, save mission and redirect
16 saveMission(updatedData)
17 .then(mission => {
18 navigate(`/missions/${mission.id}/overview`, {
19 state: { missionData: mission, isNewMission: true }
20 });
21 })
22 .catch(error => {
23 navigate('/error', { state: { error } });
24 });
25 }
26 };
27
28 const handleCancel = () => {
29 // Ask the user if they really want to cancel
30 if (window.confirm('Are you sure you want to cancel mission planning?')) {
31 navigate('/missions');
32 }
33 };
34
35 return (
36 <div className="mission-planner">
37 <h1>Planning a New Mission</h1>
38 <ProgressBar currentStep={currentStep} totalSteps={3} />
39
40 {currentStep === 1 && (
41 <MissionBasicInfoStep
42 onComplete={handleStepComplete}
43 initialData={missionData}
44 />
45 )}
46
47 {currentStep === 2 && (
48 <MissionCrewSelectionStep
49 onComplete={handleStepComplete}
50 initialData={missionData}
51 />
52 )}
53
54 {currentStep === 3 && (
55 <MissionConfirmationStep
56 onComplete={handleStepComplete}
57 initialData={missionData}
58 />
59 )}
60
61 <div className="actions">
62 {currentStep > 1 && (
63 <button onClick={() => setCurrentStep(currentStep - 1)}>
64 Back
65 </button>
66 )}
67
68 <button onClick={handleCancel}>Cancel</button>
69 </div>
70 </div>
71 );
72}1function SessionTimer() {
2 const navigate = useNavigate();
3 const [timeLeft, setTimeLeft] = useState(300); // 5 minutes
4
5 useEffect(() => {
6 if (timeLeft <= 0) {
7 // Session expired, redirect to login page
8 alert('Your session has expired. Please log in again.');
9 navigate('/login', {
10 replace: true,
11 state: { sessionExpired: true }
12 });
13 return;
14 }
15
16 // Update timer every second
17 const timer = setTimeout(() => {
18 setTimeLeft(timeLeft - 1);
19 }, 1000);
20
21 return () => clearTimeout(timer);
22 }, [timeLeft, navigate]);
23
24 const formatTime = () => {
25 const minutes = Math.floor(timeLeft / 60);
26 const seconds = timeLeft % 60;
27 return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
28 };
29
30 return (
31 <div className="session-timer">
32 <span>Remaining session time: {formatTime()}</span>
33 </div>
34 );
35}A very common use case for programmatic navigation is redirecting based on API responses. Here are a few typical scenarios:
1function NewPlanetForm() {
2 const navigate = useNavigate();
3 const [isSubmitting, setIsSubmitting] = useState(false);
4 const [error, setError] = useState(null);
5
6 const handleSubmit = async (event) => {
7 event.preventDefault();
8 setIsSubmitting(true);
9 setError(null);
10
11 try {
12 const formData = new FormData(event.target);
13 const planetData = {
14 name: formData.get('name'),
15 type: formData.get('type'),
16 diameter: Number(formData.get('diameter')),
17 hasWater: formData.get('hasWater') === 'on'
18 };
19
20 // Send data to API
21 const response = await fetch('/api/planets', {
22 method: 'POST',
23 headers: {
24 'Content-Type': 'application/json'
25 },
26 body: JSON.stringify(planetData)
27 });
28
29 if (!response.ok) {
30 throw new Error('Error adding planet');
31 }
32
33 const newPlanet = await response.json();
34
35 // Redirect to the new planet's details page
36 navigate(`/planets/${newPlanet.id}`, {
37 state: {
38 message: 'Planet has been successfully added!',
39 planetData: newPlanet
40 }
41 });
42 } catch (err) {
43 setError(err.message);
44 setIsSubmitting(false);
45 }
46 };
47
48 return (
49 <form onSubmit={handleSubmit}>
50 {/* Form fields */}
51 {error && <div className="error">{error}</div>}
52 <button type="submit" disabled={isSubmitting}>
53 {isSubmitting ? 'Adding...' : 'Add planet'}
54 </button>
55 </form>
56 );
57}1function LoginForm() {
2 const navigate = useNavigate();
3 const location = useLocation();
4
5 // Check if the user was redirected from another page
6 const from = location.state?.from?.pathname || '/dashboard';
7
8 const handleSubmit = async (event) => {
9 event.preventDefault();
10 const formData = new FormData(event.target);
11
12 try {
13 // Call login API
14 const user = await loginUser(
15 formData.get('username'),
16 formData.get('password')
17 );
18
19 // Save user data in application state
20 setUserInContext(user);
21
22 // Redirect to original location or dashboard
23 navigate(from, { replace: true });
24 } catch (error) {
25 // Handle login error
26 //
27 }
28 };
29
30 return (
31 <form onSubmit={handleSubmit}>
32 {/* Login form fields */}
33 </form>
34 );
35}Using
useNavigate, we can create route guards that automatically redirect the user if they don't meet certain conditions. This technique is often used to protect routes that require authentication:1function RequireAuth({ children }) {
2 const navigate = useNavigate();
3 const location = useLocation();
4 const { user } = useAuth();
5
6 useEffect(() => {
7 if (!user) {
8 // Redirect to login page, preserving information
9 // about where the user came from so we can return them after login
10 navigate('/login', {
11 replace: true,
12 state: { from: location }
13 });
14 }
15 }, [user, navigate, location]);
16
17 // If the user is logged in, render children (the protected route)
18 return user ? children : null;
19}
20
21// Usage in route definition
22<Route
23 path="/mission-control"
24 element={
25 <RequireAuth>
26 <MissionControlPanel />
27 </RequireAuth>
28 }
29/>We can use
navigate to navigate with search parameters, which is useful for filtering, sorting, and pagination:1function GalaxiesExplorer() {
2 const navigate = useNavigate();
3 const [searchParams] = useSearchParams();
4
5 // Read current filters from URL
6 const currentType = searchParams.get('type') || 'all';
7 const currentSort = searchParams.get('sort') || 'name';
8
9 const applyFilters = (filters) => {
10 // Create a new URLSearchParams object
11 const params = new URLSearchParams();
12
13 // Add filters to parameters
14 if (filters.type && filters.type !== 'all') {
15 params.set('type', filters.type);
16 }
17
18 if (filters.sort) {
19 params.set('sort', filters.sort);
20 }
21
22 // Navigate to the same path but with new parameters
23 navigate({
24 pathname: '/galaxies',
25 search: params.toString() ? `?${params.toString()}` : ''
26 });
27 };
28
29 return (
30 <div className="galaxies-explorer">
31 <div className="filters">
32 <label>
33 Galaxy type:
34 <select
35 value={currentType}
36 onChange={(e) => applyFilters({
37 type: e.target.value,
38 sort: currentSort
39 })}
40 >
41 <option value="all">All</option>
42 <option value="spiral">Spiral</option>
43 <option value="elliptical">Elliptical</option>
44 <option value="irregular">Irregular</option>
45 </select>
46 </label>
47
48 <label>
49 Sort by:
50 <select
51 value={currentSort}
52 onChange={(e) => applyFilters({
53 type: currentType,
54 sort: e.target.value
55 })}
56 >
57 <option value="name">Name</option>
58 <option value="size">Size</option>
59 <option value="distance">Distance</option>
60 </select>
61 </label>
62 </div>
63
64 {/* Galaxy list */}
65 </div>
66 );
67}We can combine
navigate with the createSearchParams function for easier creation of search parameters:1import { useNavigate, createSearchParams } from 'react-router-dom';
2
3function PlanetFilters() {
4 const navigate = useNavigate();
5
6 const applyFilters = (filters) => {
7 // Use createSearchParams to generate a parameters string
8 const params = createSearchParams({
9 type: filters.type,
10 hasWater: filters.hasWater ? 'true' : undefined,
11 minDiameter: filters.minDiameter,
12 sortBy: filters.sortBy
13 });
14
15 navigate({
16 pathname: '/planets',
17 search: `?${params}`
18 });
19 };
20
21 return (
22 <form onSubmit={(e) => {
23 e.preventDefault();
24 applyFilters({
25 type: e.target.type.value,
26 hasWater: e.target.hasWater.checked,
27 minDiameter: e.target.minDiameter.value,
28 sortBy: e.target.sortBy.value
29 });
30 }}>
31 {/* Filter form fields */}
32 <button type="submit">Apply filters</button>
33 </form>
34 );
35}Avoid using
navigate directly during component rendering - it causes problems with multiple re-renders. Instead, use navigate in effects, event handlers, or callbacks:1// BAD - causes infinite render loop
2function BadComponent() {
3 const navigate = useNavigate();
4
5 if (!userIsAuthenticated) {
6 navigate('/login'); // This will be called during every render!
7 }
8
9 return <div>Protected content</div>;
10}
11
12// GOOD - uses useEffect
13function GoodComponent() {
14 const navigate = useNavigate();
15
16 useEffect(() => {
17 if (!userIsAuthenticated) {
18 navigate('/login');
19 }
20 }, [navigate, userIsAuthenticated]);
21
22 return <div>Protected content</div>;
23}Remember that data passed through
navigate in state is lost on page refresh. For data that must survive a refresh, use URL parameters or store them in local storage:1// Data will be lost on refresh
2navigate('/dashboard', { state: { importantData: data } });
3
4// Better for important data
5// 1. Use URL parameters
6navigate(`/dashboard?dataId=${dataId}`);
7
8// 2. Or save in localStorage
9localStorage.setItem('importantData', JSON.stringify(data));
10navigate('/dashboard');Sometimes we want to redirect the user after an asynchronous operation completes. Be careful about potential issues when the component unmounts before the operation completes:
1function AsyncNavigation() {
2 const navigate = useNavigate();
3 const [isLoading, setIsLoading] = useState(false);
4
5 const handleClick = async () => {
6 setIsLoading(true);
7
8 try {
9 // Asynchronous operation
10 await someAsyncOperation();
11
12 // The component may already be unmounted at this point!
13 // If using React 18 with Strict Mode, this may cause issues
14 navigate('/success');
15 } catch (error) {
16 setIsLoading(false);
17 // Handle error
18 }
19 };
20
21 return (
22 <button onClick={handleClick} disabled={isLoading}>
23 {isLoading ? 'Loading...' : 'Execute operation'}
24 </button>
25 );
26}The solution is to use flags to track whether the component is still mounted:
1function SafeAsyncNavigation() {
2 const navigate = useNavigate();
3 const [isLoading, setIsLoading] = useState(false);
4 const isMounted = useRef(true);
5
6 useEffect(() => {
7 return () => {
8 // Mark component as unmounted during cleanup
9 isMounted.current = false;
10 };
11 }, []);
12
13 const handleClick = async () => {
14 setIsLoading(true);
15
16 try {
17 await someAsyncOperation();
18
19 // Check if the component is still mounted
20 if (isMounted.current) {
21 navigate('/success');
22 }
23 } catch (error) {
24 if (isMounted.current) {
25 setIsLoading(false);
26 }
27 }
28 };
29
30 return (
31 <button onClick={handleClick} disabled={isLoading}>
32 {isLoading ? 'Loading...' : 'Execute operation'}
33 </button>
34 );
35}Programmatic navigation is a powerful tool in the React developer's arsenal, allowing for the creation of advanced user flows and responding to various events in the application. The
useNavigate hook offers a flexible and intuitive API for controlling navigation:Using programmatic navigation, you can build more dynamic and responsive applications that smoothly guide the user through different cosmic spaces of your application.