I'm writing an app that pulls my GitHub repos and displays certain information from them (descriptions, etc.). The app is set up so that if I am not 'initialized' (meaning I haven't sent up an account), then I am forced to the 'CreateAccount' page. The state of being 'initialized' is held in local storage. But, if anyone else wants to look at the app, they won't be 'initialized', so I need a second check to see if there is anything in the database (records for each repo). If so, then the viewer should be 'initialized' and sent to the Home page. All of this is done in the App component:
const App = () => {
const setupCtx = useContext(SetupContext);
const devCtx = useContext(DevDataContext);
// variable to control routing
let initialized = null;
if (localStorage.getItem("jtsy-signin") === "true") {
initialized = true;
} else {
API.findRepo()
.then((repo) => {
console.log('APP found 1 repo', repo)
if (repo) {
initialized = true
localStorage.setItem('jtsy-signin', 'true')
console.log('1 REPO initialized', initialized)
} else {
initialized = false;
}
})
}
useEffect(() => {
if (initialized) {
console.log('APP useEffect signin=true, redirect to Home page');
if (localStorage.getItem('jtsy-login') === 'true') {
setupCtx.updateLoggedIn();
}
// console.log('APP devUpdated', setupCtx.state.devUpdated)
if (setupCtx.state.devUpdated) {
API.getActiveDevData().then((activeDevData) => {
// console.log('APP activeDevData', activeDevData.data);
const developerData = {
developerLoginName: activeDevData.data.developerLoginName,
developerGithubID: activeDevData.data.developerGithubID,
repositories: activeDevData.data.repositories,
fname: activeDevData.data.fname,
lname: activeDevData.data.lname,
email: activeDevData.data.email,
linkedInLink: activeDevData.data.linkedInLink,
resumeLink: activeDevData.data.resumeLink,
active: true
}
console.log('APP after DB call', developerData)
devCtx.updateDev(developerData)
setupCtx.updateInitialized();
setupCtx.updateDevUpdated(false)
})
}
};
}, [setupCtx.state.devUpdated])
return (
<div className='App'>
<React.Fragment>
<Router>
<Switch>
{console.log('IN APP SWITCH initialized', initialized)}
{initialized ? (
<Route exact path="/" component={Home} />
) : (
<Route exact path="/" component={CreateAccountComp} />
)}
<Route exact path="/contact" component={Contact} />
<Route exact path="/about" component={About} />
<Route exact path="/developer" component={Developer} />
<Route exact path="/login" component={LoginModal} />
<Route exact path="/logout" component={LogoutModal} />
<Route exact path="/signin" component={CreateAccountComp} />
<Route exact path="/settings" component={Settings} />
<Route component={NoMatch} />
</Switch>
</Router>
</React.Fragment>
</div >
);
};
export default App;
The variable 'initialized' controls whether the Home or CreateAccount pages are rendered. As the code shows, first local storage is checked, and if there is a value there, 'initialized' is set to 'true' and the Home page renders. This works fine. But, if there is no value in local storage, I next call findRepo(), which executes a .findOne() in the Mongo database. Then, the value of 'initialized' is set accordingly. The delay in getting a response from the database is too long. If there is no value in local storage, initialized is 'null' when rendering occurs, so the app always goes to the "CreateAccount" page. I can tell that the database call is responding properly and initialized is getting set to 'true', but it's too late (CreateAccount is already rendered).
I tried putting the database call inside of useEffect, and in a separate useEffect, but neither worked. I need a better approach to make this work.
So I have went through your code a little and came up with a solution to add a loading flag to state and use that to render a loading screen if we are waiting on any sort of update.
You could make this loading screen anything you wanted to show when waiting for updates.
Below is the code changes made all in App.js
// add isLoading flag to state
const [state, setState] = useState(
{
loggedIn: null,
sync: false,
initialized: null,
isLoading: true
}
)
Anytime setState is called, update the isLoading flag to true.
// line 55 in App.js
setState({
...state,
initialized: true,
isLoading: false
})
// line 75
setState({
...state,
initialized: true,
isLoading: false
})
// line 103
setState({
...state,
initialized: false,
isLoading: false
})
Then the return update.
{state.isLoading && <div>Loading...</div>}
<Route exact path="/" component={state.initialized ? Home : CreateAccountComp} />
With these changes the loading screen will show when we are waiting on any sort of update to initialized. Then the home screen will once it is updated to true or the create account page will show if it is updated to false.
Related
I'm trying to create protected routes that are only viable while user is logged in, but I have trouble getting loggedIn state in ProtectedRoutes component, it's always set to false thus redirecting to "/login". What am I not getting correctly here?
App.tsx
interface loginContextInterface {
loggedIn: boolean;
setLoggedIn: (value: (((prevState: boolean) => boolean) | boolean)) => void;
}
export const LoginContext = createContext({} as loginContextInterface);
export default function App() {
const [ loggedIn, setLoggedIn ] = useState(false)
useEffect(() => {
console.log("before", loggedIn)
isLoggedIn().then((r) => {
console.log("R", r)
setLoggedIn(r)
})
console.log("after", loggedIn)
}, [loggedIn])
return (
<LoginContext.Provider value={{loggedIn, setLoggedIn}}>
<Router>
<MenuHeader />
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/tasks" element={<ProtectedRoutes/>}>
<Route path="/" element={<Tasks/>}/>
</Route>
<Route path="/login" element={<Login />}/>
<Route path="/logout" element={<Logout />}/>
<Route path="/register" element={<Register/>}/>
</Routes>
</Router>
</LoginContext.Provider>
);
}
ProtectedRoutes.tsx
export const ProtectedRoutes = () =>{
const location = useLocation();
const {loggedIn} = useContext(LoginContext)
console.log("protected", loggedIn)
return (
loggedIn ? <Outlet/> : <Navigate to={"/login"} replace state={{location}}/>
);
}
Edit:
isLoggedIn just authenticates that the user is logged in via cookie using api on the server side. Added logging
Produces these after trying to access /tasks route and redirecting me to /login again
VM109:236 protected false
App.tsx:21 before false
App.tsx:26 after false
App.tsx:21 before false
App.tsx:26 after false
2App.tsx:23 R true
App.tsx:21 before true
App.tsx:26 after true
App.tsx:23 R true
There is an issue with the useEffect hook, using the loggedIn state value as the dependency. You should not use dependencies that are unconditionally updated by the hook callback. My guess here is that you wanted to do an initial authentication check when the app mounts. You can remove loggedIn from the dependency since it's not referenced at all.
useEffect(() => {
isLoggedIn().then(setLoggedIn);
}, []);
I suggest also using an initial loggedIn state value that doesn't match either the authenticated or unauthenticated states, i.e. something other than true|false. This is so the ProtectedRoutes can conditionally render null or some loading indicator while any pending authentication checks are in-flight and there isn't any confirmed authentication state already saved in state.
Update the context to declare loggedIn optional.
interface loginContextInterface {
loggedIn?: boolean;
setLoggedIn: React.Dispatch<boolean>;
}
Update App to have an initially undefined loggedIn state value.
const [loggedIn, setLoggedIn] = useState<boolean | undefined>();
Update ProtectedRoutes to check for the undefined loggedIn state to render a loading indicator and not immediately bounce the user to the login route.
const ProtectedRoutes = () => {
const location = useLocation();
const { loggedIn } = useContext(LoginContext);
if (loggedIn === undefined) {
return <>Checking authentication...</>; // example
}
return loggedIn ? (
<Outlet />
) : (
<Navigate to={"/login"} replace state={{ location }} />
);
};
Also in App, remove/move the "/tasks" path from the layout route rendering the ProtectedRoutes component to the nested route rendering the Tasks component. The reason is that it's invalid to nest the absolute path "/" in "/tasks".
<Route element={<ProtectedRoutes />}>
<Route path="/tasks" element={<Tasks />} />
</Route>
It's not recommended to reset the dependency inside the useEffect(), it may cause an infinite loop.
useEffect(() => {
// loggedIn will be update here and trigger the useEffect agan
isLoggedIn().then((r) => setLoggedIn(r))
}, [loggedIn])
What does the console.log(loggedIn) and console.log(r) show? I'm guessing isLoggedIn returns false, loggedIn is set to false initially so useEffect not being triggered again and it remains as false
I'm trying to create a callable component to check if device is mobile or desktop
import { useMediaQuery } from 'react-responsive'
const CheckDeviceScreen = () => {
return useMediaQuery({
query: '(min-device-width: 1224px)'
})
}
export default CheckDeviceScreen;
Then I'm passing that props to a Route
import CheckDeviceScreen from "./Others/CheckDeviceScreen";
<Route exact path="/"> <Home isDesktop = {<CheckDeviceScreen/>}/></Route>
then in my Home page
const Home = ({isMobile}) => (
{!isMobile &&
//display something
}
)
But isMobile always return true even I'm viewing it at different device. What am I doing wrong?
What you created is not called a "callable component" but simply just a function. Because react components should return JSX.
Keeping that in mind, then in your code should be
import CheckDeviceScreen from "./Others/CheckDeviceScreen";
<Route exact path="/"> <Home isDesktop = {CheckDeviceScreen()}/></Route>
if you tried console.log(<CheckDeviceScreen />) vs console.log(CheckDeviceScreen()) you will find that the first returns an object which will always have a true condition when checked with !isMobile. But the other console.log will return the returned calling of the function, which is the check condition that you want: true or false
I'm currently working on a project with Firebase Integration and React.
I can register and login, but I do want some Guard in order to have access to the "connected" pages when I'm connected, or to be redirect to the default page for disconnected state if I'm not.
I coded the following for having the different Routes and set it in an App.tsx file :
const ConnectedRoute: React.FC<RouteProps> = ({ component, ...rest }) => {
var route = <Route />
var connected = isConnected()
console.log(connected)
if (connected){
route = <Route {...rest} component={component}/>
}
else{
route = <Route {...rest} render={() => <Redirect to ="/welcome" />} />
}
return route
}
const UnconnectedRoute: React.FC<RouteProps> = ({ component, ...rest }) => {
var route = <Route />
var connected = isConnected()
console.log(connected)
if (connected){
route = <Route {...rest} render={() => <Redirect to ="/home" />} />
}
else{
route = <Route {...rest} component={component}/>
}
return route
}
const App: React.FC = () => {
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/" render={() => <Redirect to="/welcome" />} />
<UnconnectedRoute path="/welcome" component={UnconnectedHome} exact />
<ConnectedRoute path="/home" component={ConnectedHome} exact />
<UnconnectedRoute path="/login" component={Login} exact />
<UnconnectedRoute path="/register" component={Register} exact />
<ConnectedRoute path="/events/create" component={CreateEvent} exact />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
)
};
And for the firebase initialization I did the following :
export async function initializeApp(){
console.log("Checking if app is initialized...")
if (firebase.apps.length == 0){
console.log("App initializing...")
firebase.initializeApp(firebaseConfig);
await firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL)
}
}
And the function to know if a user is connected (as a cookie) is this one :
export function isConnected(){
const res = firebase.auth().currentUser
console.log(res)
return res !== null
}
Still, when I reload the tab, it's always returning me FALSE !
So, I was wondering how could I init the firebase server before the launch of the App ? Is this the current problem ? I currently do not have any clue about it and it frustrates me so much...
If you have already encountered such a problem, that would really help me !
Thank you !
Firebase Authentication persists the user's authentication state in local storage. But when you load a new page (or reload the existing one), it has to check with the server whether the authentication state is still valid. And this check takes some time, during which there is no current user yet.
The typical way to get around such race conditions is to use an auth state listener to detect the current user:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in, redirect to "connected" pages
} else {
// No user is signed in, probably require them to sign in
}
});
I tried to answer in comment with the code, it didn't work.
I did this for now (it's not that good but at least it works) :
const connectedPaths =
[
"/home",
"/events/create"
]
const unconnectedPaths =
[
"/welcome",
"/login",
"register"
]
firebase.initializeApp(firebaseConfig);
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
if (!connectedPaths.includes(window.location.pathname)) window.location.href = "/home"
}
else {
if (!unconnectedPaths.includes(window.location.pathname)) window.location.href = "/welcome"
}
});
I have a react application with some routes, for example:
/home
/faq
/profile/1
/post/title-of-post-id-1
All works fine when url have just 1 level (/home, /faq, ecc).
For urls that have 2 or more levels (/profile/1 or /post/title-of-post-id-1) happens that the index.html file are loaded correctly but related resources (js,css) cannot be loaded because the browser sends to server a request with wrong path i.e.
GET http://HOST/profile/resources/js/main.js
instead of
GET http://HOST/resources/js/main.js
The applications loads route with this code:
<Route exact path="/" component={HomePage} />
{Object.keys(loadableRoutes).map(path => {
const { exact, ...props } = loadableRoutes[path]
props.exact = exact === void 0 || exact || false // set true as default
return <Route key={path} path={path} {...props} />
})}
and
const loadableRoutes = {
'/home': {
component: loadable(() => import('/Home/homepage.js')),
},
'/profile/:uid': {
component: loadable(() => import('/Profile/profile.js')),
exact: true,
},
'/post/:posId': {
component: loadable(() => import('/Post/post.js')),
exact: true,
},
How I can indicate the correct "basename" attribute for these urls?
Thanks
I solved issue adding this line to "index.html":
<base href="/">
My application will have 2 roles, Employee and Admin.
I'm trying to implement middleware so users get redirected if they are not authorized to see the content. Is handling not just general authentication but also user roles in React Router good practice?
My first thought was to add a custom role attribute to firebase.auth().currentUser, but adding attributes to currentUser is not allowed by firebase.
If so, how would I do it?
Through state or fetching it from my Firebase DB like this?:
var requireEmp = (nextState, replace, next) => {
var role;
var uid = firebase.auth().currentUser.uid;
firebase.database().ref('/users/' + uid + '/role').once('value').then((user) => {
role = user.val().role;
});
if (role !== 'employee') {
replace('/');
}
next();
};
...
<Router history={hashHistory}>
<Route path="/" >
<Route path="home" component={Main} onEnter={requireLogin}>
<Route path="work" component={Work} onEnter={requireEmp}/>
<Route path="profile" component={Profile} />
<IndexRoute component={Profile}/>
</Route>
</Route>
</Router>
I'm new to React and Redux and still a bit scared of working with state and important data such as the user role attribute.
What are some other areas I need to be really careful with concerning the implementation of user roles?
Thanks.
lets get that user roles working! Every project has its specificities, but here's how I would do it:
Before you first render your app, you've got to be sure that firebase user/currentUser/currentAuth has loaded. If you have roles, just make sure to fetch it it the user is logged in.
Here's an example:
On index.jsx:
import { initializeApp } from './myFirebase';
const routes = routesConfig(store);
let appHasRendered = false;
const renderAppOnce = () => {
if (appHasRendered) return;
render(
<Provider store={store}>
<Router history={syncedHistory} routes={routes} />
</Provider>,
document.getElementById('app')
);
appHasRendered = true;
};
initializeApp(renderAppOnce, store.dispatch);
and then on myFirebase.js:
export const initializeApp = (renderAppOnce, dispatch) => {
firebaseAuth.onAuthStateChanged((user) => {
if (user) {
// We have a user, lets send him and his role to the store
firebaseRef.child('users/roles').once('value', (snap) => {
dispatch(authLoggedIn({
...user.toJSON(),
role: snap.val() || 'employee'
}));
renderAppOnce();
});
} else {
// There's no user, let's move on
dispatch(authLoggedOut());
renderAppOnce();
}
});
};
All right!!! We have all we need in our store. So now we just have to check that on our onEnter functions of our app:
const routesConfig = (store) => {
// Confirms user is not authenticated
const confirmNoAuth = (nextState, replace) => {
if (store.getState().user) {
replace({ pathname: '/', state: { nextPathname: nextState.location.pathname } });
}
};
// Confirms user is authenticated
const confirmAuth = (nextState, replace) => {
if (!store.getState().user) {
replace({ pathname: '/', state: { nextPathname: nextState.location.pathname } });
}
};
// Confirms user has a specific role
const confirmRole = role => ((nextState, replace) => {
if (store.getState().user.role !== role) {
replace({ pathname: '/', state: { nextPathname: nextState.location.pathname } });
}
});
return (<Route path="/">
<IndexRoute component={HomePage} />
<Route path="login" component={LoginPage} onEnter={confirmNoAuth} />
<Route path="dasboard" component={DashboardPage} onEnter={confirmAuth} />
<Route path="adminsonly" component={AdminDashboardPage} onEnter={confirmRole('admin')} />
</Route>);
};
Theres probably tons of problems on this code, but I believe you can understand the principles. Basically you should pre-fetch the role so you don't have to do it on every route change.
One other tip I can give you is that if you'll have tons of employees and just a handful of admins, just save the admins. This way you'll only have like 20 entries on your roles object instead of hundreds of thousands. That tiny || 'employees' can save you lots of space.
Keep in mind that you can just as easily add more roles if you need. Also, this example uses Redux, but you don't have to.
!!! IMPORTANT !!!
All of this will only keep people from accessing the pages, but smartypants can use the console or a rest client to try to stick their noses in parts of your database where they shouldn't! Be sure to understand and make good use of firebase rules to keep your database secure!
Let me know if it worked