ReactJS event-driven updates on parent component - javascript

I have some components that are buried deep inside their parents. I have a user variable, which is fetched at the absolute top of this chain (a ProfilePage component), and passed all the way down with props. But, sometimes the user updates its profile in a small component on the page (the AboutMe component for example). My current flow is to have the AboutMe component update the user via ajax, and then trigger an event called 'profileupdate' on my pubsub. When 'profileupdate' is triggered, the ProfilePage fetches the user again via ajax, and so all the props are updated. This tactic appears to work, but it almost always gives:
Uncaught Error: Invariant Violation: replaceState(...): Can only update a mounted or mounting component.
I know this error is coming from the ProfilePage not being mounted and replacing its user state.
Here is an example of a place where I do this:
In the ProfilePage component:
var ProfilePage = React.createClass({
getInitialState: function() {
return {};
},
updateUser: function() {
this.setUser('/api/users/' + this.props.params.userId);
},
setCurrentUser: function(currentUser) {
this.setState({ currentUser: currentUser });
},
resetCurrentUser: function() {
auth.getCurrentUser.call(this, this.setCurrentUser);
},
componentWillReceiveProps: function(newProps) {
this.setUser('/api/users/' + newProps.params.userId);
},
componentDidMount: function() {
PubSub.subscribe('profileupdate', this.updateUser);
PubSub.subscribe('profileupdate', this.resetCurrentUser);
this.resetCurrentUser();
this.updateUser();
},
setUser: function(url) {
$.ajax({
url: url,
success: function(user) {
if (this.isMounted()) {
this.setState({ user: user });
} else {
console.log("ProfilePage not mounted.");
}
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.userId, status, err.toString());
}.bind(this)
});
},
Then, in the AboutInput component:
ProfilePage --> ProfileBox --> About --> AboutInput
updateAbout: function(text) {
var url = '/api/users/' + this.props.user._id;
var user = this.props.user;
user.about = text;
$.ajax({
url: url,
type: 'PUT',
data: user,
success: function(user) {
auth.storeCurrentUser(user, function(user) {
return user;
});
PubSub.publish('profileupdate');
this.propagateReset();
}.bind(this),
error: function(xhr, status, err) {
console.error(status, err.toString());
this.propagateReset();
}.bind(this)
});
},
The takeaways here are that the ProfilePage fetches the user again when the profileupdate event is triggered.
Am I using the wrong tactics here? How can I handle this kind of update? And, can I force the ProfilePage to become mounted? That would be really ideal. Also, interestingly, whether or not the console logs that the ProfilePage is unmounted, the user still updates.

I really would take a look at Facebook's FLUX implementation. It will simplify your workflow tremendously. I am guessing that is not an option at this moment, but make sure your components are unsubscribing when removing them. For eg:
componentWillUnmount: function () {
var self = this;
self.store.removeListener('change', self._setState);
self._setState = null;
}
Or in your case unsubscribe using the method above from your PubSub implementation when a component is being replaced. So 'profileupdate' is being called on a React component that has been unmounted. Something like.
componentWillUnmount: function () {
PubSub.unsubscribe('profileupdate', this.updateUser);
PubSub.unsubscribe('profileupdate', this.resetCurrentUser);
this.updateUser = null;
this.resetCurrentUser = null;
}
It is exactly what the error states. You trying to set state on a React component that is not longer mounted, or has been removed.

Related

React component get request being made in one or clicks late

This one is kind of hard to explain, but basically when a click on a component, I make a get request for some data for another component. This however is only made after a couple of clicks.
I should probably also admit that I am not 100% sure if the place I am making the request is even correct, so if that's the case please let me know how I can get that fixed. Here's the code:
var ChangeLogData = React.createClass({
getInitialState: function () {
return {
content: {},
}
},
render: function () {
var _this=this;
$.get(this.props.source, function (data) {
var log = $.parseJSON(data);
_this.state.content = log;
}.bind(this));
return (
<div>
{_this.state.content[0]}
</div>
);
}
});
window.ChangeLog = React.createClass({
render: function () {
return (
<div>
<ChangeLogData name={this.props.params.name}
source={currentUrl + "/changelog/" +
this.props.params.name}/>
</div>
);
}
});
Edit: I should also probably add that it seems that most people recommend doing http requests on componentWillMount, but if I do that, the request only works once.
Edit 2: Here is the code of where the event is being called:
var AboutItem = React.createClass({
render: function () {
return (
<ListGroup>
{this.props.list.map(function (listValue,key) {
var link = currentUrl + "/changelog/" + listValue.split(' ')[0];
return <ListGroupItem key={key} className="module"
bsStyle="warning">
{listValue}
</ListGroupItem>
})}
</ListGroup>
);
}
});
I guess the idea is, the user will click on an item (that is dynamically generated), and when the item is clicked, it will send to the ChangeLog component the data in which it has to do the get request. So where exactly would I put my event handler?
I think the problem is that it's not being called correctly, as jquery is async...
var jqxhr = $.get( "example.php", function() {
alert( "success" );
})
.done(function() {
// PUT YOUR CALLBACK CODE HERE WITH THE RESULTS
})
.fail(function() {
alert( "error" );
})
.always(function() {
alert( "finished" );
});
And update the state in the .done()
You should not be making requests in your render method. You should also not be directly modifying state through this.state but instead use this.setState(). You also don't seem to be adding any onClick handlers.
You can do something like the following to trigger requests onClick:
var ChangeLogData = React.createClass({
getInitialState: function () {
return {
content: {},
}
},
handleClick: function() {
$.get(this.props.source, function (data) {
var log = $.parseJSON(data);
this.setState( { content: log } );
}.bind(this));
}
render: function () {
return (
<div onClick = { this._handleClick } >
{_this.state.content[0]}
</div>
);
}
If you want to do it on component mount you can put into componentDidMount() and call setState when you retrieve your data. This will cause the component to re-render with content
First: the get request is async, so by the time you get the response back the DOM is already rendered.
Second: Never update state inside the render method, if it works and you don't get an error message you most likely will create an infinite loop, render => updateState => render => udpateState...
you have multiple options, you can have the get request inside the function called after onClick (not shown in your code), and then update state and pass data as props. In this case you would be making a new get request every single time there's a click event. If you dont need a new get request on every click look into react lifecycle methods, in particular componentDidMount, which is basically executed after the react component is mounted, then you can do a get request there and update the state
componentDidMount: function() {
$.get(this.props.source, function (data) {
var log = $.parseJSON(data);
this.setState( { content: log } );
}.bind(this));
},
I can't see from your code what component should be clicked in order to trigger the request, but as far as could see, you should take the request out of the render() method. This method is called every time state/props change, so it might make your code make the request multiple times.
Another thing is that you should always mutate your state by calling this.setState() method, so in your case it would be _this.setState({ content: log }).
Now, if you change the name prop every time another component is clicked, you should do something like:
var ChangeLogData = React.createClass({
getInitialState: function () {
return {
content: {},
}
},
componentWillMount: function () {
this.getLog(this.props.source);
},
componentWillReceiveProps: function (nextProps) {
this.getLog(nextProps.source);
},
getLog: function (source) {
var _this = this;
$.get(source, function (data) {
var log = $.parseJSON(data);
_this.setState({
content: log
});
}.bind(this));
}
render: function () {
return (
<div>
{this.state.content[0]}
</div>
);
}
First you can extract the process to create the request call from the render() method into its own method, in this case, getLog(). Then, with two new methods from the React lifecycle, you can call getLog() when the component will be mounted and whenever new props come in from the parents components.

React: initial render fires before state is set (via ajax)

I've set things up so the HomePage component renders UserShow for the current logged in user. For example, if a user with an ID of 2 is logged in and visits the HomePage page, it will render their UserShow.
The "normal" UserShow works correctly. For example if you type in /users/18, it will properly render. However it's not working when HomePage renders it.
I'm new to React (especially its lifecycle methods), so my debugging has been to throw alerts in at various steps. I'd say the most important findings to share are:
currentUserID( ) is functioning and returns the correct ID
Hard-coding the value of state.userID within componentDidMount causes things to work correctly
These two points lead me to believe that Render is being called before it can update state.userID with its (correct) return value. Even more specific is that it's rendering before the .success portion of the this.currentUserID() ajax call returns. If this is so, what's the best way to go about not doing an initial render until an ajax call like this completes?
My code is in a state of spaghetti - it's my first time doing front-end routing with JavaScript. I'm also managing sessions via using the user's email as the token in localStorage - I'm new to sessions in JS as well. Please bear with me.
HomePage component:
var HomePage = React.createClass({
getInitialState: function(){
return{
didFetchData: false,
userID: null,
}
},
componentWillMount: function(){
newState = this.currentUserID()
this.setState({userID: newState})
// this.setState({userID: 2}) //hard-coding the value works
},
currentUserID: function(){
if(App.checkLoggedIn()){
var email = this.currentUserEmail()
this.fetchUserID(email)
}else{
alert('theres not a logged in user')
}
},
currentUserEmail: function(){
return localStorage.getItem('email')
},
fetchUserID: function(email){ //queries a Rails DB using the user's email to return their ID
$.ajax({
type: "GET",
url: "/users/email",
data: {email: email},
dataType: 'json',
success: function(data){
this.setState({didFetchData: 'true', userID: data.user_id})
}.bind(this),
error: function(data){
alert('error! couldnt fetch user id')
}
})
},
render: function(){
userID = this.state.userID
return(
<div>
<UserShow params={{id: userID}} />
</div>
)
}
})
UserShow component:
var UserShow = React.createClass({
getInitialState: function(){
return{
didFetchData: false,
userName: [],
userItems: [],
headerImage: "../users.png"
}
},
componentDidMount: function(){
this.fetchData()
},
fetchData: function(){
var params = this.props.params.id
$.ajax({
type: "GET",
url: "/users/" + params,
data: "data",
dataType: 'json',
success: function(data){
this.setState({didFetchData: 'true', userName: data.user_name, userItems: data.items, headerImage: data.photo_url})
}.bind(this),
error: function(data){
alert('error! couldnt load user into user show')
}
})
},
render: function(){
var userItem = this.state.userItems.map(function(item){
return <UserItemCard name={item.name} key={item.id} id={item.id} description={item.description} photo_url={item.photo_url} />
})
return(
<div>
<Header img_src={this.state.headerImage} />
<section className="body-wrapper">
{userItem}
</section>
</div>
)
}
})
So what you want to do is to avoid rendering anything until your ajax-request returns your result.
You can do a check in the render method if the state is how you want it. If it's not, then return null, or a loader or some other markup. When the componentDidMount then sets the state, it will trigger a re-render, since the userID then is set, it will return the userShow component
Example:
render(){
if(this.state.userID === null){
return null; //Or some other replacement component or markup
}
return (
<div>
<UserShow params={{id: userID}} />
</div>
);
}
Fetching the data in the userShow component could be done like this:
componentWillReceiveProps(nextProps){
//fetch data her, you'll find your prop params in nextProps.params
}
You can also avoid doing this here, by kicking the data-fetch in the render-method.
Your initial data set what your doing in componentWillMount will not help to set data because you want to fetch data from ajax. Debug fetchUserID and
currentUserID function whether your getting correct email from localstorage and user id from server. Others is fine.

React/reflux how to do proper async calls

I recently started to learn ReactJS, but I'm getting confused for async calls.
Lets say I have a Login page with user/pass fields and login button. Component looks like:
var Login = React.createClass({
getInitialState: function() {
return {
isLoggedIn: AuthStore.isLoggedIn()
};
},
onLoginChange: function(loginState) {
this.setState({
isLoggedIn: loginState
});
},
componentWillMount: function() {
this.subscribe = AuthStore.listen(this.onLoginChange);
},
componentWillUnmount: function() {
this.subscribe();
},
login: function(event) {
event.preventDefault();
var username = React.findDOMNode(this.refs.email).value;
var password = React.findDOMNode(this.refs.password).value;
AuthService.login(username, password).error(function(error) {
console.log(error);
});
},
render: function() {
return (
<form role="form">
<input type="text" ref="email" className="form-control" id="username" placeholder="Username" />
<input type="password" className="form-control" id="password" ref="password" placeholder="Password" />
<button type="submit" className="btn btn-default" onClick={this.login}>Submit</button>
</form>
);
}
});
AuthService looks like:
module.exports = {
login: function(email, password) {
return JQuery.post('/api/auth/local/', {
email: email,
password: password
}).success(this.sync.bind(this));
},
sync: function(obj) {
this.syncUser(obj.token);
},
syncUser: function(jwt) {
return JQuery.ajax({
url: '/api/users/me',
type: "GET",
headers: {
Authorization: 'Bearer ' + jwt
},
dataType: "json"
}).success(function(data) {
AuthActions.syncUserData(data, jwt);
});
}
};
Actions:
var AuthActions = Reflux.createActions([
'loginSuccess',
'logoutSuccess',
'syncUserData'
]);
module.exports = AuthActions;
And store:
var AuthStore = Reflux.createStore({
listenables: [AuthActions],
init: function() {
this.user = null;
this.jwt = null;
},
onSyncUserData: function(user, jwt) {
console.log(user, jwt);
this.user = user;
this.jwt = jwt;
localStorage.setItem(TOKEN_KEY, jwt);
this.trigger(user);
},
isLoggedIn: function() {
return !!this.user;
},
getUser: function() {
return this.user;
},
getToken: function() {
return this.jwt;
}
});
So when I click the login button the flow is the following:
Component -> AuthService -> AuthActions -> AuthStore
I'm directly calling AuthService with AuthService.login.
My question is I'm I doing it right?
Should I use action preEmit and do:
var ProductAPI = require('./ProductAPI')
var ProductActions = Reflux.createActions({
'load',
'loadComplete',
'loadError'
})
ProductActions.load.preEmit = function () {
ProductAPI.load()
.then(ProductActions.loadComplete)
.catch(ProductActions.loadError)
}
The problem is the preEmit is that it makes the callback to component more complex. I would like to learn the right way and find where to place the backend calls with ReactJS/Reflux stack.
I am using Reflux as well and I use a different approach for async calls.
In vanilla Flux, the async calls are put in the actions.
But in Reflux, the async code works best in stores (at least in my humble opinion):
So, in your case in particular, I would create an Action called 'login' which will be triggered by the component and handled by a store which will start the login process. Once the handshaking ends, the store will set a new state in the component that lets it know the user is logged in. In the meantime (while this.state.currentUser == null, for example) the component may display a loading indicator.
For Reflux you should really take a look at https://github.com/spoike/refluxjs#asynchronous-actions.
The short version of what is described over there is:
Do not use the PreEmit hook
Do use asynchronous actions
var MyActions = Reflux.createActions({
"doThis" : { asyncResult: true },
"doThat" : { asyncResult: true }
});
This will not only create the 'makeRequest' action, but also the 'doThis.completed', 'doThat.completed', 'doThis.failed' and 'doThat.failed' actions.
(Optionally, but preferred) use promises to call the actions
MyActions.doThis.triggerPromise(myParam)
.then(function() {
// do something
...
// call the 'completed' child
MyActions.doThis.completed()
}.bind(this))
.catch(function(error) {
// call failed action child
MyActions.doThis.failed(error);
});
We recently rewrote all our actions and 'preEmit' hooks to this pattern and do like the results and resulting code.
I also found async with reflux kinda confusing. With raw flux from facebook, i would do something like this:
var ItemActions = {
createItem: function (data) {
$.post("/projects/" + data.project_id + "/items.json", { item: { title: data.title, project_id: data.project_id } }).done(function (itemResData) {
AppDispatcher.handleViewAction({
actionType: ItemConstants.ITEM_CREATE,
item: itemResData
});
}).fail(function (jqXHR) {
AppDispatcher.handleViewAction({
actionType: ItemConstants.ITEM_CREATE_FAIL,
errors: jqXHR.responseJSON.errors
});
});
}
};
So the action does the ajax request, and invokes the dispatcher when done. I wasn't big on the preEmit pattern either, so i would just use the handler on the store instead:
var Actions = Reflux.createActions([
"fetchData"
]);
var Store = Reflux.createStore({
listenables: [Actions],
init() {
this.listenTo(Actions.fetchData, this.fetchData);
},
fetchData() {
$.get("http://api.com/thedata.json")
.done((data) => {
// do stuff
});
}
});
I'm not big on doing it from the store, but given how reflux abstracts the actions away, and will consistently fire the listenTo callback, i'm fine with it. A bit easier to reason how i also set call back data into the store. Still keeps it unidirectional.

Halt React from rendering/clearing state of a view until animations/callback is complete?

So i've got a react app using react router. The router hooks up to a nav that updates the same component view for 4 different routes - 2012, 2013, 2014, 2015. Each route triggers an API call that pulls in some JSON, and the component is re-rendered with the new state.data.
What i'd like to do is be able to fade out the view, then call the API, then fade in the new view with the new state.data.
The issue i have at the moment, is the view is rendering blank before it has time to fade out, then the new data is pulled in, and the fade in transition is triggered once the data is loaded.
Are there any hooks i can tap into that'll stop the this.state.data clearing before view has faded out?
/** #jsx React.DOM */
var React = require('react');
var Router = require('react-router');
var Reqwest = require('reqwest');
var StreamItem = require('./StreamItem.jsx');
var Stream = React.createClass({
mixins: [ Router.State ],
getInitialState: function() {
return {
data: null
}
},
readFromAPI: function(url, successFunction) {
Reqwest({
url: url,
type: 'json',
method: 'get',
contentType: 'application/json',
success: successFunction,
error: function(error) {
console.error(url, error['response']);
location = '/';
}
});
console.log('read api called');
},
componentWillMount: function () {
this.readTweetsFromAPI();
},
readTweetsFromAPI: function() {
var self = this;
this.readFromAPI(this.getPath(), function(tweets) {
setTimeout(function() {
self.setState({
data: tweets
});
// state of page transitions is kept a level up to include
// other components. This function toggles a boolean to true,
// which toggles a css class transition on the parent
// component.
self.props.fadeInPage();
}, 500);
}.bind(this));
},
render: function() {
if (this.state.data) {
var tweetItems = this.state.data.map(function(tweet, index) {
return <StreamItem key={tweet.id} tweet={tweet}/>
}.bind(this));
}
return (
<div className="tweet-list__archive">
{tweetItems}
</div>
);
}
});
module.exports = Stream;

react.js - Deep Object in state with async data does not work

I've just figured out that object in React's state that have multiple children cannot be rendered easily.
In my example I have component which speaks with third-party API through AJAX:
var Component = React.createClass({
getInitialState: function () {
return {data: {}};
},
loadTrackData: function () {
api.getDataById(1566285, function (data) {
this.setState({data: data});
}.bind(this));
},
componentDidMount: function () {
this.loadTrackData();
},
render: function () {
return (
<div>
<h2>{this.state.data.metadata.title}</h2>
</div>
);
}
});
The problem is that {this.state.data.metadata} renders fine..
But {this.state.data.metadata.title} throws error Uncaught TypeError: Cannot read property 'title' of undefined!
What is the proper way to deal with such async data?
I always like to add the loading spinner or indicator if the page has async operation. I would do this
var Component = React.createClass({
getInitialState: function () {
return {data: null};
},
loadTrackData: function () {
api.getDataById(1566285, function (data) {
this.setState({data: data});
}.bind(this));
},
componentDidMount: function () {
this.loadTrackData();
},
render: function () {
var content = this.state.data ? <h2>{this.state.data.metadata.title}</h2> : <LoadingIndicator />;
return (
<div>
{content}
</div>
);
}
});
with the loading indicator basically it improve the user experience and won't get much of unwanted surprise. u can create your own loading indicator component with lots of choices here http://loading.io/
this.state.data.metadata is undefined until loading occurs. Accessing any property on undefined gives you a TypeError. This is not specific to React—it's just how JavaScript object references work.
I suggest you use { data: null } in initial state and return something else from render with a condition like if (!this.state.data).

Categories

Resources