How to update a react component on a socket.io event - reactjs

Following advice from this question on forcing a component to update, I wrote the following code to update the component when my socket connection receives a broadcast.
constructor(props) {
super(props)
this.state = { key: 0 };
}
componentWillMount(){
const socket = io()
socket.on('data', function(data) {
// Do stuff with the data
this.setState({ key: Math.random() })
})
}
But I get the error
Uncaught TypeError: this.setState is not a function
I understand that this is because this is referring to something different when It's inside the socket.on() function but I don't know a way around it.
I also tried
constructor(props) {
super(props)
this.state = { key: 0 };
this.update = this.update.bind(this);
}
componentWillMount(){
const socket = io()
socket.on('data', function(data) {
// Do stuff with the data
update()
})
}
update(){
this.setState({ key: Math.random() })
}
But then I get the error
Uncaught ReferenceError: update is not defined
Does anyone have a better way to update my component or a fix for my current implementation?

You loose the context of this unless you do it as an arrow function:
socket.on('data', (data) => {
Also as you are using a class component you don't need that setState trick you can do this.forceUpdate().

Related

How to properly get rid of UNSAFE_componentWillMount

For a React app that I inherited from another developer, one of the pages includes:
import { getLogUser } from "../../appRedux/actions/authAction";
constructor(props) {
super(props);
this.state = {
user: null,
};
}
UNSAFE_componentWillMount() {
let user = getLogUser();
this.setState({ user });
// user state is used inside the render part
}
componentDidMount = () => {
let { username } = getLogUser();
// ... username is used inside some logic within the componentDidMount method.
I would like to get rid of the UNSAFE_componentWillMount method.
Can I remove the UNSAFE_componentWillMount part if I use user: getLogUser() inside the constructor?
If that is indeed the correct way to do it, shouldn't I then also
replace let { username } = getLogUser(); inside
componentDidMount with let { username } = this.state.user?
To start, let me explain what is UNSAFE_componentWillMount first
By defination
UNSAFE_componentWillMount() is invoked just before mounting occurs. It is called before render(), therefore calling setState() synchronously in this method will not trigger an extra rendering.
So it means UNSAFE_componentWillMount() will be called before render() (the component has not been on UI yet). This is totally opposite of componentDidMount() which is called after render()
To go deeper into why React's team wanted to make it UNSAFE as for a deprecated function, you can check this RFC.
Following up on your questions
Can I remove the UNSAFE_componentWillMount part if I use user: getLogUser() inside the constructor?
The benefit to having your function calls in the constructor is similar to UNSAFE_componentWillMount which makes sure your data available before rendering trigger.
So I'd say yes for your case, you can do it as long as it's not an asynchronous function (like async/await)
constructor(props) {
super(props);
this.state = {
user: await getLogUser(), //YOU CANNOT DO THIS WAY
};
}
This is the correct way
constructor(props) {
super(props);
this.state = {
user: getLogUser(), //no asynchronous call
};
}
So what if getLogUser() is asynchronous? componentDidMount comes in handy. It will be triggered after first rendering but you can wait for your data as much as you want and beyond that, it won't block your UI's interactions (or you can show a loading UI instead)
componentDidMount = async () => {
const user = await getLogUser()
setState({ user })
}
render() {
//show loading if `user` data is not populated yet
const { user } = this.state
if(!user) {
return <div>Loading</div>
}
}
If that is indeed the correct way to do it, shouldn't I then also replace let { username } = getLogUser(); inside componentDidMount with let { username } = this.state.user?
Yes, indeed. You can do it if you already populate user state in constructor, but you need to ensure your function will be executed in a small amount of time. If your function call takes too long, that will cause UI problems due to the blocked rendering.
//trigger before first rendering
constructor(props) {
super(props);
this.state = {
user: getLogUser(), //no asynchronous call
};
}
//trigger after first rendering
componentDidMount = () => {
const { username } = this.state.user;
}

this.setState does not update state

I'm trying to use this.setState within handleFormSubmit however this.setState isn't updating and I'm not sure why. If I run console.log(updatePosition) before this.setState I can that all the data is there. What am I missing? I use similar code for handleChange and I don't have problems.
constructor(props) {
super(props);
let uniqueId = moment().valueOf();
this.state = {
careerHistoryPositions: [{company: '', uniqueId: uniqueId, errors: {} }],
};
this.handleFormSubmit = this.handleFormSubmit.bind(this);
}
handleFormSubmit(event) {
event.preventDefault();
const { careerHistoryPositions } = this.state;
const updatePosition = this.state.careerHistoryPositions.map((careerHistoryPosition) => {
const errors = careerHistoryValidation(careerHistoryPosition);
return { ...careerHistoryPosition, errors: errors };
});
console.log(updatePosition)
this.setState({ careerHistoryPositions: updatePosition });
}
Keep in mind that the state isn't updated immediately. If you want to check if it's updated use callback function. Something as follows:
this.setState({ careerHistoryPositions: updatePosition }, () => console.log(this.state.careerHistoryPositions);
From the docs :
setState() does not immediately mutate this.state but creates a
pending state transition. Accessing this.state after calling this
method can potentially return the existing value. There is no
guarantee of synchronous operation of calls to setState and calls may
be batched for performance gains.
Hope this helps.
You should show how you are calling handleFormSubmit chances are that it's bound to a Dom event. So this is not the class/component, instead if you console.log(this); you'll see that it's the Dom element, the form element.
To make your code work as intended, in your component constructor() method, add this to rebind the handler function to the react component's class method, and you'll have access to this.state and this.setState()
this.handleFormSubmit = this.handleFormSubmit.bind(this);

Reset the state before render in reactjs

I am using react with Meteor and my data is coming from createContainer
export default createContainer((props) => {
const { search } = props.dataTables.favorites;
let data = [];
data = Meteor.user().favorites;
return { data };
}, StarredListView);
Now I wanted to do some processing on the data after state intialization
constructor(props) {
super(props);
this.state = {
data: props.data
};
this.resetOldData = this.resetOldData.bind(this);
}
Now how can I make sure that before render the data my resetOldData called and reset the state. Where can I call this this function to reset my state.
resetOldData() {
Meteor.call('reset.old.favorite.data', (err, res) => {
if (err) { this.props.renderAlert(err.reason, null, true); }
if (res) {
this.props.renderAlert(res.message);
this.setState({ data: res });
}
});
}
You need to take a look at React lifecycle documentation: https://facebook.github.io/react/docs/react-component.html
There are very specific places where you will want to respond to data coming in and how that should effect the state of your component.
Certain changes to the state only belong in very specific places in the lifecycle of a component.

Acces this.state variables in React

I have the following code in react :
getInitialState: function() {
var state= {
data: {}
};
fetch(this.props.dataurl)
.then(function(response) {
return response.json();
})
.then(function(result) {
this.setState({data:result});
}.bind(this));
return this.state;
},
componentDidMount: function() {
console.log(this);
console.log(this.state);
}
So in the getInitialState function I initialize the state variable data with results from fetch and then I want to access the data variable in the second function componentDidMount.
The problem I have is that this.state returns the data object empty but when I try to log this I'm getting the data variable with the data in it.
So why I'm having this behavior and how can I solve it?
componentDidMount does not guarantee that the async fetch has been completed.
You should define componentDidUpdate that will be called when the state has been changed, so that you can do anything with that new data.
componentDidUpdate(object prevProps, object prevState)
See React Lifecycle.

ReactNative: this.setState Error: null is not an object

React script
class TransactionsList extends Component {
constructor(props) {
super(props);
this.state = {
activeAccountId: "",
accessToken: "",
TransactionsData: "",
};
}
replaceRoute(route, passProps) {
this.props.replaceRoute(route, passProps);
}
async _getToken() {
try {
let accessToken = await AsyncStorage.getItem('AUTH_TOKEN');
if(!accessToken) {
this.replaceRoute('login');
} else {
this.setState({accessToken: accessToken})
}
} catch(error) {
Alert.alert('Print Errorr', error.message)
this.replaceRoute('login');
}
}
componentWillMount(){
this._getToken()
let token = 'Token '+this.state.accessToken
this.load_data(token)
}
render() {
return (
<Container>
// other code
</Container>
)
}
}
Got error in setState in getToken below is catch(error) block output
Print Error null is not an object(evaluating
prevComponentInstance._currentElement)
But same above code works in other screens.
It is not advisable to make api calls in componentWillMount because it is possible that the component will not have been mounted when the api call has finished and you call setState.
Instead, you should make api calls in componentDidMount. According to the documentation:
componentDidMount() is invoked immediately after a component is
mounted. Initialization that requires DOM nodes should go here. If you
need to load data from a remote endpoint, this is a good place to
instantiate the network request. Setting state in this method will
trigger a re-rendering.
And, you also need to bind _getToken as #Jazib mentioned.
You need to bind _getToken method using something like this:
this._getToken().bind(this)
Or you can do this in the constructor for better code (I prefer this one):
constructor(props) {
super(props);
this.state = {
activeAccountId: "",
accessToken: "",
TransactionsData: "",
};
this._getToken() = this._getToken().bind(this)
}
Hope this helps
I know I am replying a bit late but a better way is to use an arrow function instead of using bind on a named function. So you could write your _getToken method like this:
const _getToken = async () => {
// your logic here
}
The arrow function here implicitly assigns the current instance of the component to this keyword whereas in the named function you have to give the this keyword the context by using bind method as mentioned by others.
Also, componentWillMount is now deprecated and its better if you call your method in componentDidMount

Resources