Declarative or Imperative? That is the question. React Router’s latest update saw a huge shift towards a routing library more aligned with the core React framework, making it far easier to grasp the mental model and easily compose with in our projects.
Navigation within single page apps often requires a blend of two different approaches - declarative and imperative. For simple approaches, a declarative approach is often favored, but for operations that require more complex logic then an imperative approach is often called upon to save the day.
React is a declarative framework by nature, so it makes sense to begin with the approach we know and love.
<Navigate /> the declarative way
Using a declarative navigation approach with React Router’s <Navigate />
component allows us to specify exactly when a navigation event occurs within our JSX - making it clear when and where the navigation will happen.
It’s also easier to read and understand because the navigation is part of the JSX structure, and not nested somewhere deep down within a callback function. Why might you want to do this? Conditional rendering.
Let’s assume a settings component that displays the app settings, with a button to return to the home page - redirecting a user based on an event they initiated. Using the <Navigate />
component is a super use case here to create a clear divide in component logic and operations:
import { useState } from 'react';
import { Navigate } from 'react-router-dom';
function LoginComponent() {
const [toHome, setToHome] = useState(false);
if (toHome) {
return <Navigate to="/home" />;
}
return (
<div>
<h1>Settings Page</h1>
<button onClick={() => setToHome(true)}>Home</button>
</div>
);
}
It aligns nicely with the rest of the component code and follows React’s concept of declarative UI and internal state. Personally, this adds ultimate readability and allows for a very “React way” of doing things, with our component logic and flow clear and concise - all written with JSX.
The <Navigate />
component is a simple wrapper around the useNavigate
hook, but stays true to React and personally I think it’s a clearer approach. That said, let’s explore the alternative routing option to fully grasp each approach.
useNavigate() the imperative way
Hooks paved the way to the new React and React Router shortly followed with the useNavigate
hook. You’ve probably got enough hooks to worry about, and if you’d like to use another then useNavigate
is for you.
It offers a more streamlined and minimal code footprint (no use of useState
within the component) and an easy imperative style, but it fails to be as clear on the intent of the code without further investigation - making us look inside the callback function for the onClick
handler to see what is expected to happen. It requires more thinking, whereas the declarative <Navigate />
does not.
Here’s the same example, which while it’s small and trivial I hope you can appreciate it in a larger context:
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
function LoginComponent() {
const navigate = useNavigate();
return (
<div>
<h1>Login Page</h1>
<button onClick={() => navigate('/home')}>Login</button>
</div>
);
}
It requires a few extra steps to see what’s happening and you follow a trail of events to get you there (onClick
then navigate
) versus seeing a <Navigate />
element abstracted nicely above the component’s core template. Separation of concerns is at play here.
Ultimately, the decision is yours on which route to take (pun intended). Be declarative with React-style navigation and use internal component state, or step into the imperative world. Both approaches are completely valid, and personal preference will vary. I think here though, the benefits of using <Navigate />
outweigh the cons of adding a component useState
declaration and splitting the JSX out where appropriate.
Hop on over to our React courses where there’s lots more to treat your developer skillset with.
And that’s it, both ways to programmatically navigate with React Router.