How to set state before render with react(redux) - javascript

I want to make a component able to redirect when not loggedIn.
Components were made with react, checking auth function works well with redux.
//App.js
class App extends Component {
checkUserInfo () => {
const loggedInfo = storage.get('loggedInfo');
if(!loggedInfo) return;
const { UserActions } = this.props;
UserActions.setLoggedInfo(loggedInfo)
}
constructor(props) {
super(props);
this.checkUserInfo();
}
render() {
console.log(this.props.logged)
return(...)
}
}
export default connect((state) => ({logged: state.user.get('logged')}, (dispatch)=> ...)
and UserActions.setLoggedInfo action is look like this.
...
export default handleActions({
[SET_LOGGED_INFO]: (state, action) => {
return state.set('logged', true)
}
})
...
So, I want situation that component is redirected when auth is not logged in. I made a rendering component <Route/> with condition which is that if state.logged==false, <Redirect to='login/>.
But in very front point, logged is false before executing checkUserInfo function. so when I'm loggedIn, Redirect to /login, and when I'm not loggedIn, Redirect to /login too.
//PrivateRoute.js
...
render() {
const { logged } = this.props;
console.log(logged);
return(
<Route path="/myPage" render={props =>
logged ? <Component/> : <Redirect to={{pathname: '/login'}}/>
}/>
)
}
...
this is screenshot what is logged value in console.
I want to skip very front state before set state by myFunction(checkUserInfo), how can I do.
plz help me.
and sorry to not good english syntax.

You need to check your global state before rendering the private component.
render prop provided by Route is a good place for that
<Route path='/secretarea' render={() =>{
return props.isLoggedIn ? <SecretComp /> : <Login />
}}/>

Set PrivateRoute like this
This could help to check auth in simple way.

Related

using JWTs saved in localstorage with react context

Im having a really weird issue with My react authentication context (authProvider) and properly rendering my application page. Currently, my root app looks like this:
const App = () => {
const authCtx = useContext(AuthState);
return(
<AuthStateProvider>
<BrowserRouter>
<Switch>
{!authCtx.isLoggedIn && <Route path="/admin" component={SignInUp} />}
{authCtx.isLoggedIn && <Route path="/admin" component={Admin} />}
<Redirect from="/" to="/admin/myDashboard" />
</Switch>
</BrowserRouter>
</AuthStateProvider>
)};
Then in a seperate file that I use to manage the authentication context, I attempt to pull a JWT from local storage and verify it. If that's successful then the context is updated using state variables (including the "isLoggedIn" variable you see above").
const AuthState = React.createContext({
userName: "",
isLoggedIn: false,
authToken: null,
});
const AuthStateProvider = (props) => {
let token = null;
if(localStorage.getItem("token")) return token = localStorage.getItem("token");
const [ user, setUser ] = useState({
userName: "Anonymous",
isLoggedIn: false,
authToken: token,
});
const autoLogin = useCallback( async () => {
try {
const response = await axios({
method:'post',
url: 'http://127.0.0.1:3001/authEn'
headers: {
"Content-Type": "application/json",
"Authentication": user.authToken
}
});
if(response.status === 200){
//code to update context using setUser state handler
} else {
throw new Error("request failed");
}
} catch (e) {
console.log(e.message);
}
});
useEffect( async () => {
await autoLogin();
}, [autoLogin]);
return (
<AuthState.Provider
value={{
userName: user.userName,
isLoggedIn: user.isLoggedIn,
authToken: user.authToken
}}
>
{props.children}
</AuthState.Provider>
);
}
(I've excluded the code for my setUser handler to try and keep this short.)
So the problem is that as of right now, I'm just trying to see that the application can
A: check for stored token on initial page load / reload
B: Navigate you to either logIn or Admin page accordingly.
The app has no problem taking you to logIn page if there is a faulty/no JWT in localstorage. But when I try testing if the application can properly navigate to the admin page when there is a valid token in local storage (i have a seperate helper function to save a valid token), the page loads, but with NONE of the actual admin dashboard. Instead, all there is on the page is the token itself displayed at the top of the window as if it were just an html page with a single div containing the token as a string. I have no Idea why this happens. When I try rendering the admin component (removing the "isLoggedIn" logic and the authStateProvider) everything is fine. But each time I try adding authentication this way things start getting weird. Am I missing something obvious (usually the case)? Is this just a really stupid approach (also usually the case)? Or is this a low-level react issue (I'm not super familiar with all the intricacies of how react works under the hood.)
I think this is a bad practice to make conditions in the Switch.
Instead, you can create a separate component like ProtectedRoute or wrap your pages with a Higher-Order Component (HOC)
First way with ProtectedRoute
Pass isProtected in props if your wrapped route requires authentification
// ProtectedRoute.js
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
const ProtectedRoute = ({
isProtected,
children,
}) => {
const { isLoggedIn } = useContext(AuthState);
if (isProtected && !isLoggedIn) {
return <Redirect to='/login' />;
}
return children
};
export default ProtectedRoute;
Then in your switch:
<Switch>
{ /* Other routes */ }
<ProtectedRoute isProtected>
<Route path="/admin" component={Admin} />
</ProtectedRoute>
</Switch>
HOC
// withAuth
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
const withAuth = (WrappedComponent: any) =>
function (props: any) {
const { isLoggedIn ) = useContext(AuthState);
if (!isLoggedIn) {
<Redirect to='/login' />
}
return <WrappedComponent {...props} />;
}
export default withAuth;
Now you can insert your route in the switch without conditions. If your component requires authentification, wrap it with withAuth.
Example:
const NeedAuth = () => (
<div>Hello I need auth</div>
);
export default withAuth(NeedAuth)
I figured out the issue, and yes it was something super small. In the line of code where I check to see if there is a token stored on localstorage, I use an if block with a return statement. I saw a while back that doing this allows for "if" statements to be written completely on a single line and without brackets {} encapsulating the code. At the time it was really just a style choice but now I see that when the if statement runs (i.e. there is a token in local storage) the return statement within overrides the return statement of the whole functional component. So rather than having a context file that returns a provider that wraps your desired children (my admin page router), It just prints the authtoken. So I returned to traditional styling for the If statement and removed the return statement and it worked fine!

How to make globally accessible info through Context API to be saved locally to persist across a refresh?

I'm trying to manage user login with context API and here's my auth.js:
const AuthContext = createContext({
isAuthenticated: false
});
class AuthProvider extends React.Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false
};
this.signIn = this.signIn.bind(this);
this.logout = this.logout.bind(this);
}
/**
* signIn function
*/
signIn() {
setTimeout(() => this.setState({ isAuthenticated: true }), 1000);
}
/**
* logOut function
*/
logout(){
setTimeout(()=>this.setState({isAuthenticated:false}),1000);
}
render() {
return (
<AuthContext.Provider value={{
isAuthenticated: this.state.isAuthenticated,
signIn: this.signIn,
logout:this.logout,
getStatus:this.getStatus
}}>
{this.props.children}
</AuthContext.Provider>
)
}
}
const AuthConsumer = AuthContext.Consumer;
export {AuthProvider,AuthConsumer};
And I have some protected routes, here's my ProtectedRoute component:
export default function ProtectedRoute({...props }) {
return (
<AuthConsumer>
{({ isAuthenticated }) => (
<Route
render={() =>
isAuthenticated ?
<Redirect to="/" /> :
(
<Route {...props} />
)}
/>
)}
</AuthConsumer>
);
}
And a Protected Route like login, Registration component and ... would be sth like this:
<ProtectedRoute exact path="/profile" component={UserProfile}/>
Now if you login from login page and use the signIn function the, the isAuthenticated flag from AuthProvider would be set to true ergo you wouldn't be able to go to login or registration page after login(they R protected and trying to access them would redirect you to homePage ), everything works fine as long as you try to manually change the addressbar to the desired path(say "/login") from the address bar and that is when isAuthenticated flag gets set to false like no login has happened before, so my question is why is it not working like promised in this case and what I should do to protect the routes completely ?
Thats because on changing the url it reloads the page which re-initialize the state the its default state. In single page applications(React, vue) the state of applications is re-initialize to default when reloaded.
The solution for this is the save token(or authentication identifier used) in local storage and and initialize the state of app from the local storage
As mentioned by #Umair Farooq "The state of the application is re-initialized to default when it is reloaded", so how to make the shared global state to persist across a refresh? We use localStorage from windowobject so the code in auth.js will change into this:
//auth.js
......
let localState = JSON.parse(localStorage.getItem("hasLoggedIn"));
Here, we’re going to pull in whatever value might be stored locally as "hasLoggedIn" and parse it into a JSON object (local storage can only store strings, so we have to parse it back and forth).
Next we are going to change initial state in the constructor as such(we’re going to set the initial state of to the localState if it exists, and, otherwise, the initialState that we defined earlier):
//auth.js
....
constructor(props) {
super(props);
this.state = {
user: null,
isAuthenticated: false || localState
};
this.signIn = this.signIn.bind(this);
this.logout = this.logout.bind(this);
}
Next, we are going to modify the "signIn" & "logout" functions as such:
//auth.js
....
signIn() {
setTimeout(() => this.setState({ isAuthenticated: true },()=>{
localStorage.setItem("hasLoggedIn",true);
}), 1000);
}
/**
* logOut function
*/
logout(){
setTimeout(()=>this.setState({isAuthenticated:false},()=>{
localStorage.removeItem("hasLoggedIn");
}),1000);
}

The props of a component rendered inside a route will never update

What I am trying to do is getting the login user's information from the login component, so that the App component could pass the login user's information to the main component.
Here is some code in the App.js:
state = {
usersArr: [], // all the users
logInUser: 0, // the login user, default value as 0
}
getLogInUser = () => {
return this.state.logInUser;
}
assignLogInUser = user =>{
this.setState({logInUser: user});
console.log("LogIn: " + this.state.logInUser.username);
}
render(){
return(
<div className="App">
<Router>
<div className="App">
<Switch>
<Route path="/main" exact render={() => <Main logInUser={() => this.getLogInUser()}/>}/>
<Route path="" exact render={() => <Landing usersArr={this.state.usersArr} getUser = {this.assignLogInUser}/>}></Route>
</Switch>
</div>
</Router>
)
}
The first page would be landing page and the landing component would be rendered. The landing component will render the login component. The login component will check if the username and password from input would match any user in the usersArr. If it does match, the login component will call the assignLogInUser to assign the login user.
landing.jsx:
render(){
return(
<Login usersArr={this.props.usersArr} getUser={this.props.getUser}/>
)
}
login.jsx:
constructor(props){
super(props);
this.userName = React.createRef(); // username referrence
this.password = React.createRef(); // password referrence
}
validateLogin = () => {
this.props.usersArr.forEach(user => {
if(user.username === this.userName.current.value && user.password === this.password.current.value){
this.props.getUser(user);
}
})
}
The problem is that the logInUser passed into the main is always 0.
Looks like once the route has been set up, it will never update the component it's going to render. No matter what I tried, I would get the default value 0 for logInUser in the main component.
I can probably use redux, but I have to refactor all of the code.
I have tried forceUpdate() in the main component, or using key in the main component. None of them works.
Any help would be appreciated.

Using lifecycle method with redux versus checking prop state in render method

Is there anything wrong with this flow in redux? I change isAuthenticated in my reducer like so
export function userReducer(state=initState, action) {
switch(action.type){
case AUTH_SUCCESS:
return {
...state,
isAuthenticated: true
}
//...
//...
}
}
so in my render I check isAuthenticated if equal to true I redirect the user to login.
#connect(state => state.user, {loginUser})
class LoginForm extends React.Component {
constructor() {
super()
this.state = {
email: '',
password: ''
}
}
handleSubmit() {
const { email, password } = this.state
this.props.login(email, password)
}
render(){
const { email, password } = this.state
return(
<div>
{this.props.user.isAuthenticated && <Redirect to={'/dashboard'} />}
<Form input={{email, password}} handleSubmit={this.handleSubmit} />
</div>
)
}
}
export default LoginForm
Another way is I can make the render method cleaner, I use componentWillRecieiveProps
componentWillReceiveProps(nextProps) {
if(nextProps.user.isAuthenticated !== this.props.user.isAuthenticated && nextProps.user.isAuthenticated) {
this.props.history.replace('/dashboard')
}
}
Which one is appropriate and why?
They are both valid ways, except componentWillReceiveProps is deprecated in react v16.3 so it would be better to use componentDidUpdate. And I would simplify logic as follows:
componentDidUpdate(prevProps) {
if(prevProps.user.isAuthenticated !== this.props.user.isAuthenticated) {
this.props.history.replace('/dashboard')
}
}
There is one difference though, that with Redirect you don't have to create class component but you can use it in stateless component and it doesn't even have to be inside Route or connected withRouter.
const SomeComponent = ({ isLogged }) => (
<div>
{isLogged <Redirect to="/path" />}
<div>something</div>
</div>
)
Both of your methods are equivalent since, Redirect also replaces the current history stack and so does this.props.history.replace(). You can use anyone of the methods.
Also considering that React wants to deprecate the componentWillReceiveProps from v17 onwards you are better of using componentDidUpdate for the same
However componentDidUpdate here has a slight disadvantage here that for a fraction of a second Form will be visible and then redirect will happen which might be a bad user experience.
So consider future development in React, you might use the Redirect approach instead of doing prop comparison and using history.replace()
As others have said, they're both valid.
Just from a react point of view, a login component should handle exactly that - logging in.
I would expect a higher component than the login page (the app perhaps), to do the logic of whether to display the Login form, or to redirect. E.g. something like this:
import {withRouter} from 'react-router-dom';
class App {
render() {
if (this.props.user.isAuthenticated && this.props.location === '/login') {
return <Redirect to={'/dashboard'} />;
}
return (<div>
REST OF APP HERE
</div>);
}
}
export default withRouter(props => <App {...props}/>);
The benefit here is you can easily test your login form, as well as compose it within other components really easily. Your routing concerns are effectively global, and those can live up at the top with your app.

React Router v4: Sending requests when navigation changes

I'm coding an authentication with react-router v4 and I'm using the PrivateRoute with render props, like the documentation: Redirects (Auth)
What I'm trying to do is: Whenever the user navigates to a route, I want to dispatch an action to make a request to the backend to verify if he's logged in.
Like this:
// App.js
class App extends Component {
checkAuth = () => {
const { dispatch, } = this.props;
// callback to dispatch
}
render() {
const props = this.props;
return (
<Router>
<div className="App">
<Switch>
<Route exact path="/" component={Login} />
<PrivateRoute
exact
path="/dashboard"
component={Dashboard}
checkIsLoggedIn={this.checkAuth}
/>
{/* ... other private routes here */}
</Switch>
</div>
</Router>
);
}
In PrivateRoute.js I'm listening the route to check if it changes, but when a route changes, this function is called too many times, and that's a problem to dispatch an action to make a request.
// PrivateRoute.js
const PrivateRoute = ({ component: Component, auth, checkIsLoggedIn, ...rest }) => (
<Route
{...rest}
render={props => {
props.history.listen((location, action) => {
if (checkIsLoggedIn) {
// Here I check if the route changed, but it render too many times to make a request
checkIsLoggedIn(); // here is the callback props
}
});
if (auth.login.isLoggedIn) {
return <Component {...props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />
}
}
}
/>
);
I need a help to figure it out a good way to call the backend whenever the route changes.
Creating a Higher Order Component (HOC) is a very clean way to do this. This way, you won't need to create a separate PrivateRoute component, and it would take only one line of change to convert any Component from public to protected, or vice versa.
Something like this should work:
import React from 'react';
import { Redirect } from "react-router-dom";
export function withAuth(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
isUserLoggedIn: false,
isLoading: true
};
}
componentDidMount() {
// Check for authentication when the component is mounted
this.checkAuthentication();
}
checkAuthentication() {
// Put some logic here to check authentication
// You can make a server call if you wish,
// but it will be faster if you read the logged-in state
// from cookies or something.
// Making a server call before every protected component,
// will be very expensive, and will be a poor user experience.
this.setState({
isUserLoggedIn: true, // Set to true or false depending upon the result of your auth check logic
isLoading: false
});
}
render() {
// Optionally, you can add logic here to show a common loading animation,
// or anything really, while the component checks for auth status.
// You can also return null, if you don't want any special handling here.
if (this.state.isLoading) return (<LoadingAnimation />);
// This part will load your component if user is logged in,
// else it will redirect to the login route
if (this.state.isUserLoggedIn) {
return <WrappedComponent authData={this.state} {...this.props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />;
}
}
}
}
Once you have that component in place, all you need to do is use the HOC in any component that you wish to have protected. For example, in your case, the export line in your Dashboard file would be something like this:
/* Dashboard.js */
class Dashboard extends React.Component { ... }
export default withAuth(Dashboard);
and in your App, you can use a simple Route component:
<Route exact path='/dashboard' component={Dashboard} />
Your App does not need to care about which routes are protected, and which ones aren't. In fact, only the actual components need to know that they are protected.
Hope this helps. Cheers! :)

Categories