We use cookies to enhance your experience on the site
CodeWorlds

Programmatic Navigation in Cosmic Travels

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.

Introduction to Programmatic Navigation

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:

  • Redirecting after successful login
  • Navigation after completing a multi-step form
  • Returning after saving data
  • Redirecting after session timeout
  • Navigation based on API responses

In React Router, the main tool for programmatic navigation is the

useNavigate
hook.

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}

Basic Usage of navigate

The

navigate
function can accept different arguments:

  1. Target path as a string:
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. A number (delta) for history navigation:
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. An object with options:
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});

The replace Option

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}

Passing State Through Navigation

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}

Building Advanced Navigation Mechanisms

Using the

useNavigate
hook, we can build advanced navigation mechanisms that handle complex user flows:

1. Conditional Redirects

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}

2. Controlled Multi-step Flow

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}

3. Time-based Redirects

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}

Navigation Based on API Responses

A very common use case for programmatic navigation is redirecting based on API responses. Here are a few typical scenarios:

1. Redirect After Successful Form Submission

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}

2. Redirect After Successful Login

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}

Route Guards

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/>

Navigation with Search Parameters

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}

Using navigate with URL Generator

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}

Problems and Pitfalls of Programmatic Navigation

1. Redirects During Rendering

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}

2. Lost State Data

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');

3. Problems with Asynchronous Redirects

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}

Summary

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:

  1. Basic redirects - simple transitions between paths
  2. Navigation with parameters - passing data through the URL
  3. State passing - invisible data available on the target page
  4. History replacement - control over the browser's history stack
  5. Conditional redirects - navigation based on business logic and API responses

Using programmatic navigation, you can build more dynamic and responsive applications that smoothly guide the user through different cosmic spaces of your application.

Go to CodeWorlds