1
votes

I'm creating a React Router app and I'm learning authentication. Here are some parts:

Login component (login.jsx)

  • authenticates user's credential on the back end
  • sets the authentication token in local storage
  • return's the user's data
  • sets state with the data
  • passes state to Admin component

Admin component (admin.jsx)

  • protected by a private route (see privateroute.jsx)
  • basically a container component
  • passes user's data to and renders other components that display/edit the data

Authentication Button on the nav bar (authbutton.jsx)

  • checks if user is logged in and renders "login" or "logout" button
  • if logged in, also renders "my posts" button that routes to Admin

All works well from Login to Admin. My problem is that when I click away from the Admin page (like the home page) and then click on "my posts" button, it reroutes to Admin and knows I'm logged in, but the user's data is no longer available. Before, coming from the login component, the user's data was in this.props.location.state.me.

I'm stuck because I'm trying to route to Admin from two different components and I've never done that before. Furthermore, I feel like there's a solution in the authentication setup that I'm missing.

Other ideas:

Should I conditionally set state in Admin when the user's data is passed?

Should I store the data in local storage in the browser like I'm doing with the authentication token?

I tried fetching data, setting state in Admin with componentDidMount but it didn't re-render so I read to use componentWillReceiveProps but that's being deprecated and replaced with getDerivedStateFromProps. Could not figure that out.

login.jsx

import React, { Component, Fragment } from 'react';
import * as userService from '../../services/user';
import { Redirect } from 'react-router-dom';
import IndeterminateProgress from '../utilities/indeterminateprogress';
import Nav from '../home/nav';

class Login extends Component {
    constructor(props) {
        super(props);
        this.state = {
            redirectToReferrer: false,
            email: '',
            password: '',
            feedbackMessage: '',
            checkingLogin: true,
            me: ''
        };
    }

    componentDidMount() {
        userService.checkLogin()
            .then((loggedIn) => {
                if (loggedIn) {
                    this.setState({ redirectToReferrer: true, checkingLogin: false });
                } else {
                    this.setState({ checkingLogin: false });
                }
            });
    }

    login(e) {
        e.preventDefault();
        userService.login(this.state.email, this.state.password)
            .then((meData) => {
                this.setState({ redirectToReferrer: true, me: meData })
            })  
            .catch((err) => {
                if (err.message) {
                    this.setState({ feedbackMessage: err.message });
                }
            });
    }

    handleEmailChange(value) {
        this.setState({ email: value });
    }

    handlePasswordChange(value) {
        this.setState({ password: value });
    }

    render() {
        const { from } = this.props.location.state || { from: { pathname: '/admin', state: { ...this.state } } };
        const { redirectToReferrer, checkingLogin } = this.state;

        if (checkingLogin) {
            return <IndeterminateProgress message="Checking Login Status..." />;
        }
        if (redirectToReferrer) {
            return (
                <Redirect to={from} />
            );
        }

        return (
            <Fragment>
                <Nav />
                <h2 className="heading center">Login to continue</h2>
                <form className="center" onSubmit={(e) => this.login(e)}>
                    <div className="form-group">
                        <input
                            placeholder="Email"
                            id="email"
                            className="col-3"
                            type="email"
                            onChange={(e) => this.handleEmailChange(e.target.value)}
                            required
                        />
                    </div>
                    <div className="form-group">
                        <input
                            placeholder="Password"
                            id="password"
                            className="col-3"
                            type="password"
                            onChange={(e) => this.handlePasswordChange(e.target.value)}
                            required
                        />
                    </div>
                    {this.state.feedbackMessage ? (
                        <p>{this.state.feedbackMessage}</p>
                    ) : null}
                    <input type="submit" value="Login" className="btn btn-info btn-sm" />
                </form>
            </Fragment>
        );
    }
}

export { Login };

admin.jsx

import React, { Component } from 'react';
import Nav from '../home/nav';
import AdminBlogContainer from './adminblogcontainer'
import { BrowserRouter as Router, Link } from 'react-router-dom';

const Admin = (props) => {
    return (
        <div className="flexcol center">
            <Nav />
            <h1 className="heading">Your Blog Posts</h1>
            <AdminBlogContainer {...props.location.state.me} />
            <Link to={{
                pathname: '/write',
                state: { ...props.location.state.me }
            }}
                className="btn btn-outline-secondary mt-4"
            >Create a New Blog Post</Link>
        </div>
    )
}

export { Admin };

privateroute.jsx

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { isLoggedIn } from '../../services/user';

const PrivateRoute = (props) => {
    const Component = props.component;
    const propsToPass = Object.assign({}, props);
    delete propsToPass.component;

    return (
        <Route {...propsToPass} render={props => (
            isLoggedIn() ? (
                <Component {...props} />
            ) : (
                <Redirect to={{
                    pathname: '/login',
                    state: { from: props.location }
                }} />
            )
        )} />
    );
};

export { PrivateRoute }

authbutton.jsx

import React from 'react';
import { Link } from 'react-router-dom';
import { isLoggedIn } from '../../services/user';

const AuthButton = (props) => {
    if (isLoggedIn()) {
        return (
            <div>
                <Link className="btn btn-info m-1" to="/logout">Logout</Link>
                <Link className='btn btn-info m-1' to={{
                    pathname: '/admin',
                    // state: { ...this.state }
                }}
                    >My Posts</Link>
            </div>
        );
    } else {
        return (
            <div>
                <Link className="btn btn-info m-1" to="/login">Login</Link>
                <Link className="btn btn-info m-1" to="/register">Register</Link>
            </div>
        )
    }
};

export { AuthButton };
2

2 Answers

1
votes

This is one of the reasons as to the creation of stores. Think of it as a global object that can be accessed anywhere within your application.

I personally have used mobx/mobx-react (easy and does things magically) and there is also redux

Using mobx you can do something like so:

Global store

// /stores/authentication.js
class AuthenticationStore {
   user = {};
   //authentication logic here
}
const authenticationStore = new AuthenticationStore();
export default authenticationStore;

Root App Component

// /app.js
import authenticationStore from './stores/authentication';
import { Provider } from 'mobx-react';

export default class App extends Component {

  render(){
   return (
   <Provider authenticationStore={authenticationStore}>
      <BrowserRouter>
         <SomeComponent/>
      </BrowserRouter>
   <Provider>);
   }

}

Some component;

// /components/some.component.js
@inject('authenticationStore')
class SomeComponent extends Component {
    render(){
       const {authenticationStore} = this.props;
       const {user} = authenticationStore;
       render(
          <div>${user.name}</div>
       )
    }
}
1
votes

There seem to be two main solutions to my problem. 1 is to use a store as answered on this page. The other is to store the data in local storage, which looks like:

in login.jsx set local storage instead of setting state

login(e) {
        e.preventDefault();
        userService.login(this.state.email, this.state.password)
            .then((meData) => {
                localStorage.setItem("me", JSON.stringify(meData))
                this.setState({ redirectToReferrer: true})
            })  
            .catch((err) => {
                if (err.message) {
                    this.setState({ feedbackMessage: err.message });
                }
            });
    }

retrieve data in adminblogcontainer.jsx

componentDidMount() {
    let meData = JSON.parse((localStorage.getItem("me")))
    authorsService.one(meData.id) ...