I am trying to redirect user in case user is not authenticated and vice versa
so, I have the directory structure as follow
myproject
src
App.js
UserContext.js
routes
index.js
route.js
pages
Dashboard
index.js
authentication
login.js
In my app.js i do a call and get my authentication token
and set auth to true and pass it in user context but it has the default values and i cannot redirect currently redirecting with only window.location.href
my code for usercontext.js
import { createContext } from "react";
export const UserContext = createContext(null)
APP.js
const App = props => {
const [user,setUser] = React.useState(null)
var [auth,setAuth] = React.useState(false)
const isAuthenticated = ()=>
{
var isAdmin = true;
axios.get(`/verifyToken`).then((response)=>{
console.log(response.data.auth)
setUser({...response.data.user})
setAuth(response.data.auth)
console.log(response.data.user)
})
}
useEffect(() => {
isAuthenticated()
console.log(auth)
},[]);
function getLayout() {
let layoutCls = VerticalLayout
switch (props.layout.layoutType) {
case "horizontal":
layoutCls = HorizontalLayout
break
default:
layoutCls = VerticalLayout
break
}
return layoutCls
}
const Layout = getLayout()
return (
<React.Fragment>
<Router>
<Switch>
<UserContext.Provider value={{user,setUser,auth,setAuth,isAuthenticated}}>
{publicRoutes.map((route, idx) => (
<Authmiddleware
path={route.path}
layout={NonAuthLayout}
component={route.component}
key={idx}
isAuthProtected={auth}
exact
/>
))}
{authProtectedRoutes.map((route, idx) => (
<Authmiddleware
path={route.path}
layout={Layout}
component={route.component}
key={idx}
isAuthProtected={auth}
exact
/>
))}
</UserContext.Provider>
</Switch>
</Router>
</React.Fragment>
)
}
My index.js file has component and routes names array which i am looping above
and this is my route.js
const Authmiddleware = ({
component: Component,
layout: Layout,
isAuthProtected,
...rest
}) => (
<Route
{...rest}
render={props => {
return (
<Layout>
<Component {...props} />
</Layout>
)
}}
/>
)
Authmiddleware.propTypes = {
isAuthProtected: PropTypes.bool,
component: PropTypes.any,
location: PropTypes.object,
layout: PropTypes.any,
}
export default Authmiddleware;
So, now If in my dashboard.js I try to access user on wan tto redirect if auth is false it only has default values of user and auth
I am fetching as follows in dashboard.js
import {UserContext} from '../../UserContext'
const {user,setUser,auth,setAuth,isAuthenticated} = React.useContext(UserContext)
React.useEffect(()=>{
if(auth == false){
window.location.href='/login'
//IT TAKES ME LOGIN EVERYTIME AT IT IS ONLY GETTING DEFAULT VALUE THAT IS FALSE
},[])
WHAT I HAVE TRIED
If i place the isAuthenticated() function call in every component it works
but that would be like so many lines of code same in every component
What is the way to go with?
Anyone facing the same issue I resolved it by
bringing out
<UserContext.Provider></UserContext.Provider>
outside the switch
<UserContext.Provider value={{user,setUser,auth,setAuth,isAuthenticated}}>
<Switch>
</Switch>
</UserContext.Provider value={{user,setUser,auth,setAuth,isAuthenticated}}>
I FOUND THE REASON HERE: https://coderedirect.com/questions/324089/how-to-use-context-api-with-react-router-v4
The reason posted in answer here was that Switch expects routes directly.
Related
I am working on a react application.
I am trying to create login and register functionality.
I have a Authorized.tsx component which looks like this
export const Authorized = (props: authorizedProps) => {
const [isAuthorized, setIsAuthorized] = useState(true);
const { claims } = useContext(AuthContext);
useEffect(() => {
if (props.role) {
const index = claims.findIndex(
claim => claim.name === 'role' && claim.value === props.role)
setIsAuthorized(index > -1);
} else {
setIsAuthorized(claims.length > 0);
}
}, [claims, props.role]);
return (
<>
{isAuthorized ? props.authorized : props.notAuthorized}
</>
);
};
interface authorizedProps {
authorized: ReactElement;
notAuthorized?: ReactElement;
role?: string;
}
This component hides and shows diffrent kind of components depending on if the user is authorized or not.
I am using this component to only show the Login.tsx component for users that are not logged in. I dont want anyone who is not logged in to be able to visit the website.
In my Index.tsx I am using the Authorized.tsx component like this
const Index = () => {
const [claims, setClaims] = useState<claim[]>([
// { name: "email", value: "test#hotmail.com" },
]);
return (
<div>
<BrowserRouter>
<AuthContext.Provider value={{ claims, update: setClaims }}>
<Authorized authorized={<App />} notAuthorized={<Login />} />
</AuthContext.Provider>
</BrowserRouter>
</div>
);
};
All the authorized users will be able to visit the site, everyone else will be asked to log in.
However, the problem I have is when I tried adding the Register.tsx component into the Login.tsx component as a navigational link.
I wish to be able to navigate between Register and Login
This is how the Login.tsx component looks like
export const Login = () => {
return (
<>
<h3>Log in</h3>
<DisplayErrors errors={errors} />
<AuthForm
model={{ email: "", password: "" }}
onSubmit={async (values) => await login(values)}
BtnText="Log in" />
<Switch>
<Route path="/register">
<Register />
</Route>
<Link to='/register'>Register</Link>
</Switch>
</>
);
};
But what actually happends when I press the 'Register' link is that the Register component gets added below the Login component
Before pressing the 'Register' link
After pressing the 'Register' link
I understand it has something to do with the Authorized.tsx component in Index.tsx.
That I am telling it to only show the Login component when not authorized.
But I dont know how I could fix it so I will be able to navigate between the Login and the Register
All help I could get would be much appreciated!
Thanks
With the current implementation you are rendering a Login component that then also renders a route for a Register component to be rendered on. Login remains mounted and rendered the entire time. From what you describe you want to render Login and Register each on their own route.
Abstract both these components into a parent component that manages the route matching and rendering.
Example
const Unauthenticated = () => (
<Switch>
<Route path="/register" component={Register} />
<Route component={Login} />
</Switch>
);
...
export const Login = () => {
...
return (
<>
<h3>Log in</h3>
<DisplayErrors errors={errors} />
<AuthForm
model={{ email: "", password: "" }}
onSubmit={login}
BtnText="Log in"
/>
<Link to='/register'>Register</Link>
</>
);
};
...
const Index = () => {
const [claims, setClaims] = useState<claim[]>([
// { name: "email", value: "test#hotmail.com" },
]);
return (
<div>
<BrowserRouter>
<AuthContext.Provider value={{ claims, update: setClaims }}>
<Authorized
authorized={<App />}
notAuthorized={<Unauthenticated />}
/>
</AuthContext.Provider>
</BrowserRouter>
</div>
);
};
So I have a straight forward app that requires you to login to see a dashboard. I've based my auth flow off of https://reactrouter.com/web/example/auth-workflow which in return bases their flow off of https://usehooks.com/useAuth/
Currently, when a user logs in it calls a function within the context provider to sign in and that function updates the state of the context with the user data retrieved from the server. This is reflected in React dev tools under my context providers as shown in the teacher attribute:
When the context state has successfully been updated I then use useHistory().push("dashboard/main") from the react-router API to go to the dashboard page. The dashboard is a consumer of the context provider but the teacher value is still null when I try rendering the page- even though React dev tools clearly shows the value has been updated. When I log in again, the dashboard will successfully render, so, ultimately, it takes two context updates in order for my Dashboard to reflect the changes and render. See my following code snippets (irrelevant code has been redacted):
App.js
const App = () => {
return (
<AuthProvider>
<div className="App">
<Switch>
<Route path="/" exact >
<Home setIsFetching={setIsFetching} />
</Route>
<ProtectedRoute path="/dashboard/:page" >
<Dashboard
handleToaster={handleToaster}
/>
</ProtectedRoute>
<ProtectedRoute path="/dashboard">
<Redirect to="/dashboard/main"/>
</ProtectedRoute>
<Route path="*">
<PageNotFound/>
</Route>
</Switch>
<Toaster display={toaster.display} setDisplay={(displayed) => setToaster({...toaster, display: displayed})}>{toaster.body}</Toaster>
</div>
</AuthProvider>
);}
AuthProvider.js
const AuthProvider = ({children}) => {
const auth = useProvideAuth();
return(
<TeacherContext.Provider value={auth}>
{children}
</TeacherContext.Provider>
);};
AuthHooks.js
export const TeacherContext = createContext();
export const useProvideAuth = () => {
const [teacher, setTeacher] = useState(null);
const memoizedTeacher = useMemo(() => ({teacher}), [teacher]);
const signin = (data) => {
fetch(`/api/authenticate`, {method: "POST", body: JSON.stringify(data), headers: JSON_HEADER})
.then(response => Promise.all([response.ok, response.json()]))
.then(([ok, body]) => {
if(ok){
setTeacher(body);
}else{
return {...body};
}
})
.catch(() => alert(SERVER_ERROR));
};
const register = (data) => {
fetch(`/api/createuser`, {method: "POST", body: JSON.stringify(data), headers: JSON_HEADER})
.then(response => Promise.all([response.ok, response.json()]))
.then(([ok, body]) => {
if(ok){
setTeacher(body);
}else{
return {...body};
}
})
.catch(() => alert(SERVER_ERROR));
};
const refreshTeacher = async () => {
let resp = await fetch("/api/teacher");
if (!resp.ok)
throw new Error(SERVER_ERROR);
else
await resp.json().then(data => {
setTeacher(data);
});
};
const signout = () => {
STORAGE.clear();
setTeacher(null);
};
return {
...memoizedTeacher,
setTeacher,
signin,
signout,
refreshTeacher,
register
};
};
export const useAuth = () => {
return useContext(TeacherContext);
};
ProtectedRoute.js
const ProtectedRoute = ({children, path}) => {
let auth = useAuth();
return (
<Route path={path}>
{
auth.teacher
? children
: <Redirect to="/"/>
}
</Route>
);
};
Home.js
const Home = ({setIsFetching}) => {
let teacherObject = useAuth();
let history = useHistory();
const handleFormSubmission = (e) => {
e.preventDefault();
const isLoginForm = modalContent === "login";
const data = isLoginForm ? loginObject : registrationObject;
const potentialSignInErrors = isLoginForm ?
teacherObject.signin(data) : teacherObject.register(data);
if(potentialSignInErrors)
setErrors(potentialSignInErrors);
else{
*******MY ATTEMPT TO PUSH TO THE DASHBOARD AFTER USING TEACHEROBJECT.SIGNIN********
history.replace("/dashboard/main");
}
};
};)};
Dashboard.js
const Dashboard = ({handleToaster}) => {
const [expanded, setExpanded] = useState(true);
return (
<div className={"dashboardwrapper"}>
<Sidebar
expanded={expanded}
setExpanded={setExpanded}
/>
<div className={"dash-main-wrapper"}>
<DashNav/>
<Switch>
<Route path="/dashboard/classroom" exact>
<Classroom handleToaster={handleToaster} />
</Route>
<Route path="/dashboard/progressreport" exact>
<ProgressReport/>
</Route>
<Route path="/dashboard/help" exact>
<Help/>
</Route>
<Route path="/dashboard/goalcenter" exact>
<GoalCenter />
</Route>
<Route path="/dashboard/goalcenter/create" exact>
<CreateGoal />
</Route>
<Route path="/dashboard/profile" exact>
<Profile />
</Route>
<Route path="/dashboard/test" exact>
<Test />
</Route>
<Route path="/dashboard/main" exact>
<DashMain/>
</Route>
</Switch>
</div>
</div>
);
};
Let me know if there's anything that stands out to you that would be preventing my Dashboard from rendering with the updated context values the first time instead of having to update it twice. Do let me know if you need more insight into my code or if I missed something- I'm also fairly new to SO. Also, any pointers on the structure of my app would be greatly appreciated as this is my first React project. Thank you.
I think the problem is in the handleFormSubmission function:
const handleFormSubmission = (e) => {
e.preventDefault();
const isLoginForm = modalContent === "login";
const data = isLoginForm ? loginObject : registrationObject;
const potentialSignInErrors = isLoginForm ?
teacherObject.signin(data) : teacherObject.register(data);
if(potentialSignInErrors)
setErrors(potentialSignInErrors);
else{
history.replace("/dashboard/main");
}
};
You call teacherObject.signin(data) or teacherObject.register(data) and then you sequentially change the history state.
The problem is that you can't be sure the teacher state has been updated, before history.replace is called.
I've made a simplified version of your home component to give an example how you could approach the problem
function handleSignin(auth) {
auth.signin("data...");
}
const Home = () => {
const auth = useAuth();
useEffect(() => {
if (auth.teacher !== null) {
// state has updated and teacher is defined, do stuff
}
}, [auth]);
return <button onClick={() => handleSignin(auth)}>Sign In</button>;
};
So when auth changes, check if teacher has a value and do something with it.
I created a store with redux to experiment with the app state management in react. So far I'm just trying to make a fake authentication behavior when clicking on the "sign in" button on the login page, which is working because my isLogged state change to true. But then when I try to access a path that I protected by checking if isLogged is true, I get false... why is the state of isLogged not saved when routing with react-router_dom?
index.js
const store = createStore(
allReducers,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App/>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
App.js
class App extends Component {
render() {
return (
<Box style={{width: "99.6vw", height: "95.6vh"}}>
<Router>
<SideNavBar/>
<Switch>
<Route exact path={"/"} render={() => <Redirect to={"/login"}/>}/>
<Route path={"/login"} component={LoginPage}/>
<ProtectedRoute path={"/somepage"} component={somePage}/>
</Switch>
</Router>
</Box>
);
}
}
LoginPage.js
class LoginPage extends Component {
render() {
const {dispatch} = this.props;
return (
<LoginPageContainer>
<img src={logo} alt={""} height={"350rem"}/>
<FilledInput placeholder={"Login or email"}/>
<FilledInput placeholder={"Password"}/>
<Button onClick={() => dispatch({ type: "SIGN_IN" })}>
Sign in
</Button>
</LoginPageContainer>
);
}
}
export default connect(null, null)(LoginPage);
ProtectedRoute.js
import {connectProtectedRoute as connect} from "../redux/connectProtectedRoute";
class ProtectedRoute extends Component {
render() {
const {isLogged, component} = this.props;
return (
<Route render={
() => {
if (isLogged)
return (component);
else
return (<Redirect to={"/login"}/>);
}
}/>
);
}
}
ProtectedRoute.propTypes = {
component: PropTypes.elementType.isRequired
};
export default connect(ProtectedRoute);
connectProtectedRoute.js
import {connect} from "react-redux";
function mapStateToProps(state) {
return ({
isLogged: state.isLogged
});
}
export const connectProtectedRoute = connect(mapStateToProps, null);
reducers.js
const allReducers = combineReducers({
isLogged: isLoggedReducer
});
export default allReducers;
isLoggedReducer.js
const isLoggedReducer = (state = false, action) => {
switch (action.type) {
case "SIGN_IN": return true;
case "SIGN_OUT": return false;
default: return state;
}
}
export default isLoggedReducer;
So I was just unaware of the losing state fact upon refresh. Comment from original post said it all, here they are for anyone ending here:
Modifying the URL manually (outside of react router) will cause a full page refresh and all state will be lost (unless you persist it in local storage or by some other method). This is your problem, nothing in the code looks wrong. – Brian Thompson
Modifying the url causes the page refresh and follows the rerunning the app, so all data in store are removed. Try to use history for page navigation. Here is how to use it. reacttraining.com/react-router/web/api/Hooks/usehistory – TopWebGhost
I am not able to send the parameter through state using useHistory history.push method from react-router dom.
Now suppose I want to pass more than a string to the Paging component i.e. some props too.
My Paging Component which throws error for state value state is not defined
const PAGING = ({ location }) => {
console.log(location);
console.log(location.state);
console.log(location.state.id);
return <div>Hello <div>}
History.push method in another component
const handleDetails = (id,name) => {
console.log(name)
if (id) {
return history.push({
pathname: `/detailing/${name}`,
state: { id }
});
} else {
return history.push("/");
}
};
const Switch = () => {
const { state: authState } = useContext(AuthContext)
return (
<div>
<Router>
<Switch>
<ProtectedSystem
path= "/detailing/:name"
exact
auth={authState.isAuthenticated}
component={PAGING}
/>
</Switch>
</Router>
</div>
);
const ProtectedSystem = ({auth , component: Component, ...rest}) =>{
return(
<Route
{...rest}
render={() => auth ? (<Component/>) : (<Redirect to = '/' /> )}
/>
)
}
If I use simple route without condition based its working fine
<Route path= "/detailing/:name" exact component={PAGING} />
You need to pass on the Route params to the rendered component so that it can use them
const ProtectedSystem = ({auth , component: Component, ...rest}) =>{
return(
<Route
{...rest}
render={(routeParams) => auth ? (<Component {...routeParams}/>) : (<Redirect to = '/' /> )}
/>
)
}
You can do this entirely with React hooks and pure functions, eg.
import React from 'react';
import { useHistory } from 'react-router-dom';
const ProtectedSystem = ({ auth }) => {
const history = useHistory();
if (!authUser) {
history.push("/signin");
}
return (
<div><h1>Authorized user</h1></div>
)
}
export default ProtectedSystem
I'm setting up a basic authentication system with React and while signup and login actions correctly redirect and render the appropriate components, my logout action redirects to the protected route and renders the associated component, even though the authentication variable managed with the context API is successfully updated when logging out. The whole operation works in the end, as when I'm refreshing the page, I am successfully redirected to my login page.
I'm using Node.js to manage my sessions and dispatching the logout action works well as, as I said, the variable used with the Context API is updated. I'm using the Effect Hook on my Header component where the logout is initiated and I can see the auth variable being changed.
Here is my code:
AppRouter.js
export const history = createBrowserHistory();
const AppRouter = () => (
<Router history={history}>
<Switch>
<PublicRoute path="/" component={AuthPage} exact={true} />
<PrivateRoute path="/dashboard" component={DashboardPage} />
<Route component={NotFoundPage} />
</Switch>
</Router>
);
PublicRoute.js
const PublicRoute = ({ component: Component, ...rest }) => {
const { uid } = useContext(AuthContext);
useEffect(() => {
console.log("Public Route - Variable set to:", uid);
}, [uid])
return (
<Route
render={props =>
uid !== undefined ? (
<Redirect to="/dashboard" />
) : (
<Component {...props}/>
)
}
{...rest}
/>
)
};
PrivateRoute.js
const PrivateRoute = ({ component: Component, ...rest }) => {
const { uid } = useContext(AuthContext);
useEffect(() => {
console.log("Private Route - Variable set to:", uid);
}, [uid])
return (
<Route
render={props =>
uid !== undefined ? (
<div>
<Header />
<Component {...props}/>
</div>
) : (
<Redirect to="/" />
)
}
{...rest}
/>
)
};
Header.js
export const Header = () => {
const { uid, dispatch } = useContext(AuthContext);
useEffect(() => {
console.log("Header - Variable set to:", uid);
// console.log("HIST", history);
}, [uid])
const logout = async () => {
const result = await startLogout();
if (result.type !== undefined) {
dispatch(result); // Works well
// window.location.href = '/';
// history.push('/');
history.replace('/');
} else {
console.log(result);
}
}
return (
<header className="header">
<div className="container">
<div className="header__content">
<Link className="header__title" to="/dashboard">
<h1>A React App</h1>
</Link>
<button className="button button--link" onClick={logout}>Logout</button>
</div>
</div>
</header>
);
};
I tried both history.push('/') and history.replace('/'). Both these 2 methods work well as if I switch the path to an unknown route, my component that handles 404 is successfully rendered.
Below is my console output when I click the logout button. As you can see, the auth variable is well updated to undefined but that does not prevent my router to keep showing me the protected route. The router should not redirect me to the dashboard as my auth variable is set to undefined after logging out.
Header - Variable set to: {uid: undefined}
Private Route - Variable set to: {uid: undefined}
Public Route - Variable set to: {uid: undefined}
Header - Variable set to: {uid: undefined}
Private Route - Variable set to: {uid: undefined}
For the time being I'm using window.location.href = '/'; which works well, as it automatically reload the root page but I'd like to stick to react-router. Any thoughts? Thanks
in the private route pass renders props.. like this:
const PrivateRoute = ({ component: Component, ...rest }) => {
const { uid } = useContext(AuthContext);
useEffect(() => {
console.log("Private Route - Variable set to:", uid);
}, [uid])
return (
<Route
render={props =>
uid !== undefined ? (
<div>
<Header {...props} />
<Component {...props}/>
</div>
) : (
<Redirect to="/" />
)
}
{...rest}
/>
)
};
then in header use props to push history:
export const Header = (props) => {
const { uid, dispatch } = useContext(AuthContext);
useEffect(() => {
console.log("Header - Variable set to:", uid);
// console.log("HIST", history);
}, [uid])
const logout = async () => {
const result = await startLogout();
if (result.type !== undefined) {
dispatch(result); // Works well
// window.location.href = '/';
// history.push('/');
props.history.push('/');
} else {
console.log(result);
}
}