How can I avoid infinite loops in my React Router private routes? - reactjs

In my App.js, I have some authenticated pages I protect with <PrivateRoute>, like so:
<PrivateRoute path="/dashboard">
<Dashboard />
</PrivateRoute>
I implement <PrivateRoute> like so:
function PrivateRoute({ children, ...rest }) {
return (
<Route {...rest} render={() => <CheckRedirect children={children} />} />
);
}
The problem is, the <CheckRedirect> function calls out to an endpoint on my server which dynamically tells you where to redirect.
Here's the function:
export const CheckRedirect = ({ children }) => {
const [isChecking, setIsChecking] = React.useState(true);
const [target, setTarget] = React.useState(null);
const url = "https://example.com/get-redirect"
useEffect(() =>{
async function getPage() {
axios.get(url)
.then(function (response) {
setTarget(response.data.message)
}).finally(() => setIsChecking(false))
}
getPage();
}, []);
if (isChecking) {
return "... Checking";
}
return {target} ? (
<Redirect to={target} />
) : (
<Redirect to='/404' />
);
};
If you're not logged in, it will send back "/login" in the message field. If you're logged in, it will send "/dashboard".
If it sends back "/dashboard", then React Router produces an infinite loop! It tries the same <PrivateRoute> again, which calls out to the endpoint again, which will once again return "/dashboard", and so on...
Is there a way I can tell my <PrivateRoute> to not do the <CheckRedirect> function if this is already the result of a redirect?

I haven't tested it myself, but have you tried passing path as a prop to CheckRedirect and only do the setTarget in your getPage fetch if it returns a different route?
function PrivateRoute({ children, path, ...rest }) {
return (
<Route {...rest} render={() => <CheckRedirect children={children} path={path} />} />
);
}
export const CheckRedirect = ({ children, path }) => {
const [isChecking, setIsChecking] = React.useState(true);
const [target, setTarget] = React.useState(null);
const url = "https://example.com/get-redirect"
useEffect(() =>{
async function getPage() {
axios.get(url)
.then(function (response) {
const newPath = response.data.message
if (path !== newPath) {
setTarget(newPath)
}
}).finally(() => setIsChecking(false))
}
getPage();
}, []);
if (isChecking) {
return "... Checking";
}
return {target} ? (
<Redirect to={target} />
) : (
<Redirect to='/404' />
);
};
To avoid CheckRedirect to do any redirect if everything is ok (ie. it's a valid request for that route), ensure CheckRedirect actually returns null in that case. If you have control over the server response, I'd return a different value (not null, but -1 for example) for non-existent routes (ie. to redirect to 404), and keep null for when you really just want to return null.

In CheckRedirect component, you don't even use children prop. It renders a string and then redirects to a page. It's normal that it loops forever. Pass path as a prop to CheckRedirect component and if it's same as server response, render the children.
Add path prop and pass it:
export const CheckRedirect = ({ children, path }) => {
Add your conditional before redirecting:
if (target === path) {
return children
}

Just change your PrivateRoute Logic to something like this
const PrivateRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render = { props =>
user.isOnline ? ( <Component {...props} /> ) :
(
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
)
}
then
<PrivateRoute exact path="/dashboard" component={Dashboard} />

Related

How to test component that depend on config set on app.js?

I'm trying to run a test that checks if the history.goBack has been called, by using jest.fn. I have set my routes in app.js with an outer FirebaseConfigProvider
function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<FirebaseConfigProvider>
<AutomaticRedirectContextProvider>
<Routes>
<Route exact path={PAGES.HOME} component={HomePage} />
<Route path={PAGES.CONFIRMATION} component={ConfirmationPage} />
</Routes>
</AutomaticRedirectContextProvider>
</FirebaseConfigProvider>
</ThemeProvider>
);
}
export default App;
And I'm using the createMemoryHistory() to set initialEntries with pathname and state, however the test seems to ignore the configuration from FirebaseConfigProvider, set in app.js.
confirm.js
export default function ConfirmationPage() {
const handleGoBack = () => {
// this line works when running the app, but breaks when running the test
firebase.analytics().logEvent('click_back_button', firebaseAnalyticsData);
history.goBack();
};
return (
<div>...</div>
);
}
confirm.test.js
import { mockPerson } from '../mocks/person';
const mockHistoryGoBack = jest.fn();
async function setupPage() {
await wait(async () => {
const history = createMemoryHistory({initialEntries: [
{
pathname: PAGES.CONFIRMATION.replace(
':id',
1
),
state: mockPerson
}
]});
history.goBack = mockHistoryGoBack;
render(
<Router history={history}>
<ConfirmationPage />
</Router>
);
});
}
describe('View/Pages/Confirmation', () => {
it('calls history.goBack when back button is clicked', async () => {
await setupPage();
const backButton = screen.getByTestId(HISTORY_BACK_ID);
fireEvent.click(backButton);
expect(mockHistoryGoBack).toHaveBeenCalled();
});
}
I have also tried with but got the same results. Error "TypeError: _firebase.default.analytics is not a function" on this line from confirm.js: firebase.analytics().logEvent('click_back_button', firebaseAnalyticsData);
What am I missing?

PrivateRoute always redirects to sign in page when refreshing the page; How to wait for useEffect() and localStorage?

I am implementing security and user management in my application and ran into an issue with react router, Context, localStorage.
With router, I have created a private route and I check if the userID exists on Context or localStorage, if userID does not exist then I re-direct the user to the sign in page.
This works great, BUT if I refresh the page, it ALWAYS redirects to the sign in page. Even though the user is logged in, and should exist on localStorage.
I think the issue stems from localStorage and "useEffect()".
My theory is that the router redirects BEFORE localStorage (and useEffect) is done checking if the userID exists.
Not sure how I can go around this?
Here's the code for my Private Route component"
import {
Route,
Redirect,
RouteProps,
} from 'react-router-dom';
import {MyContextProvider, useMyContext} from '../Context/UserContext';
interface PrivateRouteProps extends RouteProps {
// tslint:disable-next-line:no-any
component: any;
UserID: number;
}
const PrivateRoute = (props: PrivateRouteProps) => {
const { component: Component, UserID, ...rest } = props;
console.log("Inside Route: "+ UserID);
return (
<Route
{...rest}
render={(routeProps) =>
UserID > 0 ? (
<Component {...routeProps} />
) : (
<Redirect
to={{
pathname: '/',
state: { from: routeProps.location }
}}
/>
)
}
/>
);
};
export default PrivateRoute;
^ The " console.log("Inside Route: "+ UserID); " prints a 0, so thats why it always redirects to sign in page. This is why I think the useEffect() (shown below in routing) may have something to do with userID not being set properly.
Code for Routing part:
function Routing() {
const classes = useStyles();
const [store, setStore] = useMyContext();
useEffect(() => { // check if user exists in localstorage, if so, set the values on react Context
const res = localStorage.getItem("UserID");
if (res) {
// since LocalStorage can return a type or null, we must check for nulls and set accordingly.
const uID = localStorage.getItem("UserID");
const UserID = uID !== null ? parseInt(uID) : 0;
const un = localStorage.getItem("Username");
const Username = un !== null ? un : '';
const iA = localStorage.getItem("IsAdmin");
const isAdmin = iA != 'true' ? true : false;
const eN = localStorage.getItem("EmailNotifications");
const emailNotifications = eN != 'true' ? true : false;
setStore({ // update the 'global' context with the logged in user data
UserID:UserID,
Username: Username,
IsAdmin: isAdmin,
EmailNotifications: emailNotifications
});
console.log(UserID);
console.log(Username);
console.log(isAdmin);
console.log("foundUser");
}
}, []);
const id = store.UserID; // get the userID from context; to pass into PrivateRoute
console.log("ID: ++"+ id);
return (
<div className="App">
<Router>
<Switch>
<Route exact path="/" component={Login}/>
<PrivateRoute path="/dashboard" UserID={id} component={Dashboard}/>
<PrivateRoute path="/add" UserID={id} component={AddTicket}/>
<PrivateRoute path="/settings" UserID={id} component={Settings}/>
<Route component={NotFound}/>
</Switch>
</Router>
</div>
);
}
export default Routing;
To solve for what I wanted to accomplish, I just added the localStorage retrieval directly inside PrivateRoute (instead of the parent node of PrivateRoute with useEffect)
const PrivateRoute = (props: PrivateRouteProps) => {
const { component: Component, UserID, ...rest } = props;
console.log("Inside Route: "+ UserID);
return (
<Route
{...rest}
render={(routeProps) =>
localStorage.getItem("UserID") ? ( // if UserID == 0 when user is NOT logged in. So, if ID == 0, redirect to signinpage
<Component {...routeProps} />
) : (
<Redirect
to={{
pathname: '/',
state: { from: routeProps.location }
}}
/>
)
}
/>
);
};
Storing the user's id in storage is very bad idea. This is sensitive information. Try to implement logic which will display a spinner or HTML skeleton while waiting to fetch the id from your server or whoever server you are using. The URL might change for a bit but the user will only see the spinner/HTML skeleton and this is way better UX than redirecting and showing pages the user shouldn't see, not to mention the user's id saving in the LocalStorage.

Firebase.auth().onstateChanged() not working after clearing browser cache

I cleared my browser cache and now my app cant login
export function IsUserRedirect({ user, loggedInPath, children, ...rest}){
return (
<Route
{...rest}
render={() => {
if(!user){
return children;
}
if(user){
return (
<Redirect
to={{
pathname: loggedInPath
}}
/>
)
}
return null;
}}
/>
)
}
export function ProtectedRoute({ user, children, ...rest}){
return(
<Route
{...rest}
render={({location}) => {
if(user){
return children;
}
if(!user){
return (
<Redirect
to={{
pathname: 'signin',
state: { from : location}
}}
/>
)
}
return null;
}}
/>
)
}
I think it stored my login info on the browser as a localstorage but after clearing it still recognizes it as the user is logged in and takes me to the next page.
But on the next page i have kept a loading state for getting user data, as it doesnt has any user it just keeps loading and goes nowhere. can someone help
export default function useAuthListener(){
const [user, setUser] = useState(JSON.parse(localStorage.getItem('authUser')));
const {firebase} = useContext(FirebaseContext);
useEffect(() => {
const listener = firebase.auth().onAuthStateChanged((authUser) => {
if(authUser){
localStorage.setItem('authUser', JSON.stringify(authUser));
setUser(authUser);
}else {
localStorage.removeItem('authUser');
setUser(null);
}
});
return ()=> listener();
}, []);
return { user};
}
just a quick suggestion:
localStorage.clear();
Source:
(Another aproach might be a reboot, to see if it acts differently...)
Greetings,
Alexander

React typescript useParams() always returns undefined in useEffect

I am trying to achieve my profile component fetching logged user's data on path /me and fetching someone else's data on path user/:username.
Following example from react router blogs, I came up with something like this:
function App(): JSX.Element {
//...
<PrivateRoute
exact
path="/me"
render={(props) => (
<ProfileComponent {...props} principal={states.username} isMyProfile={true} />
)}
/>
<PrivateRoute
path="/user/:username"
render={(props) => (
<ProfileComponent {...props} principal={states.username} isMyProfile={false} />
)}
/>
// ...
}
interface ParamTypes {
path: string;
}
export default function ProfileComponent
(props: { principal: string; isMyProfile: boolean }): JSX.Element {
const { path } = useParams<ParamTypes>();
useEffect(() => {
async function fetch(username: string) {
// ...
}
props.isMyProfile ? fetch(props.principal) : fetch(path);
}, [props.principal, path, props.isMyProfile]);
// ...
}
but the path is always undefined. What am I missing?
May be this can help you:
const { username } = useParams();
useEffect(()=>{
console.log(username)}
,[username]);
in case it's your profile then userName should be undefined otherwise you will get some value

Simulate localStorage data in test

I am using Create React App.
I am trying to simulate isLoggedIn behaviour in my component to get all lines code coverage.
To do that localStorage key: user must exist with data.accessToken
I tried set localStorage data in the test but it is not working. the same method actually working in isLoggedIn function and generate 100% line coverage.
isLoggedIn function
export const isLoggedIn = () => {
const userFromLocalStorage = store('user');
return _get(userFromLocalStorage, 'data.accessToken', false);
};
PrivateRoute.js:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
isLoggedIn() ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: 'login' }} />
)
}
/>
);
PrivateRoute.spec.js
import store from 'store2';
describe('PrivateRoute Logged In', () => {
store('user', {
data: {
accessToken: 'dfg',
},
});
const ShallowPrivateRoute = shallow(
<PrivateRoute path="/" name="Home" component={TestComponent} />
);
it('should cover logged in case', () => {
expect(ShallowPrivateRoute).toBeDefined();
});
});
Is there the way I can mock isLoggedIn function to return true just for one test??
What is the best way to test that kind of behaviour?
You could mock the entire file like this:
jest.mock("you-module", () =>({...methodsMock}));
or you could recieve isLoggedIn in props, that way you only need to pass a mock function when you render your component in test.
<Component isLoggedIn={jest.fn().mockReturnValue(true)} />

Resources