Meteor/React - How to wait for Meteor.user()
Asked Answered
C

6

11

I'm struggling to figure out how to wait for the Meteor.user() subscription in a react component. I know the user is logged in because Meteor.userId() returns the _id, but trying to access the email address show Meteor.user() returns undefined. I assume because it's not available just yet.

Alas, since Meteor.user() isn't a publication I manually subscribe to, I'm not sure how to wait for it in a React component. Can someone point me towards an example?

import { Meteor } from 'meteor/meteor';
import React from 'react';
import { Link } from 'react-router';
import { createContainer } from 'meteor/react-meteor-data';

import './BaseLayout.scss';


class BaseLayout extends React.Component {

    constructor(props) {
        super(props);

        this.state = {
            isAuthenticated: (Meteor.userId() !== null) ? true : false,
        }
    }

    componentWillMount() {
        if (!this.state.isAuthenticated) {
            this.context.router.push('/login');
        }
    }

    componentDidUpdate(prevProps, prevState) {
        if (!this.state.isAuthenticated) {
            this.context.router.push('/login');
        }
    }

    toggleUserMenu() {
        this.refs.userMenu.style.display = (this.refs.userMenu.style.display === 'block') ? 'none' : 'block';
    }

    logout(e) {
        e.preventDefault();
        Meteor.logout();
        this.context.router.push('/login');
    }

    render() {
        return(
            <div id="base">
                <div id="header">
                    <div id="top-bar" className="clear-fix">
                        <div className="float-left" id="logo"></div>
                        <div className="float-right" id="user-menu">
                            <div onClick={this.toggleUserMenu.bind(this)}>
                                <span>{this.props.currentUser.emails[0].address}</span>
                                <div className="arrow-down"></div>
                            </div>
                            <div id="user-menu-content" ref="userMenu">
                                <a onClick={this.logout.bind(this)}>Sign Out</a>
                            </div>
                        </div>
                    </div>
                </div>
                <div id="bodyContainer">
                    {this.props.children}
                </div>
                <div id="footer">
                    <ul>
                        <li><a href="mailto:[email protected]">Made by Us</a></li>
                    </ul>
                </div>
            </div>
        )
    }
}
BaseLayout.contextTypes = {
    router: React.PropTypes.object.isRequired,
}


export default BaseLayoutContainer = createContainer(() => {

    var handle = Meteor.subscribe("user.classrooms");

    return {
        currentUser: Meteor.user(),
        dataLoading: !handle.ready(),
        classrooms: Classrooms.find({}).fetch(),
    };
}, BaseLayout);
Cobden answered 18/9, 2016 at 16:54 Comment(0)
A
5

You could use a ternary operation

currentUser ? true : flase

Also a great tip to keep components organized is to always destructure for example instead of doing const currentUser = this.props.currentUser you can do const { currentUser } = this.props; Have a look:

render() {
const { currentUser, children } = this.props;
  return (
  <div id="base">
    <div id="header">
      <div id="top-bar" className="clear-fix">
        <div className="float-left" id="logo"></div>
        <div className="float-right" id="user-menu">
          <div onClick={this.toggleUserMenu.bind(this)}>
           {currentUser ?
             <span>{currentUser.emails[0].address}</span> :
             <span>Loading...</span>
           }
            <div className="arrow-down"></div>
          </div>
          <div id="user-menu-content" ref="userMenu">
            <a onClick={this.logout.bind(this)}>Sign Out</a>
          </div>
        </div>
      </div>
    </div>
    <div id="bodyContainer">
      {children}
    </div>
    <div id="footer">
      <ul>
        <li><a href="mailto:[email protected]">Made by Us</a></li>
      </ul>
    </div>
  </div>
)
}
Alcina answered 18/9, 2016 at 17:18 Comment(2)
I love it when the answer is obvious, just wish I would've seen it! Thank you so much for the tip about destructuring, I can see how it will keep the markup cleaner and less dependent. Thanks again!Cobden
Glad I could help @DMX Thanks!Alcina
Y
6

The accepted answer is good however after awhile you will notice that you will have to repeat this many times on many components. Alternatively you can listen to make sure that the Accounts.loginServicesConfigured() is true before even rendering at the top level component with Tracker. Quoting the meteor docs: "The function Accounts.loginServicesConfigured() is a reactive data source that will return true once the login service is configured". I am using meteor, react, redux, and react router.

import React, { Component } from 'react';
import { Accounts } from 'meteor/accounts-base';
import { Route, Switch } from 'react-router-dom';
import { Tracker } from 'meteor/tracker';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = { loading: true };
  }

  componentWillMount() {
    Tracker.autorun(() => {
      if (Accounts.loginServicesConfigured()) {
        this.setState({ loading: false });
      }
    });
  }

  render() {
    return (
      <div className="app">
        {
          this.state.loading ? (<Spinner />) : (
            <Route exact path="/" component={HomePage} />
          )
        }
      </div>
    );
  }
}

Hopefully this alternative helps someone else out. Good luck!

Yolandayolande answered 4/10, 2017 at 5:23 Comment(2)
this answer is ideal if you're using redirects with react router but want to avoid loading the router when the user data hasn't loaded yet, thanks a ton!Leandra
awesome glad i can help!Yolandayolande
A
5

You could use a ternary operation

currentUser ? true : flase

Also a great tip to keep components organized is to always destructure for example instead of doing const currentUser = this.props.currentUser you can do const { currentUser } = this.props; Have a look:

render() {
const { currentUser, children } = this.props;
  return (
  <div id="base">
    <div id="header">
      <div id="top-bar" className="clear-fix">
        <div className="float-left" id="logo"></div>
        <div className="float-right" id="user-menu">
          <div onClick={this.toggleUserMenu.bind(this)}>
           {currentUser ?
             <span>{currentUser.emails[0].address}</span> :
             <span>Loading...</span>
           }
            <div className="arrow-down"></div>
          </div>
          <div id="user-menu-content" ref="userMenu">
            <a onClick={this.logout.bind(this)}>Sign Out</a>
          </div>
        </div>
      </div>
    </div>
    <div id="bodyContainer">
      {children}
    </div>
    <div id="footer">
      <ul>
        <li><a href="mailto:[email protected]">Made by Us</a></li>
      </ul>
    </div>
  </div>
)
}
Alcina answered 18/9, 2016 at 17:18 Comment(2)
I love it when the answer is obvious, just wish I would've seen it! Thank you so much for the tip about destructuring, I can see how it will keep the markup cleaner and less dependent. Thanks again!Cobden
Glad I could help @DMX Thanks!Alcina
L
2

Add dataLoading: !handle.ready() || !Meteor.user() to the createContainer.

export default BaseLayoutContainer = createContainer(() => {

  var handle = Meteor.subscribe("user.classrooms");

  return {
    currentUser: Meteor.user(),
    dataLoading: !handle.ready() || !Meteor.user(),
    classrooms: Classrooms.find({}).fetch(),
  };
}, BaseLayout);

Meteor.user() will be undefined inside the component before loading so make sure you prevent it's calling by adding a condition such as dataLoading ? "" : Meteor.user().

You can also check if user is logged in with !!Meteor.user(). Normally Meteor.userId() is faster to load but if you need to access user's data such as email you need to call Meteor.user() in that case no need to call both.

Lacewing answered 4/5, 2017 at 21:47 Comment(0)
G
1

Here are mine 2 cents

Beware if you're using Meteor.user in can be undefined during server restart or some other cases so you need to check it it's either object or null check meteor docs

So if I take Rimskys's answer it should look like this.

export default BaseLayoutContainer = createContainer(() => {

  var handle = Meteor.subscribe("user.classrooms");
  var user = Meteor.user()
  return {
    currentUser: Meteor.user(),
    dataLoading: !handle.ready() || !(Accounts.loginServicesConfigured() && (user || user === null)),
    classrooms: Classrooms.find({}).fetch(),
  };
}, BaseLayout);

Also, I think it's probably a good idea to redirect the user to login or forbidden page if user is null.

Graner answered 25/9, 2019 at 8:11 Comment(0)
P
0

This is my solution, hope it helps you.

const Loading = () => <div>LOADING</div>;

const PrivateRoute = ({ component: Component, user, ...props }) =>
  <Route
    {...props}
    render={props => {
      if (user) return <Component {...props} />;
      if (user === null)
        return <Redirect to={{ pathname: '/', state: { from: props.location }}} />

      return <Loading />
    }}
  />;

const AdminRoute = ({ component: Component, user, ...props }) => {
  const userData = user || {};
  const admin = userData.admin || false;

  return <Route
    {...props}
    render={props => {
      if (user && admin) return <Component {...props} />;
      if (user && !admin) return <Redirect to={{ pathname: '/', state: { from: props.location }}} />;
      return <Loading />
    }}
  />
}
Psychologist answered 26/7, 2021 at 14:30 Comment(0)
A
0

Was looking for an option that uses hooks, but found this. If anyone will need to solve this with hooks.

Inspired by the responses above I added "meteor/react-meteor-data" to the equation and it looks super sleek:

import React from 'react';
import { useTracker } from 'meteor/react-meteor-data';

export const App = () => {
  const accountsReady = useTracker(() => Accounts.loginServicesConfigured(), []); //make sure the account system loaded

  //TODO: Design a loader
  if(!accountsReady){
    return (<h2>Loading...</h2>)
  }

  //Once the loginService has loaded - do whatever
  return (<YourApp/>)

}
Aalst answered 29/7, 2022 at 22:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.