I am working on a React app with JWT authentication. In order to set the user object in state, I first need to grab the user._id from the token in their local storage, and then run a fetch call for the entire user object fresh from the backend/MongoDB.
I am trying to do this all in a componentDidMount inside my App.jsx, however my backend controller function console.logs the req.params.id (where I attached the user._id) as null, and fails to grab the user object from the DB, returning 'null' to the frontend.
App.jsx:
export default class App extends Component {
state = {
user: false,
}
setUserInState = (incomingUserData) => {
this.setState({user: incomingUserData})
}
async componentDidMount() {
let token = localStorage.getItem('token')
if (token) {
const payload = JSON.parse(atob(token.split('.')[1]))
if (payload.exp < Date.now() / 1000) {
localStorage.removeItem('token')
token = null
} else {
try {
console.log('payload.user: ' + JSON.stringify(payload.user))
let userId = payload.user._id
await fetch(`/api/users/getProfile/${userId}`)
.then(res => res.json())
.then(data => console.log('DATA: ' + JSON.stringify(data)))
.then(data => this.setState({user: data}))
} catch (error) {
console.log(error)
}
}
}
}
render() {
return (
<div className="App">
<Navbar user={this.state.user} setUserInState={this.setUserInState}/>
<Routes>
<Route path='/ideas/create' element={<NewIdea user={this.state.user} setUserInState={this.setUserInState}/>}/>
<Route path='/users/getProfile/:userId' element={<UserProfile user={this.state.user} setUserInState={this.setUserInState}/>}/>
<Route path='/users/editProfile' element={<EditProfile user={this.state.user} setUserInState={this.setUserInState}/>}/>
<Route path='/publishers/becomePublisher' element={<BecomePublisher user={this.state.user} setUserInState={this.setUserInState}/>} />
<Route path='/users/addSubscription'/>
<Route path='/users/removeSubscription'/>
<Route path='/publishers/show/:id' element={<PubProfile user={this.state.user}/>}/>
<Route path='/ideas/show/:id' element={<PubIdeas user={this.state.user}/>}/>
<Route path='/ideas/ideasFeed/:userId' element={<IdeasFeed user={this.state.user}/>}/>
<Route path='/discover/:id' element={<Discover user={this.state.user} setUserInState={this.setUserInState}/>}/>
<Route path='*' element={<Navigate to='/discover/:id' replace />}/>
</Routes>
</div>
)
}
}
payload.user (userId) console.logs correctly with the user's Id.
While that compDidMount fails to set the user in state, interestingly the useEffect hook in my Discover.jsx component does manage to set the user in state, by doing basically the same thing.
Discover.jsx: (cutting off imports/return statement to save space)
export default function Discover (props) {
const [isActive, setActive] = useState({})
const [publishers, setPublishers] = useState([])
let handleSeeMore = (id) => {
let temp = {...isActive}
temp[id] = !temp[id]
setActive(temp)
}
let getPublishers = async () => {
let token = localStorage.getItem('token')
// if there is a token/the user is signed in then fetch the user object and publishers objects both
if (token) {
let payload = JSON.parse(atob(token.split('.')[1]))
let userId = payload.user._id
let response = await fetch(`/api/publishers/discover/${userId}`)
let data = await response.json()
props.setUserInState(data.user)
setPublishers(data.publishers)
//otherwise just grab the publishers objects
} else {
let userId = 'false'
await fetch(`/api/publishers/discover/${userId}`)
.then(res => res.json())
.then(data => setPublishers(data))
}
}
useEffect(() => {
(async() => {
await getPublishers()
})()
},[])
Related
I am trying to implement Protected Routes in my app. I am using cookie-based session authentication.
The issue is: Whenever I try to access a protected page for the first time, the RequireAuth component has the isAuthenticated value as false and hence it navigates to /.
From the console logs, I can see Inside require auth. before Inside provide auth..
Questions:
Is using useEffect in the context provider the right way to set the auth state?
How do I make sure that the context provider state is set before accessing the context in the consumer RequireAuth?
I have a context provider ProvideAuth which makes an API call to check if the user is already authenticated.
const authContext = createContext();
export function ProvideAuth({ children }) {
const navigate = useNavigate();
const location = useLocation();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userInfo, setUserInfo] = useState({});
const fetchData = async () => {
const isAuthenticated = await CheckAuthentication();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const userInfo = await GetUserInfo();
setUserInfo(userInfo);
}
}
useEffect(() => {
console.log("Inside provide auth. " + isAuthenticated + " " + location.pathname);
fetchData();
}, []);
const value = {
isAuthenticated,
userInfo
};
return <authContext.Provider value={value}>{children}</authContext.Provider>;
}
Auth context consumer
export const useAuth = () => {
return useContext(authContext);
};
I use the context in a RequireAuth component to check if the user is already authenticated and redirect if not.
export default function RequireAuth({ children }) {
const { isAuthenticated, userInfo } = useAuth();
const location = useLocation();
useEffect(() => {
console.log("Inside require auth. " + isAuthenticated + " " + location.pathname);
}, []);
return isAuthenticated === true ?
(children ? children : <Outlet />) :
<Navigate to="/" replace state={{ from: location }} />;
}
The context provider is used in the App.js
return (
<ProvideAuth>
<div className='App'>
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/pricing" element={<Pricing />} />
<Route element={<RequireAuth /> }>
<Route path="/jobs" element={<Jobs />} >
<Route index element={<MyJobs />} />
<Route path="new" element={<NewJob />} />
<Route path=":jobId" element={<JobDetails />} />
<Route path=":jobId/stats" element={<JobStats />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</ProvideAuth>
);
That's because that useEffect in ProvideAuth is as any useEffect an asynchronous task, which means the component and its children may render before its callback gets executed.
A solution is to set up a loading state in ProvideAuth, called for example isCheckingAuth, set to true by default, and to false after you have done all the fetching. And you pass it down to RequireAuth, like so :
const authContext = createContext();
export function ProvideAuth({ children }) {
const navigate = useNavigate();
const location = useLocation();
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userInfo, setUserInfo] = useState({});
const fetchData = async () => {
const isAuthenticated = await CheckAuthentication();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const userInfo = await GetUserInfo();
setUserInfo(userInfo);
}
setIsCheckingAuth(false)
}
useEffect(() => {
console.log("Inside provide auth. " + isAuthenticated + " " + location.pathname);
fetchData();
}, []);
const value = {
isAuthenticated,
userInfo,
isCheckingAuth
};
return <authContext.Provider value={value}>{children}</authContext.Provider>;
}
You use that isCheckingAuth inRequireAuth to show a loader while the fetching is being done, this way:
export default function RequireAuth({ children }) {
const { isAuthenticated, userInfo, isCheckingAuth } = useAuth();
const location = useLocation();
useEffect(() => {
if(isCheckingAuth) return;
console.log("Inside require auth. " + isAuthenticated + " " + location.pathname);
}, [isCheckingAuth]);
if(isCheckingAuth) return <div>Loading...</div>
return isAuthenticated === true ?
(children ? children : <Outlet />) :
<Navigate to="/" replace state={{ from: location }} />;
}
What you can do is check, If the request is processed or not. If processing show loader if any error shows some error msg or redirect. If everything is fine load provider.
const authContext = createContext();
export function ProvideAuth({ children }) {
const [state, setState] = useState({
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
});
useEffect(() => {
const fetchData = async () => {
try {
const isAuthenticated = await CheckAuthentication();
if (isAuthenticated) {
const user = await GetUserInfo();
setState((prev) => ({ ...prev, isAuthenticated, user }));
}
} catch (error) {
setState((prev) => ({ ...prev, error }));
} finally {
setState((prev) => ({ ...prev, isLoading: false }));
}
};
fetchData();
}, []);
if (state.isLoading) return <Loading />;
if (state.error) return <ErrorMessage error={state.error} />;
return <authContext.Provider value={state}>{children}</authContext.Provider>;
}
I'm creating a simple sso login react project with python as my backend server and reactJS as my frontend. I'm using react-google-login as my SSO login button. I've already setup my backend server which receives information from the google api and verifies the Token ID from it. My react app's logged in state will depend on an api call to the backend which verifies the posted Token ID, this runs everytime the app is rendered.
My problem is that everytime I render my app when logged in state is true, my login page shows for a brief second then continues on the home page. I've realized that it is because i'm using useState for the variable that defines if the logged in state of my app is true or false.
Here's my App.js
function App() {
return (
<div id="routes">
<Routes>
<Route exact path="/login" element={<Login/>}/>
<Route element={<ProtectedRoutes />}>
<Route path="/" element={<SearchBar/>}/>
<Route path="/searched" element={<Table/>}/>
<Route path="/tablecomponent" element={<AntdTable/>}/>
</Route>
</Routes>
</div>
);
}
export default App;
ProtectedRoutes.js
const useAuth = () => {
const [users, setUsers] = useState(null)
useEffect(() => {
(async () => {
try {
const resp = await axios.get("/#me");
setUsers(resp.data);
} catch (error) {
console.log("Not authenticated");
}
})();
}, []);
const user = { loggedIn: users!=null };
return user && user.loggedIn;
};
const ProtectedRoutes = () => {
const isAuth = useAuth();
return isAuth ? <Outlet /> : <Login/>;
};
export default ProtectedRoutes;
Login.js
export default function Login() {
let navigate = useNavigate()
const [tokenID, setTokenID] = useState(null)
const clientId = "client_id"
const onLoginSuccess = (res) =>{
console.log("Login Success:", res);
setTimeout(()=>{
window.location.reload()
},1000)
try{fetch('/verifytoken',{
method: 'POST',
body: JSON.stringify({
token: res.tokenId,
email: res.profileObj.email,
}), headers:{"Content-type":"application/json; charset=UTF-8"}
}).then(response => response.json()).then(message=>console.log(message))
}
catch(error){
alert(error)
}
}
const onLoginFailure = (res)=>{
console.log("Login Failed:", res)
}
return (
<div>
Login Page <br/>
<Link to="/">
<button>button</button>
</Link>
<GoogleLogin
clientId={clientId}
buttonText="Login"
onSuccess={onLoginSuccess}
onFailure={onLoginFailure}
cookiePolicy={"single_host_origin"}
/>
</div>
)
}
I am currently trying to make a protected route function in react. I wish that result will return a boolean value rather than a promise without changing ProtectedRoute to async:
import React from "react";
import { Redirect, Route } from "react-router-dom";
import { CheckToken } from "./RequestAction"
function ProtectedRoute({ component: Component, ...restOfProps }) {
const result = (async () => {
const res = await CheckToken()
return res;
})()
console.log(result); //log-> PromiseĀ {<pending>}[[Prototype]]: Promise[[PromiseState]]: "fulfilled"[[PromiseResult]]: false
return (
<Route
{...restOfProps}
render={(props) =>
result ? <Component {...props} /> : <Redirect to="/login/" />
}
/>
);
}
export default ProtectedRoute;
This is the CheckToken function:
import axios from "axios";
async function CheckToken() {
let result = null;
await axios
.get(`http://localhost:5000/protected`,
{"withCredentials": true}
)
.then((res) => {
console.log("res.data.status:",res.data)
if (res.data.status === "success") {
result = true
}
})
.catch((err) => {
result = false
});
console.log("result:",result);
return result
}
const useToken = () => {
const [token, setToken] = useState(null);
useEffect(() => {
const f = async () => {
const res = await CheckToken();
//TODO: add check login;
setToken(res);
};
f();
})
return token;
}
function ProtectedRoute({ component: Component, ...restOfProps }) {
const token = useToken();
return (
<Route
{...restOfProps}
render={(props) =>
token ? <Component {...props} /> : <Redirect to="/login/" />
}
/>
);
}
Function component body cannot be async.
OK, so in your FunctionComponent you are trying to contact a server on a per render basis, and you are saying you want to capture the result in a boolean, that is then passed onto your child Route component
Firstly, network requests are considered effects, usually triggered by a change in your component props. you would want to handle the network behavior asynchronously as it would block the rendering of the component otherwise, which is something React doesn't allow. Also, you probably only want to check the token in specific circumstances, depending on your props.
You can still render your Route child component once your network request has resolved, but until then, there will be an intermediary state that your component will need to handle. How it does that is up to you, but you should probably not render a link until you have that result information.
I'd introduce a bit of state in a custom hook, and just have your component return out some intermediary state until you get a result / error.
I would also just return a token, you can use null to indicate the network request has not completed, and empty string if one hasn't been retrieved.
const useToken = () => {
const [state, setState] = useState(null)
useEffect(() => {
//TODO: you would also want to catch errors
CheckToken().then(setState)
})
return state
}
function ProtectedRoute({ component: Component, ...restOfProps }) {
const token = useToken()
if (token === null) {
return <Spinner/> // something that indicates you are waiting
}
return (
<Route
{...restOfProps}
render={(props) =>
token ? <Component {...props} /> : <Redirect to="/login/" />
}
/>)
}
}
I have a react component I need to render that takes one argument of a string when it is initialized. I need a button to click on that will redirect and render this new component with the string. It sends the string I want when it console.log(pro). everytinme I click on the button it goes to a blank screen and doesn't load.
My routes.js looks like
const Routes = (props) => {
return (
<Switch>
<Route exact path="/member" component={() => <Member user={props.state.member} />} />
<Route path="/posts" exact component={Posts} />
<Redirect exact to="/" />
</Switch>
)
}
export default Routes
The original component looks like this
const Posts = (props) => {
const dispatch = useDispatch();
const [postCount, setPostCount] = useState(0);
const [member, setMember] = useState({});
const getProfile = async (member) => {
const addr = dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
props.history.push('/member'
);
console.log('----------- member------------') // console.log(addr)
return (
<Member user={member}><Member/>
);
}
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default withRouter(Posts);
The component I'm trying to render from the Posts component needs to be rendered with a string
const Member = (props)=> {
const [user, setUser] = useState({});
const { state } = props.location;
const [profile, setProfile] = useState({});
useEffect(() => {
const doEffects = async () => {
try {
const pro = socialNetworkContract.members[0]
console.log(pro)
const p = await incidentsInstance.usersProfile(pro, { from: accounts[0] });
setProfile(p)
} catch (e) {
console.error(e)
}
}
doEffects();
}, [profile]);
return (
<div class="container">
{profile.name}
</div>
)
}
export default Member;
You can pass an extra data to a route using state attribute with history.push
const getProfile = async (member) => {
const addr = dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
props.history.push({
path: '/member',
state: { member }
});
}
Once you do that you can access it in the rendered route from location.state
import {
useLocation
} from "react-router-dom";
const Member = (props)=> {
const [user, setUser] = useState({});
const { state } = useLocation();
console.log(state.member);
const [profile, setProfile] = useState({});
...
}
export default Member;
Also you do not need to pass on anything while rendering the Route
const Routes = (props) => {
return (
<Switch>
<Route exact path="/member" component={Member} />
<Route path="/posts" exact component={Posts} />
<Redirect exact to="/" />
</Switch>
)
}
export default Routes
I have a bit of a problem implementing authentication for my React application. I followed this link to get the authentication going. Here's my App component:
function App() {
return (
<ProvideAuth>
<BrowserRouter>
<Header />
<Switch>
<PrivateRoute exact path="/">
<Dashboard />
</PrivateRoute>
<Route path="/login">
<Login />
</Route>
</Switch>
</BrowserRouter>
</ProvideAuth>
);
}
function PrivateRoute({ children, ...rest }) {
let auth = useAuth();
console.log("USER: ", auth.user);
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)} />
)
}
export default App;
Login component:
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
let history = useHistory();
let location = useLocation();
let auth = useAuth();
let { from } = location.state || { from: { pathname: "/" } }
let login = (e) => {
auth.signin(email, password, () => {
history.replace(from);
});
};
return (
<div>
<input onChange={e => setEmail(e.target.value)} value={email} type="email" />
<input onChange={e => setPassword(e.target.value)} value={password} type="password" />
</div>
)
}
export default Login;
Finally use-auth.js:
const authContext = createContext();
export function ProvideAuth({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
};
export const useAuth = () => {
return useContext(authContext);
};
function useProvideAuth() {
const [user, setUser] = useState(null);
const signin = (email, password, callback) => {
axios.post(`${apiUrl}/sign_in`, {
'email': email,
'password': password
},
{
headers: {
'Content-Type': 'application/json'
}
}).then(res => {
const expiryDate = new Date(new Date().getTime() + 6 * 60 * 60 * 1000).toUTCString();
document.cookie = `access-token=${res.headers['access-token']}; path=/; expires=${expiryDate}; secure; samesite=lax`;
return res.data
})
.then(data => {
setUser(data.data);
callback();
})
.catch(e => {
setUser(null);
});
};
const signout = () => {
document.cookie = "access-token=; expires = Thu, 01 Jan 1970 00:00:00 GMT";
setUser(null);
}
useEffect(() => {
const cookies = getCookies();
if (cookies['access-token']) {
axios.get(`${apiUrl}/user_info`, {
headers: {
...cookies
}
}).then(res => {
return res.data;
})
.then(data => {
setUser(data);
})
.catch(e => {
setUser(null);
})
} else {
setUser(null);
}
}, []);
return {
user,
signin,
signout
}
}
function getCookies() {
let cookies = document.cookie.split(';');
let authTokens = {
'access-token': null
};
for (const cookie of cookies) {
let cookiePair = cookie.split('=');
if (authTokens.hasOwnProperty(cookiePair[0].trim().toLowerCase()))
authTokens[cookiePair[0].trim()] = decodeURIComponent(cookiePair[1]);
}
return authTokens;
}
and then the dashboard component is the homepage. Nothing interesting.
The problem is when a user is in fact logged in (the access-token cookie is set as well as other tokens), they're still routed to the login page because of the fact that calling the API which checks that these tokens are valid is asynchronous, so the user is set to null initially.
What am I missing here? how can I wait until the API response is returned without blocking the user interface? Should I save user state in the redux state or is there some other work around?
Thanks a lot!
Like Jonas Wilms suggested, I added a loading state variable in user-auth similar to user and set it to true before each request and false after the request is completed.
In my App component, I changed the PrivateRoute function to show a loading spinner as long as the user state is loading. When it's set to false, I check whether the user is logged in or not and show the Dashboard component or redirect to login page accordingly.
function PrivateRoute({ children, ...rest }) {
let auth = useAuth();
return (
<Route
{...rest}
render={({ location }) =>
auth.loading ?
<Loading /> :
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)} />
)
}