I'm trying to implement a Protected Route, which firstly tries to get an authentification(api call) so it can display the Route.
But somehow the state value doesnt change..
Do you got any idea?
const ProtectedRoute = ({ component: Component, ...rest }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const fetch = async () => {
const result = await axios.get("http://localhost:5000/auth/", {
withCredentials: true,
});
if (result.status >= 200 && result.status < 300) {
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
};
useEffect(() => {
fetch();
}, []);
return (
<Route
{...rest}
render={(props) => {
if (isAuthenticated) {
return <Component {...props} />;
} else {
return <Redirect to={"./loginUser"} />;
}
}}
/>
);
};
export default ProtectedRoute;
What you can do is to return something when the api call is still loading. Something like this :
const ProtectedRoute = ({ component: Component, ...rest }) => {
const [isAuthenticated, setIsAuthenticated] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const fetch = async () => {
const result = await axios.get("http://localhost:5000/auth/", {
withCredentials: true,
});
if (result.status >= 200 && result.status < 300) {
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
};
useEffect(() => {
setIsLoading(true);
fetch();
setIsLoading(false);
}, []);
return (
<Route
{...rest}
render={(props) => {
if(isLoading) { // do something }
else if (isAuthenticated !== undefined && !isLoading) {
return <Component {...props} />;
} else {
return <Redirect to={"./loginUser"} />;
}
}}
/>
);
};
export default ProtectedRoute;
Related
In my React App, I'm displaying all the books with author name. User can delete a book by clicking an item. Thing is I want to refresh the page without reloading the entire page. States are the way to go for such kind of situations but it still doesn't refresh the component.
Can anybody suggest any ideas?
App.tsx
import React, { useLayoutEffect, useState } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { getAllBooks } from "./api_actions/api_calls";
import "./App.css";
import AllBooks from "./components/AllBooks";
import InsertBooks from "./components/InsertBook";
import { Book } from "./models/Book";
function App() {
const [myBooks, setMyBooks] = useState<Book[]>([]);
useLayoutEffect(() => {
getAllBooks().then((orders) => {
setMyBooks(orders);
});
}, []);
return (
<div className="App">
<header className="App-header">
<BrowserRouter>
<Routes>
<Route path="/" element={<AllBooks books={myBooks} />} />
<Route path="/add" element={<InsertBooks />} />
</Routes>
</BrowserRouter>
</header>
</div>
);
}
export default App;
Component that displays all the books, AllBooks.tsx:
interface IAllBooksProps {
books: Book[];
}
const AllBooks: React.FC<IAllBooksProps> = (props) => {
const [lastDeletedTitle, setLastDeletedTitle] = useState("");
const handleDeleteBook = (title: string) => {
console.log("Trying to delete...", title);
deleteBook(title).then((response) => {
setLastDeletedTitle(title);
});
};
useEffect(() => {
if (lastDeletedTitle !== "") {
toast(`${lastDeletedTitle} has been deleted!`);
}
}, [lastDeletedTitle]);
return (
<>
{props.books?.map((book) => {
return <Card key={book.id} book={book} onDelete={handleDeleteBook} />;
})}
<ToastContainer />
</>
);
};
It is better not to call getAllBooks in App.tsx. You just need to call getAllBooks inside your delete function and useEffect in AllBooks.tsx. Try the code given below,
AllBooks.tsx
interface IAllBooksProps {
books: Book[];
}
const AllBooks: React.FC<IAllBooksProps> = (props) => {
const [lastDeletedTitle, setLastDeletedTitle] = useState("");
useEffect(() => {
getBooks();
}, [])
const getBooks = () => {
getAllBooks().then((orders) => {
setMyBooks(orders);
});
}
const handleDeleteBook = (title: string) => {
console.log("Trying to delete...", title);
deleteBook(title).then((response) => {
setLastDeletedTitle(title);
getBooks();
});
};
useEffect(() => {
if (lastDeletedTitle !== "") {
toast(`${lastDeletedTitle} has been deleted!`);
}
}, [lastDeletedTitle]);
return (
<>
{props.books?.map((book) => {
return <Card key={book.id} book={book} onDelete={handleDeleteBook} />;
})}
<ToastContainer />
</>
);
};
Bring changes in AllBooks.tsx like below.
const AllBooks: React.FC<IAllBooksProps> = (props) => {
const [books, setBooks] = useState(props.books); //props.books set in books state
const [lastDeletedTitle, setLastDeletedTitle] = useState("");
const handleDeleteBook = (title: string) => {
console.log("Trying to delete...", title);
deleteBook(title).then((response) => {
setLastDeletedTitle(title);
});
};
useEffect(() => {
if (lastDeletedTitle !== "") {
toast(`${lastDeletedTitle} has been deleted!`);
//recall the getAllBooks here
getAllBooks().then((orders) => {
setBooks(orders); //reset books
});
}
}, [lastDeletedTitle]);
return (
<>
//return books here
{books?.map((book) => {
return <Card key={book.id} book={book} onDelete={handleDeleteBook} />;
})}
<ToastContainer />
</>
);
};
What would be the best practice for private routing? Maybe I'm doing something wrong, but when user logged in I already am redirect to the /login page
And my second question: Which of these versions is better or you have even better idea?
Code:
Auth
const authSlice = createSlice({
name: 'auth',
initialState: {
user: {},
isUserLoggedIn: null,
isLoading: false,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(me.pending, (state) => {
state.isLoading = true
})
.addCase(me.fulfilled, (state, action) => {
state.user = action.payload.userData
state.isUserLoggedIn = true
state.isLoading = false
})
.addCase(me.rejected, (state) => {
state.isUserLoggedIn = false
})
},
})
export const me = createAsyncThunk('auth/me', async () => {
try {
const user = await userService.getUserData()
return { userData: user.data.data }
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) ||
error.message ||
error.toString()
return message
}
})
CASE 1:
App
function App() {
const dispatch = useDispatch()
useEffect(() => {
dispatch(me())
}, [])
const auth = useSelector((state) => state.auth)
return (
<div data-theme={theme}>
<BrowserRouter>
<AppRoutes isAuthenticated={auth.isUserLoggedIn} />
</BrowserRouter>
</div>
)
}
export default App
Routes
export const AppRoutes = ({ isAuthenticated }) => (
<Routes>
<Route
path='/login'
element={<Login />}
/>
<Route
path='/dashboard'
element={
<PrivateRoute isAuthenticated={isAuthenticated}>
<Stats />
</PrivateRoute>
}
/>
...
PrivateRoute
export const PrivateRoute = ({ children, isAuthenticated }) => {
return isAuthenticated ? children : <Navigate to='/login' />
}
CASE 2:
App
function App() {
return (
<div data-theme={theme}>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</div>
)
}
export default App
Routes
export const AppRoutes = () => (
<Routes>
<Route
path='/login'
element={<Login />}
/>
<Route
path='/dashboard'
element={
<PrivateRoute>
<Stats />
</PrivateRoute>
}
/>
...
PrivateRoute
export const PrivateRoute = ({ children }) => {
const dispatch = useDispatch()
const { isUserLoggedIn } = useSelector((state) => state.auth)
useEffect(() => {
if (isUserLoggedIn === null) {
dispatch(me())
}
}, [])
return isUserLoggedIn ? children : <Navigate to='/login' />
}
CASE 1 or CASE 2 is better to approach or maybe you have a better idea?
For these 2 ideas, it redirects very quickly to /login when I go to /dashboard
What I want to achieve is good practice, quick verification, and waiting until we receive a positive response from the backend that the user is authenticated
Team, what do you suggest?
EDIT
New version:
Redux
const authSlice = createSlice({
name: 'auth',
initialState: {
user: {},
isUserLoggedIn: null,
isLoading: true,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(me.pending, (state) => {
state.isLoading = true
})
.addCase(me.fulfilled, (state, action) => {
state.user = action.payload.userData
state.isUserLoggedIn = true
state.isLoading = false
})
.addCase(me.rejected, (state) => {
state.isLoading = false
state.isUserLoggedIn = false
})
},
})
export const me = createAsyncThunk('auth/me', async ({}, thunkAPI) => {
try {
const user = await userService.getUserData()
return { userData: user.data.data }
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) ||
error.message ||
error.toString()
thunkAPI.rejectWithValue(message)
return message
}
})
PrivateRoute
export const PrivateRoute = ({ children }) => {
const dispatch = useDispatch()
const { isLoading, isUserLoggedIn } = useSelector((state) => state.auth)
if (isLoading) return null
return isUserLoggedIn ? (
children
) : (
<Navigate
to='/login'
replace
/>
)
}
App
function App() {
const dispatch = useDispatch()
const { isUserLoggedIn } = useSelector((state) => state.auth)
useEffect(() => {
if (isUserLoggedIn === null) {
dispatch(me())
}
}, [])
return (
<div data-theme={theme}>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</div>
)
}
export default App
Store
import { configureStore, getDefaultMiddleware } from '#reduxjs/toolkit'
import authReducer from '../features/auth/authSlice'
export const store = configureStore({
reducer: {
auth: authReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }),
})
Index.js
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
But when I reload the page I go to /login don't stay at the same page like /dashboard how could I cope with that?
Either approach is fine. The issue you are seeing is based on the initial redux state value being used for auth check and redirection prior to the effect running to set the auth state. You will want to hold off on redirecting until the authentication status is determined. Use the state to rendering null or some loading indicator until a user is verified authenticated.
const authSlice = createSlice({
name: 'auth',
initialState: {
user: {},
isUserLoggedIn: null,
isLoading: true, // <-- assume initially loading state from mouting
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(me.pending, (state) => {
state.isLoading = true
})
.addCase(me.fulfilled, (state, action) => {
state.user = action.payload.userData
state.isUserLoggedIn = true
state.isLoading = false
})
.addCase(me.rejected, (state) => {
state.isUserLoggedIn = false
})
},
});
Between the two implementations IMO the second is preferred as it leads to less component coupling. I suggest a mix of the two. Check for a user in App and dispatch the action to set the auth state, and check the auth state in the private route.
function App() {
const dispatch = useDispatch();
const { isUserLoggedIn } = useSelector((state) => state.auth);
useEffect(() => {
if (isUserLoggedIn === null) {
dispatch(me());
}
}, []);
return (
<div data-theme={theme}>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</div>
);
}
...
export const PrivateRoute = ({ children }) => {
const dispatch = useDispatch()
const { isLoading, isUserLoggedIn } = useSelector((state) => state.auth)
if (isLoading) return null; // <-- or loading spinner, etc...
return isUserLoggedIn ? children : <Navigate to='/login' replace />
}
The same can be achieved with the first version, you'd just need to pass the isLoading state along with the isUserLoggedIn state as props to the PrivateRoute.
I found a problem! Ha ha!
Seems stupid && simple but it started working ->
Redux
const authSlice = createSlice({
name: 'auth',
initialState: {
user: {},
isUserLoggedIn: null,
isLoading: true,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(me.pending, (state) => {
state.isLoading = true
})
.addCase(me.fulfilled, (state, action) => {
state.user = action.payload.userData
state.isUserLoggedIn = true
state.isLoading = false
})
.addCase(me.rejected, (state) => {
state.isLoading = false
state.isUserLoggedIn = false
})
},
})
export const me = createAsyncThunk('auth/me', async (_, thunkAPI) => { // <- {} replaced to _ and this is it!
try {
const user = await userService.getUserData()
return { userData: user.data.data }
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) ||
error.message ||
error.toString()
thunkAPI.rejectWithValue(message)
return message
}
})
I built a custom hook to get a user from firebase:
export function useAuth() {
const [currentUser, setCurrentUser] = useState<any>();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user === null) {
setCurrentUser(null);
} else {
const u = await getDoc(doc(db, "users", user.uid));
const name = u.data().name;
setCurrentUser(user === null ? null : { ...user, name: name });
}
});
return unsubscribe;
}, []);
return currentUser;
}
In my components I use it as:
const currentUser = useAuth();
useEffect(() => {
const localScore = JSON.parse(localStorage.getItem("score"));
setPush(!(localScore[currentUser.name] === tasks.length));
// ^ TypeError: Cannot read properties of undefined (reading 'name')
}, []);
I think I get the error, because when the components gets rendered the first time, there is no currentUser. But I load the component after the user logged in (what sets the user), thus I am confused.
I think I need to await the user somehow, but how?
From the comments:
I moved my onAuthStateChanged to a context, but this is checking the login state only once and is not updating:
const AppContext = createContext(null);
export function AppWrapper({ children }) {
const [currentUser, setCurrentUser] = useState<any>();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user === null) {
setCurrentUser(null);
} else {
const u = await getDoc(doc(db, "users", user.uid));
const name = u.data().name;
setCurrentUser(user === null ? null : { ...user, name: name });
}
});
return unsubscribe;
}, []);
return (
<AppContext.Provider value={currentUser}>{children}</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
function MyApp({ Component, pageProps }) {
const currentUser = useAppContext();
return (
<>
<AppWrapper>
{Component.secure && !currentUser ? (
<Login />
) : (
<>
<Component {...pageProps} />
</>
)}
</AppWrapper>
</>
);
}
Expected:
the async function checks if the user is authenticated, then return true, so that the protected component gets rendered, or false, that redirects the user to the login page.
What actually happens:
the getAuth() function returns "Promise ", breaking the code.
export default function RequireAuth({ children, redirectTo }) {
const BASE_API_URL = "https://api-backend.test";
const getAuth = async () => {
const isAuth = await axios
.get(BASE_API_URL + "/user_auth.php", {
withCredentials: true,
})
.then((res) => {
if (res.status === 201) {
return true;
}
})
.catch((err) => {
if (err.response.status === 401) {
return false;
}
});
return isAuth;
};
let isAuthenticated = getAuth();
return isAuthenticated ? children : <Navigate to={redirectTo} />;
}
This is how the "protected" component should be displayed according to the official documentation here.
<Route
path="/dashboard"
element={
<RequireAuth redirectTo="/login">
<Dashboard />
</RequireAuth>
}
/>
You have to do it properly using useEffect, try this
export default function RequireAuth({ children, redirectTo }) {
const BASE_API_URL = "https://api-backend.test";
const [isAuthenticated, setAuthenticated] = useState(false);
useEffect(() => {
const getAuth = () => {
const isAuth = axios
.get(BASE_API_URL + "/user_auth.php", {
withCredentials: true
})
.then((res) => {
if (res.status === 201) {
setAuthenticated(true);
}
})
.catch((err) => {
if (err.response.status === 401) {
setAuthenticated(false);
}
});
};
getAuth();
}, [redirectTo]);
if (isAuthenticated) {
return children;
}
return <Navigate to={redirectTo} />;
}
getAuth is an async function, so it will return a Promise. You have to either await its output or use .then to work with the resolved promise.
Side note, it's usually preferable to either use async/await or .then, but not both.
Problem: I am trying to authenticate a user through isAuth() helpers but it is acting weird. I want it to look for access token if any or call for access token from backend if refresh token is available, and though it works perfectly and sets access token cookie, the issue is if called from PrivateRoutes.jsx, it does not sees the tokens at all and sends the user to login page.
Adding required code for refs:
isAuth():
export const isAuth = () => {
if (window !== undefined) {
const accessCookieChecked = getCookie("_mar_accounts_at");
const refreshCookieChecked = getCookie("_mar_accounts_rt");
if (accessCookieChecked) {
return true;
} else if (refreshCookieChecked) {
console.log(refreshCookieChecked);
axios({
method: "POST",
url: `${API_URL}/api/token`,
data: { refresh_token: refreshCookieChecked },
}).then((res) => {
console.log(res);
setCookie("_mar_accounts_at", res.data.accessToken);
return true;
});
} else {
return false;
}
} else {
return false;
}
};
PrivateRoutes.jsx
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { isAuth } from "../helpers/auth";
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={(props) =>
isAuth() ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
)
}
></Route>
);
export default PrivateRoute;
Can someone please see this? And help!
You are running into an async issue most likely, when you make the call in axios, the return true; in the callback never actually returns to your funciton call in the PrivateRoute. Instead, you need to use a Promise/setState/useEffect:
export const isAuth = () => {
if (window === undefined) {
return Promise.resolve(false);
} else {
const accessCookieChecked = getCookie("_mar_accounts_at");
const refreshCookieChecked = getCookie("_mar_accounts_rt");
if (accessCookieChecked) {
return Promise.resolve(true);
} else if (refreshCookieChecked) {
console.log(refreshCookieChecked);
return new Promise(resolve => {
axios({
method: "POST",
url: `${API_URL}/api/token`,
data: { refresh_token: refreshCookieChecked },
}).then((res) => {
console.log(res);
setCookie("_mar_accounts_at", res.data.accessToken);
resolve(true);
});
})
} else {
return Promise.resolve(false);
}
}
};
import React, { useState, useEffect } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { isAuth } from '../helpers/auth';
const PrivateRoute = ({ component: Component, ...rest }) => {
const [isAuthTrue, setIsAuthTrue] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => {
isAuth().then(res => {
setIsAuthTrue(res);
setLoading(false);
})
})
return (
<>
{loading ? (
<div>some loading state</div>
) : (
<Route
{...rest}
render={(props) =>
isAuthTrue ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: '/login', state: { from: props.location } }}
/>
)
}
/>
)}
</>
);
};
export default PrivateRoute;