Search code examples
typescriptreact-router-dom

Invoke hook from React Router data router action


I am using React Router 6.4.3 with the new data routers.

A 3rd party authentication library exposes an authentication context and a login function through a hook, like:

const { AuthenticationContext, login } = useAuthentication();

I already managed to wrap the routes into the AuthenticationContext by using a pathless route in createBrowserRouter.

However, I fail to understand how I could make use of route actions to perform the login and redirect the user afterwards. As configuration and action are now outside of a functional component, I cannot make use of the useAuthentication hook.

My configuration looks like this:

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AuthProvider } from 'auth';
import Login, { action as loginAction } from './routes/login';

const router = createBrowserRouter([
    {
        element: <AuthProvider />,
        children: [
            {
                path: '/login',
                element: <Login />,
                action: loginAction,
            },
        ],
    },
]);

function App() {
    return (
        <RouterProvider router={router} />
    );
}

With loginAction currently only a stub:

const action = async ({ request, params }) => {
    const formData = await request.formData();
    const updates = Object.fromEntries(formData);

    // How to invoke login() method exposed by useAuthentication() hook here?

}

The AuthenticationContext and associated methods roughly look like this:

const loginApi = async (email, password) => {
   // ajax call to login endpoint
}

const AuthContext = React.createContext(null);

const AuthProvider = () => {
    const [jwt, setJwt] = useState(null);
    const navigate = useNavigate();

    const login = async (email, password) => {
        const jwt = await loginApi(email, password);
        setJwt(jwt);
        navigate('/dashboard');
    }

    const contextValue = {
        jwt,
        login: handleLogin,
    }
 
    return (
        <AuthContext.Provider value={contextValue}>
            <Outlet />
        </AuthContext.Provider>
    );
}

const useAuth = () => {
    return useContext(AuthContext);
}

export default AuthProvider;
export { useAuth };

Is there a way to make this work with the data routers without touching the auth lib?


Solution

  • You very likely need to refactor the code a bit to allow the App component to use the useAuthentication hook so it can access the login function so it can be passed to the loginAction handler.

    Example:

    './routes/login'

    import { redirect } from "react-router-dom";
    
    const action = ({ login }) => async ({ request, params }) => {
      const formData = await request.formData();
      const updates = Object.fromEntries(formData);
    
      // call login, and redirect upon success
      await login(...);
      ...
      redirect('/dashboard');
    };
    

    App

    import { createBrowserRouter, RouterProvider } from 'react-router-dom';
    import Login, { action as loginAction } from './routes/login';
    
    function App() {
      const { login } = useAuthentication();
    
      const router = createBrowserRouter([
        ....,
        {
          path: '/login',
          element: <Login />,
          action: loginAction({ login }),
        },
        ....
      ]);
    
      return (
        <RouterProvider router={router} />
      );
    }
    

    index.js

    import { AuthProvider } from 'auth';
    
    ...
    
    <AuthProvider>
      </App />
    </AuthProvider>
    

    AuthenticationContext

    const AuthContext = React.createContext(null);
    
    const AuthProvider = ({ children }) => {
      const [jwt, setJwt] = useState(null);
    
      const login = async (email, password) => {
        const jwt = await loginApi(email, password);
        setJwt(jwt);
        // return success/fail for login action handler
      }
    
      const contextValue = {
        jwt,
        login: handleLogin,
      }
     
      return (
        <AuthContext.Provider value={contextValue}>
          {children}
        </AuthContext.Provider>
      );
    }