Reputation: 692
I'm trying to implement authenticated routes (as per React router docs) alongisde dynamic theming from my top level component (essentially the theme changes when a menu item is selected but no redirection occurs) - this means a small amount of state management is needed in my top level component.
function App() {
const [ state, setState ] = React.useState({
current_theme: themes['blue']
});
const [ logged_in, setLoggedIn ] = React.useState( !!Cookie.get('JWT') );
function selectTheme( theme ) {
setState({
current_theme: themes[theme]
});
};
return (
<MuiThemeProvider theme={state.current_theme}>
<>
{
logged_in && <Header selectTheme={ selectTheme }/>
}
<AppContainer logged_in={ logged_in }>
<Switch>
<Route exact path='/' component={ Home }/>
<Route exact path='/service_users' component={ ServiceUsers } />
</Switch>
</AppContainer>
</>
</MuiThemeProvider>
);
}
export default App;
Before adding authenticated routes, this was all working fine. Users would be at '/service_users' for example, would click a menu item which would cause a top-level state change and would re-render the components with the correct colours but other aspects of children wouldn't change (as there'd been no change affecting them)
This was exactly what I was looking for, however when adding authenticated routes, the render
prop on Route
is used. The issue here is that, when state changes within the top level component, the children are completely unmounted and re-mounted. This causes a loss of state and everything the user currently sees is lost.
Updated code:
function PrivateRoute({ component: Component, ...rest }) {
return (
<Route
{ ...rest }
render={ props => {
return logged_in ? (
<Component { ...props } />
) : (
<Redirect
to={{
pathname: '/login',
state: { from: props.location }
}}
/>
)
}}
/>
);
}
function App() {
const [ state, setState ] = React.useState({
current_theme: themes['blue']
});
const [ logged_in, setLoggedIn ] = React.useState( !!Cookie.get('JWT') );
function selectTheme( theme ) {
setState({
current_theme: themes[theme]
});
};
function login() {
setLoggedIn( true );
}
return (
<MuiThemeProvider theme={state.current_theme}>
<Router>
{
logged_in && <Header drawer_open={ drawer_open } selectTheme={ selectTheme }/>
}
<AppContainer logged_in={ logged_in }>
<Switch>
<PrivateRoute exact path='/' component={ ServiceUsers }/>
<PrivateRoute exact path='/service_users' component={ ServiceUsers } />
<Route exact path='/login' render={ ( props ) => <Login { ...props } setLogin={ login.bind( this ) }/> } />
</Switch>
</AppContainer>
</Router>
</MuiThemeProvider>
);
}
export default App;
Login.js:
function Login( props ) {
const classes = useStyles();
const [ state, setState ] = useState({
username: '',
password: ''
});
const [ loading, setLoading ] = useState( false );
const [ success, setSuccess ] = useState( !!Cookie.get('JWT') );
console.log( 'success: ', success );
function onInputChange( e ) {
setState({ ...state, [e.target.id]: e.target.value });
}
async function loginRequest( e ) {
e.preventDefault();
const { username, password } = state;
//TODO: validation of email/password
if( username.length < 5 || password.length < 5 )
return;
setLoading( true );
const res = await asyncAjax( 'POST', '/login', { username, password } );
setLoading( false );
if( res.status !== 200 )
console.log( 'ERROR' ); //TODO: add error handling
//Store JWT and systems
Cookie.set( 'JWT', `Bearer ${ res.token }`, { path: '/', days: 30 } );
//Use local storage for systems as likely to be much more data
localStorage.setItem( 'SYSTEMS', JSON.stringify( res.login.systems ) );
//Set login status and push user to referrer
props.setLogin();
props.history.push( from );
}
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<HeaderLogo/>
<form className={classes.form} noValidate>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="username"
label="Email Address"
name="username"
autoComplete="username"
autoFocus
onChange={ onInputChange }
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
onChange={ onInputChange }
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={ loginRequest }
>
{
loading ? (
<CircularProgress
color='inherit'
size={ 25 }
style={{ color: '#FFF' }}
/>
) : (
'Login'
)
}
</Button>
<Grid container>
<Grid item xs>
<Link href="#" variant="body2">
Forgot password?
</Link>
</Grid>
</Grid>
</form>
</div>
</Container>
);
}
export default Login;
My question is how do I get around this? I believe the issue is occuring as the protected routes are generated from a function, causing a state change/re-render in the parent causes the function to re-run and new components to be returned. So how do i render these protected routes outside of a function/without a re-render causing them to be unmounted/re-mounted?
EDIT: That's now working - thank you. However, now when login is successful, the user is not being redirected to the previous page from the Login component (this part at least worked fine before adding the fix). props.history
exists and there are no errors, but the URL does not change unless i add forceRefresh
to the Router which I cannot do.
I've edited the updated code block and added the Login function from Login.js
Upvotes: 4
Views: 1413
Reputation: 3603
So how do i render these protected routes outside of a function/without a re-render causing them to be unmounted/re-mounted?
I am not sure that's the source of your problem, but have you tried to just build a <PrivateRoute />
component outside of the <App />
component?
something like this:
function App() {
const [ state, setState ] = React.useState({
current_theme: themes['blue']
});
const [ logged_in, setLoggedIn ] = React.useState( !!Cookie.get('JWT') );
function selectTheme( theme ) {
setState({
current_theme: themes[theme]
});
};
function login() {
setLoggedIn( true );
}
return (
<MuiThemeProvider theme={state.current_theme}>
<>
{
logged_in && <Header selectTheme={ selectTheme }/>
}
<AppContainer logged_in={ logged_in }>
<Switch>
<PrivateRoute exact path='/' component={ ServiceUsers } logged_in={logged_in}, log_in={login}/>
<PrivateRoute exact path='/service_users' component={ ServiceUsers } logged_in={logged_in}, log_in={login} />
<Route exact path='/login' render={ ( props ) => <Login { ...props } setLogin={ login.bind( this ) }/> } />
</Switch>
</AppContainer>
</>
</MuiThemeProvider>
);
}
function PrivateRoute({ component: Component, logged_in, log_in, ...rest }) {
return (
<Route
{ ...rest }
render={ props => {
return props.logged_in ? (
<Component { ...props } />
) : (
<Redirect
to={{
pathname: '/login',
setLogin: log_in.bind( this ),
state: { from: props.location, test: 'test' }
}}
/>
)
}
}
/>
);
}
export default App;
So I wanted to address your routing redirecting issue now that you've mentioned it. I think the problem is that the history
object is not accessible independently to the function. I'm not sure why it was available previously, since you havn't shared more code and the structure of you factoring.
Anyways, the following steps should allow you to use an independent and exported history object:
npm i --save history
.in the file where the <App />
component is do:
import createHistory from 'history/createBrowserHistory'
.const history = createHistory();
Router
instead of BrowserRouter
: import {Router} from 'react-router';
history
object as a prop to the Router
: <Router history={history}
.So all of this is done so you can have an independent history object, and now you need to export it. So a little fix:
* export const history = createHistory();
Now you have and exported independent history object that you can import and use at login.js
:
import {history} from './app.jsx';
(put your own proper path of course)That's it. Now you can use it in login.js and the function in that file.
Upvotes: 5