Loading react with initial data/user-side settings page

Hmm, the issue I seem to be having is that:

Props are being passed by react-container. Therefore, when the component initially renders, the props are not yet ready (the subscription is not yet ready) which causes a typeerror:

Uncaught TypeError: Cannot read property 'emailFromName' of undefined(…)

Anyone figure out/have suggestions on how to get round this?

Your render function should be able to handle this: check if the data property is defined, else return something like an empty string.

First render, the empty string is displayed, and when the data comes back from the server, the component rerenders with the username

Data seems to be properly defined. E.g. simplifying my code to test it out:

If in my code I type in this, it works and renders fine:

export default Settings;

class Settings extends Component {
	constructor(props) {
		super(props);
		this.state = {
			email: '',
		};
	}
	render() {
		//loading prop checks to see if subscription is ready
		if ( this.props.loading ) {
			return <p>loading...</p>
		}
		
		return (
			<div>this.props.email</div>
		)
	}
}

It works perfectly fine.

If I instead use this, it does not work:

class Settings extends Component {
	constructor(props) {
		super(props);
		this.state = {
			email: this.props.email,
		};
	}
	render() {
		//loading prop checks to see if subscription is ready
		if ( this.props.loading ) {
			return <p>loading...</p>
		}
		
		return (
			<div>this.state.email</div>
		)
	}
}

I use the following format for pre-populated inputs:

<input placeholder="some placeholder text" value={this.props.text || ""} onChange={this.handleTextChange.bind(this)}></input>

where handleTextChange(event) sends a change to the top-level state and the text value is passed back down as props. Seems to work pretty well for me.

Thanks @globalise

Issue is, though, that in your example, the default value is the same as the value being changed/updated by your onChange handler (i.e. this.props.text) . However, I am trying to load an initial value from Mongo, while the onChange handler is updating the state in the component (or a top level component, same principle applies).

Or to put it another way via the use of a specific example. I want to allow a user to update their email address from their end. When they go to their settings page, they need to see their current email address (via the default value in the input). When they edit it, the onChange handler needs to store the new value until the save button is clicked. (Unless we wire up the onChange handler to update Mongo directly, but that seems like it will be a lot of unnecessary Mongo queries for each character).

Does that make sense?

Instead of updating with onChange, could you use an ‘onBlur’ function which only updates Mongo when the user leaves the input (i.e. once for the whole change rather than for each character)? That at least has the benefit of not storing your props as state. It also eliminates the need for a save button.

Great idea mate - might do this actually!

But workarounds aside (even if in this case it is better than what I was trying), I would like to learn the flow on how to go about doing this. Never know what other scenario this may come up in.

We have an answer!! Thanks to @callum and @emminnn from the TMC Slack group for it.


You can set state in the lifecycle methods - either componentWillReceiveProps or componentDidUpdate. E.g:

componentWillReceiveProps(nextProps) {
			this.setState({
				email: nextProps.currentUser.emails[0].address
			})
	}

Thanks for all the help guys!

1 Like

Is there any specific reason you need to have the data available in both props and state? Setting state through the use of props is considered an anti-pattern, and in this case redundant as far as I can tell.

Re: Antipattern - I thought so too. See here: Loading react with initial data/user-side settings page

Specific reason - to set initial form value state for users to update their e.g. email address saved in Mongo

Hello, from where you are passing data to this form. Sorry I am asking this question as I am new to Meteor-React. Can you share your code. I’m new to React so I wasn’t aware of details. I know how to pass data when having array i.e using map function. But when there are only one document(findOne) i.e when we receive only one object in this case how to pre-populate it in form.Thanks in advance!

Hey mate. I’ve already shared my code above.

If you want to share your code, we can have a look and find a solution.

Note - Your subscriptions should be returning a cursor - I was under the impression that findOne will not work for that reason - though you can of course use findOne after subscribing.

Please have a look at my code. And tell me how to pass the value of object to input

This is my subscription:

if (Meteor.isServer) {
  Meteor.publish('userfunction', function(){
      return Meteor.users.find({});
  });
}

Parent component ‘App’ :

import React, { Component} from 'react';
import { render } from 'react-dom';
import TrackerReact from 'meteor/ultimatejs:tracker-react';
import ProfileCandidateForm from './ProfileCandidateForm.jsx';

export default class App extends TrackerReact(Component) {
	constructor(){
		super();
		this.state = {
			subscription : {
				"usersData" : Meteor.subscribe('userfunction')
			}
		}
	}

  'editProfile'(event){
    event.preventDefault();
    var doc = {
	            username   : this.refs.username.value,
             }

    console.log(doc);
  }

	userData(){
		var userId  = Meteor.userId();
		return Meteor.users.find({'_id': userId}).fetch();
	}

	render(){
	   console.log(this.userData());	
       return(
			  <form id="edit" onSubmit={this.editProfile.bind(this)}>

					{ this.userData().map( (usersData)=>{
						return <ProfileCandidateForm key={usersData._id} user={usersData}/>
					  }) 
					}
					 
				   <div className="form-group col-lg-12 col-md-4 col-xs-12 col-sm-12 noPadLR">
					    <input type="submit" value="Update" className="btn btn-primary col-lg-4 col-lg-offset-4"/>
				    </div>
 
			  </form>	    
	   );

	} 

}

Child component ‘ProfileCandidateForm’ :

import React, { Component} from 'react';
import { Meteor } from 'meteor/meteor';

export default class ProfileCandidateForm extends Component {

  render() {
    return (
		    <div className="form-group col-lg-5 col-md-4 col-xs-12 col-sm-12 ">
			   <label className="signupLabel control-label">User Name*</label>
			   <input type="text" className="form-control" ref="username" defaultValue={this.props.user.username} />
		    </div>
    );
  }
}

Any help is appreciated!

Hmmm…

I don’t recognise that pattern of loading data. Generally, data should be loaded using createContainer or Komposer, which will allow data to be fed into your component as a prop, at which point you can render it on your UI or wire it up to a method.

Also, I believe it is best (probably necessary) to have your constructor at the top (i.e. - before you define your userData method)

Ok, constructor seems to be at the top now :S

Anyway, don’t have much experience with tracker, but, if it is similar to createContainer, then you will need to wait for the subscription to be ready before data is displayed.

Something along the lines of my code above: Loading react with initial data/user-side settings page

OK. Let me try this. Thank you.

Added ‘react-meteor-data’ using command ‘meteor add react-meteor-data’. And then added line’ import { createContainer } from ‘meteor/react-meteor-data’; ’ at the top of the file.

Then also at console I am getting error => modules-runtime.js?hash=8587d18…:231 Uncaught Error: Cannot find module ‘meteor/react-meteor-data’.

Hello,
Your post helped me a lot. Finally have a code, Just wants to make sure is it correct? Thank you so much. Now I can work on edit part.

UPDATE: FindOne will not be available initially so we need to handle it. Its working for me now.

import React, {Component} from 'react';
import {createContainer} from 'meteor/react-meteor-data';
import {PropTypes} from 'prop-types';
import {Meteor} from 'meteor/meteor';

//db
import {ParkingSlots} from '/imports/addressVerification/api/verificationAddress.js';

class Print extends Component {

    constructor(props) {
        super(props);
        this.state = {
            data : this.props.post,
        };
    }

    componentWillReceiveProps(nextProps) {
        console.log(nextProps.post);
            this.setState({
                id      : nextProps.post._id,
                owner   : nextProps.post.owner,
                street1 : nextProps.post.street1,
            })
    }

    render(){
        console.log(this.props);
        console.log(this.props.loading);

        if(!this.props.loading){
        return(
            <div>

                    {/* It will need 'componentWillReceiveProps'
                   {this.state.id} &nbsp;
                   {this.state.owner} &nbsp;
                   {this.state.street1} &nbsp;
                   */}

                   {/* This will take data  directly from props*/}
                   {this.props.post._id}
                   {this.props.post.owner}
                   {this.props.post.state}
                   {this.props.post.city}
                   {this.props.post.zip}

            </div>
        );
        }else{
            return (<h1>loading...</h1>);
        }
    }
}
PostContainer = createContainer((props)=>{
    const postHandle = Meteor.subscribe('parkingSlots');
    const post = ParkingSlots.findOne({'owner':Meteor.userId()});
    const loading = !postHandle.ready();

    return {
        loading,
        post,
    };
}, Print);

export default PostContainer;
import React, { Component} from 'react';
import { render } from 'react-dom';
import TrackerReact from 'meteor/ultimatejs:tracker-react';
import ProfileCandidateForm from './ProfileCandidateForm.jsx';

export default class App extends TrackerReact(Component) {
	constructor(props){
		super(props);
		this.state = {
			subscription : {}
		};
                this.editProfile = this.editProfile.bind(this)
	}
  componentWillReceiveProps(nextProps) {
                        const subscription = 
			this.setState({
				subscription
			})
	}
  editProfile(event){
    event.preventDefault();
    var doc = {
	            username   : this.refs.username.value,
             }

    console.log(doc);
  }

	userData(){
		var userId  = Meteor.userId();
		return Meteor.users.find({'_id': userId}).fetch();
	}

	render(){
	   console.log(this.userData());	
       return(
			  <form id="edit" onSubmit={this.editProfile.bind(this)}>

					{ this.userData().map( (usersData)=>{
						return <ProfileCandidateForm key={usersData._id} user={usersData}/>
					  }) 
					}
					 
				   <div className="form-group col-lg-12 col-md-4 col-xs-12 col-sm-12 noPadLR">
					    <input type="submit" value="Update" className="btn btn-primary col-lg-4 col-lg-offset-4"/>
				    </div>
 
			  </form>	    
	   );

	} 

}

Looks good (nothing out of place from what I can see!)

Thank you.
Getting console as -

Warning: EditVerifiedAddr is changing an uncontrolled input of type text to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://fb.me/react-controlled-components

Ah right.

Yup your input’s value needs to be controlled (so, set by state) - then have a setState function fire on the onChange

So your input should have:

value={this.state.inputValue}
onChange{this.state.onInput}

With the onInput function bound to ‘this’ in your constructor, and:

onInput(event){
  this.setState({inputValue: event.target.value})
}