React protected route with redux - reactjs

I created a separate node.js restful API for checking if the user token is valid. I did this for my react protected routes, even though most of my endpoints already checks the user token.
My redux Thunk:
const verifyToken = () => {
return async (dispatch: Dispatch) => {
await axios
.get("http://localhost:9999/authenticate", {
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
})
.then(async (resp) => {
const { message } = await resp.data;
console.log(message);
if (message === "verified") dispatch({ type: "signIn-success" });
})
.catch((err) => {
console.log(err);
dispatch({ type: "logout-user" });
});
};
};
Protected Route:
const ProtectedRoute: FC<{ children: ReactNode; path: string }> = ({
children,
path,
}) => {
const dispatch = useDispatch();
dispatch(verifyToken()); //dispatch thunk
const status = useSelector((store: RootStore) => store.status)
return status && status === "loggedOut" ? (
<Redirect to="/" />
) : (
<Route path={path} render={() => <>{children}</>} />
);
};
The problem is that it takes time to dispatch my thunk, so it redirects to "/" even though the user token checking hasn't finished.
So what happens is, the value of status is "loggedOut" at first then my thunk will check if the token is valid. If it is, status becomes "loggedIn", but its too late because it already redirected to "/"

You need a loading for the time you're dispatching the action, something like this:
const ProtectedRoute: FC<{ children: ReactNode, path: string }> = ({
children,
path,
}) => {
const [loading, setLoading] = useState(true);
const dispatch = useDispatch();
const status = useSelector((store: RootStore) => store.status);
async function verify() {
await dispatch(verifyToken()); //dispatch thunk
setLoading(false);
}
useEffect(() => {
verify();
}, []);
if (loading) return <div>Show Loading...</div>;
return status === "loggedOut" ? (
<Redirect to="/" />
) : (
<Route path={path} render={() => <>{children}</>} />
);
};

Related

how to delay the rendering of render function until usereffect calls in react

i want to run the useEffect first before the render function which is placed inside the <Route /> tag starts to render. i expect to get currently available user details through the API and assigne them to render function.
but render function runs before the UseEffect retrieve data from the API. so help me to find the solution.
import React, { useEffect, useState } from "react";
import { Route, Redirect } from "react-router-dom";
import { Auth } from "aws-amplify";
const ProtectedRoute = ({ children, ...rest }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
setIsAuthenticated(
Auth.currentAuthenticatedUser({
// bypassCache: false,
})
.then((user) => console.log(user))
.catch((err) => console.log(err))
);
}, []);
return (
<Route
{...rest}
render={({ location }) =>
(isAuthenticated ) ? (
children
) : (
<Redirect
to={{
// pathname: "/login",
pathname: "/create-profile",
state: { from: location },
}}
/>
)
}
/>
);
};
export default ProtectedRoute;
Try this
useEffect(() => {
Auth.currentAuthenticatedUser({
// bypassCache: false,
})
.then((user) => user && setIsAuthenticated(true))
.catch((err) => err && setIsAuthenticated(false));
}, []);
You could wrap that authentication stuff into a hook of your own, and then simply not render anything until it's ready:
function useIsAuthenticated() {
const [isAuthenticated, setIsAuthenticated] = useState(null);
useEffect(() => {
Auth.currentAuthenticatedUser({})
.then(setIsAuthenticated)
.catch((err) => {
console.log(err);
setIsAuthenticated(false);
});
}, []);
return isAuthenticated;
}
const ProtectedRoute = ({ children, ...rest }) => {
const isAuthenticated = useIsAuthenticated(); // Will be the user if authenticated, null if busy, or false if error.
if (isAuthenticated === null) {
return null; // Don't render anything if authentication state is unknown
}
return <>...</>;
};

Component shows for a brief moment before redirecting in react router v6

I have a small issue I'm not able to fix. In my react app I use react-router v6 and I have to following routes:
<Route path="/" element={<App />} />
<Route path=":id" element={<CharacterDetails/>} />
<Route path="*" element={<Navigate replace to="/" />} />
As you can see, I have a route that needs an id. This works fine as long as I provide an existing Id so that CharacterDetails component fetches some data successfully. However, if I pass some random text in the URL like "localhost:3000/randomText" the CharacterDetails component still shows for a brief second till the useEffect fires to determine that the request is a 404 and only after that it redirects me to the App component.
How can I check if the URL provided should indeed return some data before rendering the component ? and redirect to the App component directly (without the flickering of the CharacterDetails component) when it is not a valid URL
Thanks!
EDIT: I'm not sure if this is a router issue or should I do it at the component level, I'm waiting for suggestions
EDIT2: Component code
const CharacterDetails = () => {
const { id } = useParams<string>();
const navigate = useNavigate();
const [state, dispatch] = useReducer(characterReducer, initialState);
const { data, episodes, loading } = state;
useEffect(() => {
const fetchData = async (id: string) => {
dispatch({ type: "LOADING_START" })
try {
let response = await getSingleCharacterData(id);
let URLs = response.data.episode;
let listOfEpisodes = await getEpisodeName(URLs);
dispatch({
type: "FETCH_SUCCESS",
payload: { data: response.data, episodeList: listOfEpisodes },
});
dispatch({ type: "LOADING_OVER" })
} catch (error) {
dispatch({ type: "LOADING_OVER" })
navigate("/");
}
};
if (id) fetchData(id);
}, [id, navigate]);
return (
<CharacterDetailsContainer>
{loading ? <Loading /> :
data && (
<div> </div>
)}
</CharacterDetailsContainer>
}
You can use the useParams hook in the child.
const acceptableIDs = ["dog", "cat"];
function CharacterDetails() {
let { id } = useParams();
return acceptableIDs.includes(id) ? (
<div>
<h3>ID: {id}</h3>
</div>
) : null; // render nothing or redirect
}
If it takes too long to check if the ID is valid, you could show a transition.
Note this is business logic and should probably not bleed into the router.
This isn't an issue with the router/routes, it's something that routed components need to handle.
In the CharacterDetails component use some "loading" state to conditionally render null or some loading indicator while the id path param is validated. Note that "loading" needs to be the initial state so the code isn't leaking any initial non-loading UI, waiting until the useEffect hook runs at the end of the initial render to dispatch({ type: "LOADING_START" }) is too late unless the initial redux state has it set true.
Example:
const CharacterDetails = () => {
const { id } = useParams();
const navigate = useNavigate();
const [isLoading, setIsLoading] = React.useState(true);
useEffect(() => {
setIsLoading(true);
// logic to validate id param
if (is404) {
navigate("/404", { replace: true }); // redirect
} else {
setIsLoading(false); // clear loading state so page content renders
}
}, [id]);
if (isLoading) {
return null; // or loading spinner/etc...
}
return page content
};
Your code:
const CharacterDetails = () => {
const { id } = useParams<string>();
const navigate = useNavigate();
const [isLoading, setIsLoading] = React.useState<boolean>(true); // <-- initially true
const [state, dispatch] = useReducer(characterReducer, initialState);
const { data, episodes } = state;
useEffect(() => {
const fetchData = async (id: string) => {
setIsLoading(true);
dispatch({ type: "LOADING_START" });
try {
let response = await getSingleCharacterData(id);
let URLs = response.data.episode;
let listOfEpisodes = await getEpisodeName(URLs);
dispatch({
type: "FETCH_SUCCESS",
payload: { data: response.data, episodeList: listOfEpisodes },
});
setIsLoading(false);
} catch (error) {
// handle any errors, etc...
// redirect home
navigate("/", { replace: true });
} finally {
dispatch({ type: "LOADING_OVER" });
}
};
if (id) fetchData(id);
}, [id, navigate]);
if (isLoading) {
return null; // or loading spinner/etc...
}
return ( <some JSX> )
}

custom hook and private route in React immediately navigates to login

I have a very straight forward private Route in React:
export default function PrivateRoute({ children }) {
const auth = useAuth();
return auth ? children : <Navigate to="/login" />;
}
and this is my custom hook:
export default function useAuth() {
const [loggedIn, setLoggedIn] = useState(false);
useEffect(() => {
const token = Cookies.get("token");
axios
.get(`${process.env.REACT_APP_API}/auth`, {
headers: { Authorization: `Bearer ${token}` },
})
.then((result) => {
if (result.status === 200) {
setLoggedIn(true);
}
})
.catch((error) => console.error(error));
}, []);
return loggedIn;
}
The hook successfully returns true or false, but at the time when the PrivateRoute is called, it is false, thus I land on the login page.
How can I wait for the hook before redirecting to login?
I call the PrivateRoute like this:
<Route
path="/admin"
exact
element={
<PrivateRoute>
<Admin />
</PrivateRoute>
}
/>
``
The initial loggedIn state value matches the unauthenticated state. Use an indeterminant value that doesn't match either the confirmed authenticated/unauthenticated status. Check for this indeterminant value and conditionally render null or some loading indicator.
export default function useAuth() {
const [loggedIn, setLoggedIn] = useState(); // <-- undefined
useEffect(() => {
const token = Cookies.get("token");
axios
.get(`${process.env.REACT_APP_API}/auth`, {
headers: { Authorization: `Bearer ${token}` },
})
.then((result) => {
setLoggedIn(result.status === 200);
})
.catch((error) => {
console.error(error)
setLoggedIn(false);
});
}, []);
return loggedIn;
}
...
export default function PrivateRoute({ children }) {
const location = useLocation();
const auth = useAuth();
if (auth === undefined) return null; // or loading spinner/etc...
return auth
? children
: <Navigate to="/login" replace state={{ from: location }} />;
}

Prevent React from re-fetching data in parent component

In my parent component, Dashboard.tsx, I have a child component, Expenses.tsx, that makes an API fetch call and then displays the data. The parent component has a Router that allows you to navigate to different URL's in the parent component, which forces everything to re-render every time you navigate to a new path or render a new child component. How can I make it so that this fetch call is only made one time? I've tried using the useRef() hook but it re-initializes every time there is a re-render and I have the same problem.
Here is Dashboard.tsx:
export const Dashboard = () => {
const d = new Date()
const history = useHistory()
const { user, setUser } = useAuth()
const [categories, setCategories] = useState({
expenseCategories: [],
incomeCategories: []
})
const getCategories = async(user_id: number) => {
await fetch(`/api/getCategories?user_id=${user_id}`)
.then(result => result.json())
.then(result => setCategories(result))
}
useEffect(() => {
if (user.info.user_id) {
getCategories(user.info.user_id)
}
}, [])
const dashboardItems = [
{
value: 'Add Expense',
path: '/dashboard/addExpense'
},
{
value: 'Add Income',
path: '/dashboard/addIncome'
},
{
value: 'Logout',
path: '/login',
onClick : async() => {
localStorage.clear()
setUser({
info: {
user_id: null,
email: null,
username: null
},
token: null
})
},
float: 'ml-auto'
}
]
return(
<div>
<DashboardNavbar items={dashboardItems}/>
<div className="wrapper">
<p>{`Hello, ${user.info.username}!`}</p>
<DateAndTime />
<Expenses date={d}/>
<Income date={d}/>
<Switch>
<Route path='/dashboard/addExpense'>
<AddItemForm user={user} type={'expenses'} categories={categories.expenseCategories} />
</Route>
<Route path='/dashboard/addIncome'>
<AddItemForm user={user} type={'income'} categories={categories.incomeCategories} />
</Route>
</Switch>
<Logout />
</div>
</div>
)
}
And here is Expenses.tsx, where the fetch call is being made:
export const Expenses = (props: ExpensesProps) => {
const [isLoading, setIsLoading] = useState(true)
const { date } = props
const { user } = useAuth()
const m = date.getMonth() + 1
const s = '0'.concat(m.toString())
const [total, setTotal] = useState<number>(0)
useEffect(() => {
const getTotalExpenses = async() => {
await fetch(`/api/expenses?user_id=${user.info.user_id}&month=${s}`)
.then(response => response.json())
.then(result => {
if (result) {
setTotal(parseFloat(result))
}
})
.then(result => {
setIsLoading(false)
})
}
if (user.info.user_id) {
getTotalExpenses()
}
}, [])
return isLoading ? (
<div>
loading...
</div>
) : (
<div>
{`Your monthly expenses so far are: $${total}.`}
</div>
)
}

My Login component flashes whenever I reload my dashboard

I'm using reactjs to build a login/register system with authentication and authorization. if authenticated(jsonwebtoken), it should route me to the dashboard else redirect me back to login.
but whenever I reload it hits the login endpoint for a second then back to dashboard. how can I fix this?
Below is a giphy to show what I'm talking about
Here are the components associated with the issue stated above
App.js
const App = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false)
// set isAuthenticated to true or false
const setAuth = (boolean) => {
setIsAuthenticated(boolean)
}
useEffect(() => {
// check if the person is still Authenticated
const isAuth = async () => {
try {
const res = await fetch('/auth/verify', {
method: 'GET',
headers: { token: localStorage.token},
})
const data = await res.json()
// if authenticated, then
if(data === true) {
await setIsAuthenticated(true)
} else {
await setIsAuthenticated(false)
}
} catch (err) {
console.error(err.message)
}
}
isAuth()
})
return (
<Fragment>
<Router>
<div className='container'>
<Switch>
<Route exact path='/login' render={props => !isAuthenticated ? <Login {...props} setAuth={setAuth} /> : <Redirect to='/dashboard' /> } />
<Route exact path='/register' render={props => !isAuthenticated ? <Register {...props} setAuth={setAuth} /> : <Redirect to='/login' />} />
<Route exact path='/dashboard' render={props => isAuthenticated ? <Dashboard {...props} setAuth={setAuth} /> : <Redirect to='/login' /> } />
</Switch>
</div>
</Router>
</Fragment>
);
Login Component
const Login = ({ setAuth }) => {
const [text, setText] = useState({
email: '',
password: ''
})
const { email, password } = text
const onChange = e => setText({ ...text, [e.target.name]: e.target.value})
const onSubmit = async (e) => {
e.preventDefault()
try {
// Get the body data
const body = { email, password }
const res = await fetch('/auth/login', {
method: 'POST',
headers: {"Content-Type": "application/json"},
body: JSON.stringify(body)
})
const data = await res.json()
if(data.token) {
// save token to local storage
localStorage.setItem("token", data.token)
setAuth(true)
toast.success('Login Successful')
} else {
setAuth(false)
toast.error(data)
}
} catch (err) {
console.error(err.message)
}
}
return (
<Fragment>
<h1 className='text-center my-5'>Login</h1>
<form onSubmit={onSubmit}>
Dashboard Component
const Dashboard = ({ setAuth }) => {
const [name, setName] = useState('')
useEffect(() => {
const getName = async () => {
try {
const res = await fetch('/dashboard', {
method: 'GET',
// Get the token in localStorage into the header
headers: { token: localStorage.token }
})
const data = await res.json()
setName(data.user_name)
} catch (err) {
console.error(err.message)
}
}
getName()
// eslint-disable-next-line
}, [])
// Log out
const logOut = (e) => {
e.preventDefault()
localStorage.removeItem("token")
setAuth(false)
toast.success('Logged Out')
}
return (
<Fragment>
<h1 className='mt-5'>Dashboard</h1>
<p>Hello, {name}</p>
<button className='btn btn-primary my-3' onClick={e => logOut(e)}>Log Out</button>
</Fragment>
There are two problems that I found in your code above.
The first is that your ueEffect does not specify any dependency.
When the dependencies are not specified in this way the useEffect would run anytime any state changes.
useEffect(()=> {
// code here
}); // this one would run anytime any state changes in the component. You usually don't want this.
When a dependency array is specified, the code in the useEffect would run anytime any of the state in the dependencies changes.
useEffect(()=> {
// code here
},
[state1, state2, ...others...] //code would run when any of the state in this array changes
In your case, however, you probably want to run that useEffect once. To do this we add an empty array as the dependency value.
useEffect(()=> {
// code here
},
[] //empty deps means that the code runs only once. When the component mounts
)
Extra ideas
I also suggest that you add a loading state to your component so that you can show a loader while the API call is being made.
You might want to show a loader while the API call is being made(or even set this state to true by default since the API call is the first thing you do in your app)
.
Also, consider putting useEffect in a custom Hook

Resources