Adam Roberts
Adam Roberts

Reputation: 692

React router render causing unmount

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

Answers (1)

aviya.developer
aviya.developer

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;

EDIT:

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:

  • install history packages: npm i --save history.

in the file where the <App /> component is do:

  • import createHistory from 'history/createBrowserHistory'.
  • const history = createHistory();
  • Use Router instead of BrowserRouter: import {Router} from 'react-router';
  • pass the 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

Related Questions