Firebase onAuthStateChanged not running as expected in React App - reactjs

So i am building a React+redux app using firebase. I'm using react-router onEnter callback function(checkAuth) for route protection.
export default function getRoutes (checkAuth) {
return (
<Router history={browserHistory}>
<Route path='/' component={MainContainer}>
<IndexRoute component = {HomeContainer} onEnter = {checkAuth}/>
<Route path='auth' component = {AuthenticateContainer} onEnter = {checkAuth} />
<Route path='feed' component = {FeedContainer} onEnter = {checkAuth} />
<Route path='logout' component = {LogoutContainer} />
</Route>
</Router>
)
}
the checkAuth function calls checkIfAuthed function to see if there is a currentUser.
function checkAuth (nextState, replace) {
const isAuthed = checkIfAuthed(store)
console.log('isAuthed from checkAuth method', isAuthed)
const nextPathName = nextState.location.pathname
console.log('nextPathName', nextPathName)
// debugger
if (nextPathName === '/' || nextPathName === 'auth') {
if (isAuthed === true) {
// debugger
replace('feed')
}
} else {
// debugger
if (isAuthed !== true) {
// debugger
replace('auth')
}
}
}
ReactDOM.render(
<Provider store = {store}>
{getRoutes(checkAuth)}
</Provider>,
document.getElementById('app')
)
The checkIfAuthed function looks like this:
export function checkIfAuthed (store) {
// debugger
// const user = firebase.auth().currentUser
firebase.auth().onAuthStateChanged((user) => {
console.log('isAuthed from on state changed', user)
// debugger
if (user === null) {
// debugger
return false
} else if (store.getState().isAuthed === false) {
// debugger
const userInfo = formatUserInfo(user.displayName, user.photoURL, user.uid)
// debugger
store.dispatch(authUser(user.uid))
// debugger
store.dispatch(fetchingUserSuccess(user.uid, userInfo))
// debugger
return true
}
// debugger
return true
})
}
However, const isAuthed is always undefined at runtime in the checkAuth() function.thus leading to replace("feed") never running. I was expecting it to be false or true.
Additionally if I instead use const user = firebase.auth().currentUser in the checkIfAuthed function the Replace function runs but this requires user to hit login button, whereas the firebase observer above automatically runs.

You can check here this implementation
The Service when we read the user auth and set the value to Redux https://github.com/x-team/unleash/blob/develop/app/services/authService.js
The reducer when set the user state to the redux state object https://github.com/x-team/unleash/blob/develop/app/reducers/userReducer.js
The action creators https://github.com/x-team/unleash/blob/develop/app/actions/UserActions.js
The login state check
https://github.com/x-team/unleash/blob/develop/app/components/UnleashApp.jsx#L17
The most important part is the authService, let me know any question

Related

React state not changing in component

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

Public/private routing. React has detected a change in the order of Hooks called

I have component AppRouter which is placed inside <BrowserRouter> and returns public or private <Route> components depending on whether the user is authenticated. Private routes are returning inside <PageWrapper> component. It contains header and sidebar, so routes are rendering in main part of this wrapper.
I am having those exceptions: React has detected a change in the order of Hooks called by AppRouter. & Rendered more hooks than previous render
Console
This is AppRouter:
export const AppRouter = () => {
const user = useAppSelector(state => state.authReducer.user)
const dispath = useAppDispatch();
useEffect(() => {
const userData = getUser()
if (userData !== null) {
dispath(authSlice.actions.updateUser(userData))
}
}, [])
const routes: IRoute[] = user === undefined ? publicRoutes : privateRoutes
let indexElement: IRoute | undefined
const routeComponents = (
<Routes>
{routes.map<React.ReactNode>((route: IRoute) => {
if (route.index === true) {
indexElement = route
}
return <Route
path={route.path}
element={route.component()}
key={route.path}
/>
})}
{
indexElement !== undefined && <Route path='*' element={<Navigate to={indexElement.path} replace />} />
}
</Routes>
)
if (user === undefined) {
return routeComponents
}
return (
<PageWrapper>
{routeComponents}
</PageWrapper>
)
}
This exception is thrown when user is authenticated and react renders component from private route(private route is only one now). It started to throw when I added useEffect(arrow function with console.log and empty dependecies array) to this private component. If i remove useEffect from this component - exceptions will not be thrown. I tried to change routes.map to privateRoutes.map - then exceptions does not throw, but I can't understand the reason why it works so.
Project is react + typescript + redux toolkit

Props lost after navigate and cloneElement

In an app I'm currently working on, the authentification has been done like this, to pass user's data into children components (in App.js, I would have rather used useContext to access user data in whatever component) [App.js]:
const RequireAuth = ({ children }) => {
// fetch user data from database, ...
return <>{React.cloneElement(children, { user: {...user, ...userExtraData} })}</>;
};
Then, an example of a Route is specified as [App.js]:
<Route
path="/addgame"
element={
<RequireAuth>
<FormAddGame />
</RequireAuth>
}
/>
However, my current problem is the following:
From a ComponentA, I want to navigate to /addgame (=FormAddGame component, see below) while setting an existingGame prop. Thus, I use [ComponentA.js]:
let navigate = useNavigate()
navigate('addgame', { existingGame: game })
The said FormAddGame component is [FormAddGame.js]:
function FormAddGame({ user }) {
const { existingGame } = useLocation()
console.log(existingGame)
...
}
export default FormAddGame;
However, while I correctly navigate to /addgame, the existingGame prop stays undefined once in FormAddGame while it should be not (as game is not undefined)
Try passing props like this:
navigate("addgame", { state: { existingGame: game } });
and destructure like this:
const { state: { existingGame } = {} } = useLocation();

Component not updating on state change with React Router

I'm using React and React Router. I have all my data fetching and routes defined in App.js.
I'm clicking the button in a nested child component <ChildOfChild /> which refreshes my data when clicking on a button (passed a function down with Context API) with a fetch request happening in my top component App.js (I have a console.log there so it's fetching on that click for sure). But the refreshed state of data never arrives at the <ChildOfChild /> component. Instead, it refreshes the old state. What am I doing wrong. And how can I ensure my state within <Link>is refreshing on state update.
I expect the item.name value to be updated on button click.
App component
has all the routes and data fetching
uses Reacts Context API, which I use to pass my fetching to child components
below the basic shape of the App component.
import React, {useEffect, useState} from "react";
export const FetchContext = React.createContext();
export const DataContext = React.createContext();
const App = () => {
const [data, setData] = useState([false, "idle", [], null]);
useEffect(() => {
fetchData()
}, []);
const fetchData = async () => {
setData([true, "fetching", [], null]);
try {
const res = await axios.get(
`${process.env.REACT_APP_API}/api/sample/`,
{
headers: { Authorization: `AUTHTOKEN` },
}
);
console.log("APP.js - FETCH DATA", res.data)
setData([false, "fetched", res.data, null]);
} catch (err) {
setData([false, "fetched", [], err]);
}
};
return (
<Router>
<DataContext.Provider value={data}>
<FetchContext.Provider value={fetchData}>
<Switch>
<Route exact path="/sample-page/" component={Child} />
<Route exact path="/sample-page/:id" component={ChildOfChild} />
</Switch>
</FetchContext.Provider>
</DataContext.Provider>
</Router>
)
}
Child component
import { DataContext } from "../App";
const Child = () => {
const [isDataLoading, dataStatus, data, dataFetchError] = useContext(DataContext);
const [projectsData, setProjectsData] = useState([]);
{
data.map((item) => (
<Link
to={{
pathname: `/sampe-page/${item.id}`,
state: { item: item },
}}
>
{item.name}
</Link>
));
}
Child of Child component
import { FetchContext } from "../App";
const ChildOfChild = (props) => {
const getData = useContext(FetchContext);
const [item, setItem] = useState({});
const [isItemLoaded, setIsItemLoaded] = useState(false);
useEffect(() => {
if (props.location.state.item) {
setItem(props.location.state.item);
setIsItemLoaded(true);
}
}, [props]);
return (
<div>
<button onClick={() => getData()}Refresh Data</button>
<div>{item.name}</div>
</div>
)
}
Issue
The specific data item that ChildOfChild renders is only sent via the route transition from "/sample-page/" to "/sample-page/:id" and ChildOfChild caches a copy of it in local state. Updating the data state in the DataContext won't update the localized copy held by ChildOfChild.
Suggestion
Since you are already rendering ChildOfChild on a path that uniquely identifies it, (recall that Child PUSHed to "/sample-page/${item.id}") you can use this id of the route to access the specific data item from the DataContext. There's no need to also send the entire data item in route state.
Child
Just link to the new page by item id.
<Link to={`/sampe-page/${item.id}`}>{item.name}</Link>
ChildOfChild
Add the DataContext to the component via useContext hook.
Use props.match to access the route's id match param.
import { FetchContext } from "../App";
import { DataContext } from "../App";
const ChildOfChild = (props) => {
const getData = useContext(FetchContext);
const [,, data ] = useContext(DataContext);
const [item, setItem] = useState({});
const [isItemLoaded, setIsItemLoaded] = useState(false);
useEffect(() => {
const { match: { params: { id } } } = props;
if (id) {
setItem(data.find(item => item.id === id));
setIsItemLoaded(true);
}
}, [data, props]);
return (
<div>
<button onClick={getData}Refresh Data<button />
<div>{item?.name}<div>
</div>
)
}
The useEffect will ensure that when either, or both, the data from the context or the props update that the item state will be updated with the latest data and id param.
Just a side-note about using the Switch component, route path order and specificity matter. The Switch will match and render the first component that matched the path. You will want to order your more specific paths before less specific paths. This also allows you to not need to add the exact prop to every Route. Now the Switch can attempt to match the more specific path "/sample-page/123" before the less specific path "/sample-page".
<Router>
<DataContext.Provider value={data}>
<FetchContext.Provider value={fetchData}>
<Switch>
<Route path="/sample-page/:id" component={ChildOfChild} />
<Route path="/sample-page/" component={Child} />
</Switch>
</FetchContext.Provider>
</DataContext.Provider>
</Router>
I've just rewrote your code here, I've used randomuser.me/api to fetch data
Take a look here, it has small typo errors but looks ok here
https://codesandbox.io/s/modest-paper-nde5c?file=/src/Child.js

How connect Context with Redirect

I want to send information to second page if the user is logged in . I would like use Context to that.
Something about my code :
const Login = () => {
...
const [logged, setLogged] = React.useState(0);
...
const log = () => {
if (tempLogin.login === "Login" && tempLogin.password == "Haslo") {
setLogged(1);
}
...
return (
{logged == 1 && (
<Redirect to="/page" />
)}
I want to send logged to /page but i don't know how . None guide help me .Page is actually empty React file.
There are 2 ways handle that:
Passing state to route(as described in docs):
{logged == 1 && (
<Redirect to={{ path: "/page", state: { isLoggedIn: true } }} />
)}
And your component under /page route will access that flag as this.props.location.state.isLoggedIn
Utilize some global app state(Redux, Context API with <Provider> at root level or anything alike).
To me second option is better for keeping auth information:
Probably not only one target component will want to check if user is authorized
I'd expect you will need to store some authorization data to send with new requests(like JWT token) so just boolean flah accessible in single component would not be enough.
some operation on auth information like say logout() or refreshToken() will be probably needed in different components not in single one.
But finally it's up to you.
Thanks skyboyer
I solved this problem with Context method .I will try tell you how i do this becouse maybe someone will have the same problem
I created new file
import React from "react";
import { Redirect } from "react-router";
const LoginInfo = React.createContext();
export const LoginInfoProvider = props => {
const [infoLog, setInfoLog] = React.useState("");
const login = name => {
setInfoLog(name);
};
const logout = () => {
setInfoLog("old");
};
const { children } = props;
return (
<LoginInfo.Provider
value={{
login: login,
logout: logout,
infolog: infoLog
}}
>
{children}
</LoginInfo.Provider>
);
};
export const LoginInfoConsumer = LoginInfo.Consumer;
In App.js add LoginInfoProvider
<Router>
<LoginInfoProvider>
<Route exact path="/" component={Login} />
<Route path="/register" component={Register} />
<Route path="/page" component={Page} />
</LoginInfoProvider>
</Router>
In page with login (parts of code in my question) i added LoginInfoConsumer
<LoginInfoConsumer>

Resources