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.
Related
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!
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.
I have lots of static forms which i show the user when he clicks on the main menu and goes to a specific route, what i want to do now is to check when going to a route if that component has permission to be visited, i can do this by doing a simple post to server but i am confused and i don't know where should be the place to do this check or post.
Here are some of the solutions i thought of:
1- Writing a Higher order component and wrapping each static component with it
2- creating a base class and making each static form to inherit it while doing this check in the parent class
3- Or maybe using the routes as a solution since i am using the react-router ?
I will appreciate any help or tips.
Thanks.
Create a custom hook like so:-
const useAdmin = (url:string) => {
const [admin, setAdmin] = React.useState(false);
React.useEffect(() => {
post(url, {some: body}).then(res => {
setAdmin(res.admin);
}).catch(err => {
setAdmin(false);
});
}, [])
return [admin];
}
Then use it anywhere:-
const mycomponent = props => {
const [admin] = useAdmin('https://foobar.com/checkPermission');
//Rest of the logic according to the variable 'admin'
return (
<div>
{
admin? <div/>:null
}
</div>
)
}
Or think of admin as permission. Pass it some url for different routes and it will handle it.
I do something similar using react-router as well. I made my own route component that wraps around react-router's route that checks permissions and conditionally renders the route or redirects.
If you're doing the api call each time, then it would look something close to this.
class AppRoute extends Component {
state = {
validCredentials: null
}
componentDidMount() {
// api call here + response
if (ok) {
this.setState({validCredentials: true})
} else {
this.setState({ validCredentials: false})
}
}
render() {
const { validCredentials } = this.state
if (validCredentials) {
return <Route {...this.props} />
} else if (validCredentials === false) {
return <Redirect to="somewhere"/>
}
return null
}
}
You can definitely accomplish this using a Higher Order Component. Just set a state for the user on login like "admin" or "basic_user." According to this state some buttons or forms are going to be available for the user to access. You can also save these access permissions to your backend and call it in the HOC whenever the user logs in.
I have the following code:
import { Redirect } from 'react-router-dom';
import QPContent from '../QPContent';
class AnswerContainer extends Component {
constructor(props) {
super(props);
this.state = {
redirect: false
};
}
render() {
const { redirect } = this.state;
if (redirect) {
return <Redirect push to={'/question/'+this.props.qID}/>;
}
return (
<div className="qp-answer-container">
....
</div>
);
}
}
const mapDispatchToProps = (dispatch, ownprops) => ({
....
});
const mapStateToProps = (state) => ({
....
});
export default connect(mapStateToProps, mapDispatchToProps)(AnswerContainer);`
I have an array of AnswerContainer components in a parent component. I am currently in the route /question/qId. I am trying to refresh the page on setting redirect: true. So when I use setState() and change the state of only one component in the list to redirect: true, the parent component doesn't re-render. Instead, the child component disappears. Why is this happening and how to trigger re-render of the whole page on <Redirect/> ?
Note: I'm calling setState() inside an action dispatch
If you really want to re-render the whole page, just use window.location.reload().
The idea of <Redirect /> component is to update the current url, not to re-render components. Of course they usually do re-render since most commonly you display something else when the url changes. But the job of react is to re-render as little as possible.
I have a PageBuilder component that dynamically builds edit/list pages according to a configuration file. I want to have dynamic routes (like "/collection/list", "/collection/edit/123123", "/dashboard", etc.) that use the same PageBuilder component.
I'm having trouble getting this to work - if I'm in "/collection/list" for example, when clicking on a link to "/collection/edit/1231" doesn't work. Only a refresh to that URL works (and vice-versa).
I tried putting my initialization code PageBuilder's componentWilLReceiveProps but it seems to call it every second.
My routes look like this:
<Route path="/" component={App}>
<IndexRedirect to="/dashboard" />
<Route path="/:page/:collection/:action(/:entity_id)" component={PageBuilder} />
<Route path="/:page" component={PageBuilder} />
</Route>
And my PageBuilder:
constructor(props) {
super(props);
this.createSectionsHTML = this.createSectionsHTML.bind(this);
this.onChange = this.onChange.bind(this);
this.onSave = this.onSave.bind(this);
}
getPageName() {
return this.props.params.page.replace(/-/g, '_').toLowerCase();
}
componentWillReceiveProps(props) {
this.action = this.props.params.action;
}
componentWillMount() {
let pageName = this.getPageName();
this.props.dispatch(setInitialItem(pageName));
}
componentDidMount() {
let pageName = this.getPageName();
let { collection, entity_id } = this.props.params;
if (collection && entity_id) {
let { dispatch } = this.props;
dispatch(getCollectionEntity(collection, entity_id, pageName));
}
}
Any ideas of how to re-render the page each time I redirect to a different route?
It would be great if I could unmount and re-mount the component when redirecting, but I'm not sure how to go about telling React Router to do that....
Thanks!
Make this.state such that it will control how your component gets rendered.
Now, within componentWillReceiveProps, check the nextProps argument
componentWillReceiveProps(nextProps, nextState) {
if( <check in nextProps if my route has changed> ) {
let newState = Object.assign({}, this.state);
// make necessary changes to the nextState Object by calling
// functions which would change the rendering of the current page
this.setState({ nextState });
}
}
This would make componentWillReceiveProps take action only when the route changes.
Now in your render function,
render() {
const { necessary, variables, to, render } = this.state;
let renderVariables = this.utilityFunctionsReqToRender(someArgs);
return (
<toRenderJsx>
...
</toRenderJsx>
)
}
This would make your component "refresh" whenever the route changes.
componentWillReceiveProps is deprecated since React 16.3.0
(as says a warning in the browser console)
Reference :
https://reactjs.org/docs/react-component.html#componentdidupdate
So componentDidUpdate can be used to get the new state and reload data depending on params
componentDidUpdate(prevProps, prevState, snapshot) {
console.log("componentDidUpdate " +prevState.id);
reloadData(prevState.id);
}