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
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?
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.
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
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
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)} />