My setup: React 18.1, React Router DOM 6.3, and React Redux 18.0.2.
My goal: If user is not authenticated (via an in-memory JWT) the redirect to login page. Upon successful auth, redirect user to the route they were originally trying to reach.
My question: Based on prior Stack Overflow answers that suggested passing the router history object (which I think it the correct solution for prior versions of React Router), I decided to pass the object returned by the useNavigate() hook. With the exception of a serialization error message (which I think I can disable according to this post), it seems to work. But will this be problematic? Bad practice?
Login.tsx
import React, { useState } from "react";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import "./Login.css";
import { authenticate } from "../features/auth/authSlice";
import { store } from "../app/store";
import { useLocation, useNavigate } from "react-router-dom";
interface LocationState {
target_url: string | null;
}
const Login: React.FC = (props) => {
const [email, setEmail] = useState("swpulitzer@gmail.com");
const [password, setPassword] = useState("password");
const location = useLocation();
const navigate = useNavigate();
const { target_url } = location.state as LocationState || { target_url: null};
function validateForm() {
return email.length > 0 && password.length > 0;
}
function handleSubmit(event: React.MouseEvent<HTMLButtonElement>) {
event.preventDefault();
store.dispatch(authenticate({
email: email,
password: password,
navigate: navigate,
target_url: target_url
}));
}
return (
<div className="Login">
<Form>
<Form.Group controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control
autoFocus
type="email"
value={email}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)}
/>
</Form.Group>
<Button
size="lg"
type="submit"
disabled={!validateForm()}
onClick={handleSubmit}>
Login
</Button>
</Form>
</div>
);
}
export default Login;
authSlice.ts
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { API } from "../../app/services";
export type User = {
email: string,
}
export interface AuthState {
user: User | null,
access: string | null
}
const initialState: AuthState = {
user: null,
access: null,
}
export interface IAuthArgs {
email: string,
password: string,
navigate: any,
target_url: string | null,
}
export interface IAuthRespPayload {
success: boolean,
access: string | null,
navigate: any,
target_url: string | null,
}
export const authenticate = createAsyncThunk(
'auth/authenticate',
async (args: IAuthArgs, thunkAPI) => {
const response = await API.authenticate(args.email, args.password);
console.log(response);
return {
success: true,
access: response.access,
navigate: args.navigate,
target_url: args.target_url
}
}
)
export const authSlice = createSlice({
name: 'authenticator',
initialState,
reducers: {
logout: (state, action) => {
state.access = null;
state.user = null;
}
},
extraReducers: (builder) => {
builder
.addCase(authenticate.fulfilled, (state, action: PayloadAction<IAuthRespPayload>) => {
state.access = action.payload.access;
if (action.payload.target_url !== null)
action.payload.navigate(action.payload.target_url);
})
.addCase(authenticate.rejected, (state, action) => {
// TODO
console.log(action.error.message);
})
.addCase(authenticate.pending, (state, action) => {
// TODO
})
},
});
const { actions, reducer } = authSlice;
export const { logout } = actions;
export default authSlice.reducer;