React Router: history.push vs. <Redirect />

February 15, 2020

If you are using the famous React Router, you maybe have asked yourself the same question: should I use the history.push or <Redirect /> API. Both APIs offer the same functionality but usage is completely different.

Usage of history.push

With the newer versions of React Router, you’re able to get access the history API with hooks.

import React from 'react';
import { useHistory } from 'react-router-dom';
import { signIn } from '~/api';
import SignInForm from './SignInForm';

const SignIn = () => {
  const history = useHistory();
  const [hasError, setError] = React.useState(null);

  const handleSignIn = async (username, password) => {
    try {
      await signIn(username, password);
      history.push('/dashboard');
    } catch (error) {
      setError(true);
    }
  };

  return (
    <SignInForm
      onSubmit={handleSignIn}
      hasError={hasError}
    />
  );
};

export default SignIn;

While this code is very much simplified, you have to look carefully to find the place where the redirect takes place. The manner we use the history.push API here is called imperative. With imperative programming you define how to do something - step by step. But recently, we see that declarative programming is more trendy. Instead of defining how to log step by step, we just tell the computer what we want. By applying declarative programming pattern, we create clearer boundaries in our applications. Let’s see how this sign-in example would look like with the declarative <Redirect /> API:

import React from 'react';
import { Redirect } from 'react-router-dom';
import { signIn } from '~/api';
import SignInForm from './SignInForm';

const SignInState = Object.freeze({
  LoggedOut: 0,
  LoggedIn: 1,
  Failed: 2
});

const SignIn = () => {
  const [signInState, setSignInState] = React.useState(
    SignInState.LoggedOut
  );

  const handleSignIn = async (username, password) => {
    try {
      await signIn(username, password);
      setSignInState(SignInState.LoggedIn);
    } catch (error) {
      setSignInState(SignInState.Failed);
    }
  };

  if (signInState === SignInState.LoggedIn) {
    return <Redirect push to="/dashboard" />;
  }

  return (
    <SignInForm
      onSubmit={handleSignIn}
      hasError={signInState === SignInState.Failed}
    />
  );
};

export default SignIn;

While this example is way bigger, it has a big benefit: It’s easier to see how the API will behave. It gives also the opportunity to change the behaviour later more easily as the action is now decoupled from the side effect.