TL;DR: I found a solution to my problem described below, but I am not sure if it's a good solution (good in the sense that it does not cause any other problems, of which I might not be aware yet)
In my Next.js project, all pages have a Layout component, which is wrapped around any child-components. It has a useEffect-hook, which checks if a user isAuthenticated and hence can access pages where the pathIsProtected or not. This is done via the check_auth_status action creator, which decides about the state of isAuthenticated.
This is my Layout:
// components/ui/Layout.js
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useDispatch, useSelector } from "react-redux";
import { check_auth_status } from "../../store/authActions";
import FullPageLoader from "../ui/FullPageLoader";
const Layout = ({ protectedRoutes, children }) => {
const router = useRouter();
const dispatch = useDispatch();
const loading = useSelector((state) => state.auth.loading);
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
// check if current route is in the pathIsProtected array, if yes return true, if no return false
const pathIsProtected = protectedRoutes.indexOf(router.pathname) !== -1;
useEffect(() => {
if (dispatch && (dispatch !== null) & (dispatch !== undefined))
dispatch(check_auth_status());
if (!loading && !isAuthenticated && pathIsProtected) {
router.push("/login");
}
}, [dispatch, loading, isAuthenticated, pathIsProtected, router]);
if ((loading || !isAuthenticated) && pathIsProtected) {
return <FullPageLoader />;
}
return <div className="min-h-full">{children}</div>;
};
export default Layout;
This is my check_auth_status:
// check_auth_status action creator function
export const check_auth_status = () => async (dispatch) => {
try {
const res = await fetch("/api/account/verify", {
method: "GET",
headers: {
Accept: "application/json",
},
});
if (res.status === 200) {
dispatch(authActions.authenticatedSuccess());
} else {
dispatch(authActions.authenticatedFail());
}
} catch (err) {
dispatch(authActions.authenticatedFail());
}
};
My problem with this is the following: When a user isAuthenticated and refreshes a page, where the pathisProtected, the following happens:
1.) As expected - the page gets rendered with the FullPageLoader because the state of isAuthenticated is initially false.
2.) As expected - the page gets re-rendered for a second with its proper child components, because check_auth_status sets isAuthenticated to true
3.) Not as expected - I get redirected to the login-page, but since the user isAutheticated, after a second I get redirected back again to my original page, rendered with any of its proper child components.
My Question: Why is this happening? My own explanation is that there is a race condition between check_auth_status updating isAuthenticated and the redirection to the login-page if a user is !isAuthenticated right after it.
My solution (which works, but I am not sure if not unproblematic) is to wrap everything inside an async function, await the promise from check_auth_status in quickAuthCheck and check the value quickAuthCheck instead of the state of isAuthenticated
This is my updated Layout:
// components/ui/Layout.js
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useDispatch, useSelector } from "react-redux";
import { check_auth_status } from "../../store/authActions";
import FullPageLoader from "../ui/FullPageLoader";
const Layout = ({ protectedRoutes, children }) => {
const router = useRouter();
const dispatch = useDispatch();
const loading = useSelector((state) => state.auth.loading);
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
const pathIsProtected = protectedRoutes.indexOf(router.pathname) !== -1;
useEffect(() => {
const checkAuth = async () => {
if (dispatch && (dispatch !== null) & (dispatch !== undefined)) {
const quickAuthCheck = await dispatch(check_auth_status());
if (!loading && !quickAuthCheck && pathIsProtected)
router.push("/login");
}
};
checkAuth();
}, [dispatch, loading, isAuthenticated, pathIsProtected, router]);
if ((loading || !isAuthenticated) && pathIsProtected) {
return <FullPageLoader />;
}
return <div className="min-h-full">{children}</div>;
};
export default Layout;
This is my updated check_auth_status:
export const check_auth_status = () => async (dispatch) => {
try {
const res = await fetch("/api/account/verify", {
method: "GET",
headers: {
Accept: "application/json",
},
});
if (res.status === 200) {
dispatch(authActions.authenticatedSuccess());
return(true)
} else {
dispatch(authActions.authenticatedFail());
return(false)
}
} catch (err) {
dispatch(authActions.authenticatedFail())
return(false);
}
};
Related
import { useState, useEffect } from "react";
import LoginModal from "./LoginModal";
import { NavLink, useLocation, useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { userLogout } from "../Features/User/userSlice";
import decode from "jwt-decode";
const Header = ({ toggleModalShow, showModal }) => {
const [burgerAnimation, setBurgerAnimation] = useState(false);
const [user, setUser] = useState();
const location = useLocation();
const dispatch = useDispatch();
const navigate = useNavigate();
// for showing login/sign up modal
const showModalButton = () => {
toggleModalShow();
};
const handleBurgerAnimation = () => {
setBurgerAnimation(!burgerAnimation);
};
const handleLogout = async (id) => {
await dispatch(userLogout({ id, navigate, dispatch }));
setUser(null);
};
const burgerListItemAnimation = ...
const burgerIconAnimation = ...
const guestHeader = (
<ul>
...
</ul>
);
const userHeader = (
<ul>
...
</ul>
);
useEffect(() => {
if (localStorage.getItem("user") && !user) {
setUser(JSON.parse(localStorage.getItem("user")));
}
const accessToken = user?.accessToken;
if (accessToken) {
const decodedAccessToken = decode(accessToken);
if(decodedAccessToken.exp * 1000 < new Date().getTime()){
handleLogout(user.user._id);
}
console.log(decodedAccessToken);
}
}, [location, user]);
return (
<header className="header">
...
</header>
);
};
export default Header;
Hi all.I just wanted to try to log out the user when the expiration date is over. If i put 'handleLogout' to useEffect dependicies warning doesnt change. Why am i getting this warning ? What kind of warning may i get if i dont fix that ? And finally, if you have time to review the repo, would you give feedback ?
repo : https://github.com/UmutPalabiyik/mook
If you keep handleLogout external to the useEffect hook it should be listed as a dependency as it is referenced within the hook's callback.
If i put handleLogout to useEffect dependencies warning doesn't
change.
I doubt the warning is the same. At this point I would expect to you to see the warning change to something like "the dependency handleLogout is redeclared each render cycle, either move it into the useEffect hook or memoize with useCallback..." something to that effect.
From here you've the 2 options.
Move handleLogout into the useEffect so it is no longer an external dependency.
useEffect(() => {
const handleLogout = async (id) => {
await dispatch(userLogout({ id, navigate, dispatch }));
setUser(null);
};
if (localStorage.getItem("user") && !user) {
setUser(JSON.parse(localStorage.getItem("user")));
}
const accessToken = user?.accessToken;
if (accessToken) {
const decodedAccessToken = decode(accessToken);
if (decodedAccessToken.exp * 1000 < new Date().getTime()) {
handleLogout(user.user._id);
}
console.log(decodedAccessToken);
}
}, [location, user, id, navigate, dispatch]);
Memoize handleLogout with useCallback so it's a stable reference and add it to the effect's dependencies.
const handleLogout = useCallback(async (id) => {
await dispatch(userLogout({ id, navigate, dispatch }));
setUser(null);
}, [id, navigate, dispatch]);
...
useEffect(() => {
if (localStorage.getItem("user") && !user) {
setUser(JSON.parse(localStorage.getItem("user")));
}
const accessToken = user?.accessToken;
if (accessToken) {
const decodedAccessToken = decode(accessToken);
if (decodedAccessToken.exp * 1000 < new Date().getTime()) {
handleLogout(user.user._id);
}
console.log(decodedAccessToken);
}
}, [location, user, handleLogout]);
I am trying to get the name of a user based on the user id, using a .map() method. The code that I wrote seems good to me, however, I get the error mentioned in the question title and the page won't render.
See the image below:
Here's an image of the error stack:
And here's the code I have:
import { SearchBox } from "../SearchBox"
import { StyledNavBar, NavSection, StyledLink, StyledLogo } from "./styles"
import logo from './Logo2.png'
import { useDispatch } from "react-redux"
import { useHistory } from "react-router"
import axios from "axios"
import { useEffect, useState } from "react"
function useApi() {
const userId = localStorage.getItem('userId')
const dispatch = useDispatch()
const [UserNames, setUserNames] = useState()
useEffect(() => {
async function getData() {
try {
const { data } = await axios({
method: 'GET',
baseURL: process.env.REACT_APP_SERVER_URL,
url: '/users'
})
console.log(data)
dispatch(setUserNames(data))
}catch(error){
dispatch(console.log(error))
}
}
getData()
}, [])
return { userId, UserNames }
}
export const NavBar = function({}) {
const { userId, UserNames } = useApi()
console.log(UserNames)
const token = localStorage.getItem('token')
const dispatch = useDispatch()
const history = useHistory()
function handleClick(){
dispatch({type: 'USER_LOGOUT'})
localStorage.clear()
history.push('/')
}
return(
<StyledNavBar>
<NavSection>
{!token && <StyledLink to='/login'>Ingresar</StyledLink>}
{!token && <StyledLink to='/signup'>Registrarse</StyledLink>}
<StyledLink to='/'>CategorÃas</StyledLink>
<StyledLink to='/'><StyledLogo src={logo} /></StyledLink>
{token && <StyledLink to='/'>Mis Transacciones</StyledLink>}
{token && <StyledLink to='/sellproduct'>Vender</StyledLink>}
{!!UserNames && UserNames.length > 0 && UserNames.map((usr, i) => {
return usr[i]._id === userId ?
<p>{usr[i].name}</p>
:
''
})}
<SearchBox />
{token && <button type='button' onClick={handleClick}>Cerrar Sesión</button>}
</NavSection>
</StyledNavBar>
)
}
So it's pretty much telling me that the usr[i]._id line is not correct, but as far as I can see nothing is wrong with that code.
I think you might just want usr and not usr[i]? The map() gives you the individual item from the iterable.
I am trying to solve how to rerender a different functional component.
onComplete: function (data) {
console.log("everything is complete");
console.log("this is the applicant id", id);
let obj;
axios
.post("http://localhost:5000/post_id", {
applicant_id: id,
})
.then((response) => {
obj = response.data.data.data.json_data.result;
});
setTimeout(() => {
if (obj === "clear") {
onfidoOut.tearDown(); // <- the Onfido view did not unmount
renderResult(); // <- BAD APPROACH? `renderResult` renders a HTML node instead of triggers a view rerender.
}
}, 3000);
},
I embarked upon an approach of rendering an HTML node instead of triggering a view rerender because I have never implemented a view rerender to a different component outside of an authentication flow in a class-based component. Also this is a microfrontend application. This is the whole file from where the snippet above is obtained:
const useOnfidoFetch = (URL) => {
const [token, setToken] = useState();
const [id, setId] = useState();
useEffect(() => {
axios
.get("http://localhost:5000/post_stuff")
.then((response) => response.data.data.data.json_data)
.then((json_data) => {
console.log("this is the json data", json_data);
const id = json_data.applicant_id;
const token = json_data.onfido_sdk_token;
setId(id);
setToken(token);
});
}, [URL]);
useEffect(() => {
if (!token) return;
console.log("this is working!");
OnfidoSDK.init({
token,
containerId: "root",
steps: [
{
type: "welcome",
options: {
title: "Open your new bank account",
},
},
"document",
],
onComplete: function (data) {
console.log("everything is complete");
console.log("this is the applicant id", id);
let obj;
axios
.post("http://localhost:5000/post_id", {
applicant_id: id,
})
.then((response) => {
obj = response.data.data.data.json_data.result;
});
setTimeout(() => {
if (obj === "clear") {
onfidoOut.tearDown();
renderResult();
}
}, 3000);
},
});
}, [id, token]);
function renderResult() {
return <Redirect to="/result" />;
}
};
export default function() {
const URL = `${transmitAPI}/anonymous_invoke?aid=onfido_webapp`;
const result = useOnfidoFetch(URL, {});
return (
<div id={onfidoContainerId} />
)
}
And this is the bootstrap.js file:
import React from 'react';
import ReactDOM from 'react-dom';
import {createMemoryHistory, createBrowserHistory} from 'history';
import App from './App';
// Mount function to start up the app
const mount = (el, { onNavigate, defaultHistory, initialPath}) => {
const history = defaultHistory || createMemoryHistory({
initialEntries: [initialPath],
});
if (onNavigate) {
history.listen(onNavigate);
}
ReactDOM.render(<App history={history} />, el);
return {
onParentNavigate({ pathname: nextPathname }) {
const {pathname} = history.location;
if (pathname !== nextPathname) {
history.push(nextPathname);
}
},
};
};
// If I am in development and isolation
// call mount immediately
if (process.env.NODE_ENV === 'development') {
const devRoot = document.querySelector('#_marketing-dev-root');
if (devRoot) {
mount(devRoot, {defaultHistory: createBrowserHIstory()});
}
}
// Assuming we are running through container
// and we should export the mount function
export {mount};
It seems obj (from the response) could be used to manage state, for example:
const [receivedResults, setReceivedResults] = useState(false);
Then when the response is received:
setReceivedResults(obj === "clear");
and use the flag to allow the component to render itself (not sure of the exact syntax):
if (receivedResults) {
return <Redirect to="/result" />;
} else {
return null;
}
I used to use class components in react-native. My new app.js file looks like this :
import React, { useState, useEffect } from 'react';
import {Text} from 'react-native';
import AppContext from './AppContext';
import {NavigationContainer} from '#react-navigation/native';
import Splash from './src/components/Splash';
import {MainStackNavigator, BottomTabNavigator} from './Navigation/StackNavigator';
export default function App() {
Text.defaultProps = Text.defaultProps || {};
Text.defaultProps.allowFontScaling = false;
const [user, setUser] = useState({ loggedIn: false });
const state = { user, setUser };
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => setLoading(false), 2000);
}, []);
if (loading) {
return <Splash />;
}
return (
<AppContext.Provider value={state}>
<NavigationContainer>
{!user.loggedIn ? (
<MainStackNavigator />
) : (
<BottomTabNavigator />
)}
</NavigationContainer>
</AppContext.Provider>
);
}
How can I add what I used before, componentDidMount or componentWillMount?
Because here I would like to add this, and i'm a little lost:
async UNSAFE_componentWillMount() {
let lang = await retrieveAppLang();
let isConnected = await userSessionActive();
if (lang.length == 2) {
i18n.changeLanguage(lang);
}
if (isConnected === true && this.props && this.props.navigation) {
this.props.navigation.navigate("TabBar");
}
}
You should use React.useEffect hook
like this:
async someFunction() {
let lang = await retrieveAppLang();
let isConnected = await userSessionActive();
if (lang.length == 2) {
i18n.changeLanguage(lang);
}
if (isConnected === true && this.props && this.props.navigation) {
this.props.navigation.navigate("TabBar");
}
}
//this will call at each render
React.useEffect(() => {
someFunction()
});
Be warned: not to add async keyword inside the useEffect itself or it will cause a crash to your app
As the componentWillMount has been deprecated from the React latest version so, it might not work.
We can move the componentWillMount code to componentDidMount to execute the code without any React warning.
async componentDidMount() {
let lang = await retrieveAppLang();
let isConnected = await userSessionActive();
if (lang.length == 2) {
i18n.changeLanguage(lang);
}
if (isConnected === true && this.props && this.props.navigation) {
this.props.navigation.navigate("TabBar");
}
}
the declaration of your languageSetup is wrong. You need to do like this, if you want a arrow function:
const languageSetup = async () => {
//Your function code
}
or
async function languageSetup(){
//Your function code
}
```.
I am currently working on a chat application and for some reason every time I pass in my array of messages as a prop to another component it passes in a number to the component instead of the message object. I have tried a lot of different methods of passing it in regarding using multiple components etc but it seems to still be passing in the number of elements for some reason. Any help is appreciated... code is below
Component receiving the props
import React, { useEffect } from 'react'
import Message from '../../Message/Message'
function Messages({ messages }) {
useEffect(() => {
console.log(messages)
}, [messages])
return (
<div>
test
</div>
)
}
export default Messages
// Import React dependencies.
import React, { useEffect, useState, } from "react";
// Import React dependencies.
import io from 'socket.io-client'
import axios from 'axios'
import Messages from './Messages/Messages'
import uuid from 'react-uuid'
import { Redirect } from 'react-router-dom'
// Import the Slate components and React plugin.
const ENDPOINT = 'http://localhost:5000/'
export const socket = io.connect(ENDPOINT)
const LiveChatFunction = ({ group_id }) => {
// Add the initial value when setting up our state.
const [message, setValue] = useState("")
const [user, setUser] = useState("")
const [groupId, setGroup] = useState('')
const [messages, setMessages] = useState([])
const [toLogin, userAuth] = useState(false)
useEffect(() => {
setGroup(group_id)
axios.post('http://localhost:5000/api/users/refresh_token', null, { withCredentials: true }).then(data => {
if (!data.data.accessToken) {
userAuth(true)
}
})
axios.get('http://localhost:5000/api/users/userInfo', { withCredentials: true }).then(data => {
setUser(data.data.user)
})
socket.on(`message-${group_id}`, data => {
setMessages(messages.push(data))
});
axios.get(`http://localhost:5000/live/${group_id}`).then(x => {
console.log(x.data)
})
}, [group_id, messages])
function setClick() {
const data = {
messageId: uuid(),
user,
groupId,
message
}
socket.emit('message', data)
}
if (toLogin) {
return (
<Redirect to="/login" />
)
}
return (
<div>
<input placeholder="message" type="text" onChange={value => {
setValue(value.target.value)
socket.emit('typing-message', { username: user, time: new Date() })
}} />
<button onClick={setClick}>Submit</button>
<Messages messages={messages} />
</div>
)
}
export default LiveChatFunction;
I have added some comments of what I think you can change:
useEffect(() => {
const recieveFunction = (data) => {
//using callback so no dependency on messages
setMessages((messages) => messages.push(data));
};
async function init() {
//next line is pointless, this runs when group_id
// has changed so something must have set it
// setGroup(group_id);
await axios //not sure if this should be done before listening to socket
.post(
'http://localhost:5000/api/users/refresh_token',
null,
{ withCredentials: true }
)
.then((data) => {
if (!data.data.accessToken) {
userAuth(true);
}
});
await axios
.get('http://localhost:5000/api/users/userInfo', {
withCredentials: true,
})
.then((data) => {
setUser(data.data.user);
});
//start listening to socket after user info is set
socket.on(`message-${group_id}`, recieveFunction);
axios
.get(`http://localhost:5000/live/${group_id}`)
.then((x) => {
console.log(x.data);
});
}
init();
//returning cleanup function, guessing socket.off exists
return () =>
socket.off(`message-${group_id}`, recieveFunction);
}, [group_id]); //no messages dependencies
console.log('messages are now:',messages);
If messages is still not set correctly then can you log it
So I think I found your problem:
In your useEffect hook, you're setting messages to the wrong thing.
socket.on(`message-${group_id}`, data => {
setMessages(messages.push(data))
});
An example:
const m = [].push();
console.log(m);
// m === 0
const n = [].push({});
console.log(n);
// n === 1
As you can see this is the index.
So what you need is:
socket.on(`message-${group_id}`, data => {
messages.push(data);
setMessages(messages);
});
This will set messages to the array of messages.