Why this setState call does not trigger rendering? - reactjs

I have made a small treeview component, which gets a json file with the structure and should render the treeview.
The json is correct, the promise on the constructor is resolved correctly, but the render is not called after the setState inside the constructor.
I´ve tried to read similar questions, but no success.
export default class Tree extends Component<ITreeProps, ITreeState> {
public constructor(props: ITreeProps, state: ITreeState) {
super(props, state);
Tree.GetJsonStructure(props.Path).then(response => {
this.setState({
data : JSON.parse(response)
});
console.log(response);
});
}
public static GetJsonStructure(jsonPath: string): Promise<string>{
return new Promise((resolve, reject) => {
axios.get(jsonPath, { responseType: 'arraybuffer' }).then(response => {
if (response.status !== 200) {
// handle error
reject("FAIL!");
}
var buf = new Buffer(response.data);
//var buf = Buffer.concat(response.data);
var paki = pako.inflate(buf);
var decoder = new encoding.TextDecoder();
var stringo = decoder.decode(paki);
resolve(stringo);
});
});
}
public render(): React.ReactElement<ITreeProps> {
console.log("RENDERINGGGG");
console.log(this.state)
if(!this.state || !this.state.data || this.state.data.length == 0 ){
return <div></div>;
}
const data = this.state.data;
if (!data || data.length == 0)
console.log("No properties set for the application");
return (
<div className={styles.ToolboxLinkPanel}>
{
data.map(node => (
<TreeNode key={node.key} label={node.label} children={node.nodes} isOpen={true} ></TreeNode>
))
}
</div>
);
}
}

You need to call your api method inside componentDidMount and not in the constructor:
componentDidMount() {
Tree.GetJsonStructure(props.Path).then(response => {
this.setState({
data: JSON.parse(response)
});
console.log(response);
});
}
As to why it's the place to make api calls, the official documentation elaborates (emphasis added):
componentDidMount() is invoked immediately after a component is
mounted (inserted into the tree). 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 [...] It will trigger an extra rendering, but it will happen before the browser updates the screen..

Related

setState not returned from render when using Axios

I'm using axios to get data from an endpoint. I'm trying to store this data inside the state of my React component, but I keep getting this error:
Error: Results(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
I've struggled with many approaches: arrow functions etc., but without luck.
export default class Map extends Component {
constructor() {
super();
this.state = {
fillColor: {},
selectedCounty: "",
dbResponse: null,
};
}
getCounty(e) {
axios.get("/getWeatherData?county=" + e.target.id)
.then((response) => {
this.setState(prevState => {
let fillColor = {...prevState.fillColor};
fillColor[prevState.selectedCounty] = '#81AC8B';
fillColor[e.target.id] = '#425957';
const selectedCounty = e.target.id;
const dbResponse = response.data;
return { dbResponse, selectedCounty, fillColor };
})
}).catch((error) => {
console.log('Could not connect to the backend');
console.log(error)
});
}
render() {
return (
<div id="map">
<svg>big svg file</svg>
{this.state.selectedCounty ? <Results/> : null}
</div>
)
}
I need to set the state using prevState in order to update the fillColor dictionary.
Should this be expected? Is there a workaround?

React Chatbox, how to get the string displayed?

I am a newbie, and am trying to build a simple restaurant recommendation web app using AWS and React. So, I am using this chat window(https://www.npmjs.com/package/react-chat-window). Basically, when the user types something, the chatbot gets triggered and asks questions like "what kind of food do you want?" So far, I am able to pass the user's input and get the response back from the AWS. I can log the response to the console and verify it. But I have trouble getting the response displayed in the chatbox.
Here is the snippet of the code
class chatBox extends Component {
constructor() {
super();
this.state = {
messageList: chatHistory,
newMessagesCount: 0,
isOpen: false
};
}
// message is the user's input
_onMessageWasSent(message) {
var body = {
messages: message.data['text']
}
// every time the user types something, this function passes the user's input to AWS
apigClient.invokeApi(pathParams, pathTemplate, method, additionalParams, body)
.then(function (result) { // result contains the response to the user's input
var text = result.data.body
console.log(text) // logs the response to the user's input
console.log(text.length)
}).catch(function (result) {
});
this.setState({ //this displays what the user types
messageList: [...this.state.messageList, message]
})
}
// This is a function that displays the input of the other side
// I can manually test it and see that whatever I pass to this function gets displayed as
// the other person's speech, not the user.
_sendMessage(text) {
console.log("sendMessage")
if (text.length > 0) {
this.setState({
messageList: [...this.state.messageList, {
author: 'them',
type: 'text',
data: { text }
}],
newMessagesCount: this.state.newMessagesCount + 1
})
}
}
As can be seen, I am logging the response to the console. Now, I want to get the response displayed so I tried inside the constructor
this._onMessageWasSent = this._sendMessage.bind(this)
and calling the function inside _onMessageSent
apigClient.invokeApi(pathParams, pathTemplate, method, additionalParams, body)
.then(function (result) { // result contains the response to the user's input
var text = result.data.body
console.log(text) // logs the response to the user's input
console.log(text.length)
this._sendMessage(text) // Calling the function
}).catch(function (result) {
});
this.setState({ //this displays what the user types
messageList: [...this.state.messageList, message]
})
}
I can see that the _sendMessage function gets triggered, because I have a console.log. But now the chatbox displays neither the user and the chatbot. If I don't bind this._onMessageWasSent = this._sendMessage.bind(this), at least I get the user displayed.
What could be the problem??
This is my render()
render() {
return (<div>
<Launcher
agentProfile={{
teamName: 'Foodophile',
imageUrl: 'https://a.slack-edge.com/66f9/img/avatars-teams/ava_0001-34.png'
}}
onMessageWasSent={this._onMessageWasSent.bind(this)}
messageList={this.state.messageList}
onFilesSelected={this._onFilesSelected.bind(this)}
newMessagesCount={this.state.newMessagesCount}
handleClick={this._handleClick.bind(this)}
isOpen={this.state.isOpen}
showEmoji
/>
</div>)
}
UPDATE
class chatBox extends Component {
constructor(props) {
super(props);
this.state = {
messageList: chatHistory,
newMessagesCount: 0,
isOpen: false
};
this._onMessageWasSent = this._onMessageWasSent.bind(this);
this._onFilesSelected = this._onFilesSelected.bind(this);
this._handleClick = this._handleClick.bind(this);
this._sendMessage = this._sendMessage.bind(this);
}
_onMessageWasSent(message) {
var body = {
messages: message.data['text']
}
apigClient.invokeApi(pathParams, pathTemplate, method, additionalParams, body)
.then(function (result) {
var text = result.data.body
console.log(text)
console.log(text.length)
this._sendMessage(text)
}).catch(function (result) {
});
this.setState({
messageList: [...this.state.messageList, message]
})
}
_sendMessage(text) {
console.log("sendMessage")
if (text.length > 0) {
this.setState({
messageList: [...this.state.messageList, {
author: 'them',
type: 'text',
data: { text }
}],
newMessagesCount: this.state.newMessagesCount + 1
})
}
}
render() {
return (<div>
<Launcher
agentProfile={{
teamName: 'Foodophile',
imageUrl: 'https://a.slack-edge.com/66f9/img/avatars-teams/ava_0001-34.png'
}}
onMessageWasSent={this._onMessageWasSent}
messageList={this.state.messageList}
onFilesSelected={this._onFilesSelected}
newMessagesCount={this.state.newMessagesCount}
handleClick={this._handleClick}
isOpen={this.state.isOpen}
showEmoji
/>
</div>)
}
You have to bind your class methods in class components in order to call them with this. But you have to do this, e.g. in the constructor BUT not in your render function!
Check out this very nice explanation on why and how to bind your functions.
constructor( props ){
super( props );
this._onMessageWasSent = this._onMessageWasSent.bind(this);
this._onFilesSelected = this._onFilesSelected.bind(this);
this._handleClick = this._handleClick.bind(this);
this._sendMessage = this._sendMessage.bind(this);
}
In your render function, just pass the functions like follows:
render() {
return (<div>
<Launcher
agentProfile={{
teamName: 'Foodophile',
imageUrl: 'https://a.slack-edge.com/66f9/img/avatars-teams/ava_0001-34.png'
}}
onMessageWasSent={this._onMessageWasSent}
messageList={this.state.messageList}
onFilesSelected={this._onFilesSelected}
newMessagesCount={this.state.newMessagesCount}
handleClick={this._handleClick}
isOpen={this.state.isOpen}
showEmoji
/>
</div>)
}
Also, there is one more issue. This binding is a tricky thing in JavaScript and function vs ()=>{} arrow functions do treat this differently. In your case, just use an arrow function instead.
apigClient.invokeApi(pathParams, pathTemplate, method, additionalParams, body)
.then((result) => {
var text = result.data.body
console.log(text)
console.log(text.length)
this._sendMessage(text)
}).catch(function (result) {
});
This will make sure that this inside your then-callback function is still the this that you expect it to be. This is why, if you would refactor all your functions (_onMessageWasSent, _onMessageWasSent, _onFilesSelected, handleClick, _sendMessage ) to arrow functions, there is no need anymore to bind them to this in the constructor.
See this for example:
_onMessageWasSent = (message) => {
// your function body
}
You could already get rid of the line this._onMessageWasSent = this._onMessageWasSent.bind(this);.
Read more about this binding in functions at w3school.

Duplicate lists printing after adding new document to firestore collection

When the component loads, it pulls all the data from a specific collection in firestore and renders it just fine. then when i add a new document, it adds that document but then prints them all out (including the new one) under the previous list.
This is my first real react project and I am kinda clueless. I have tried resetting the state when the component loads and calling the method at different times.
import React, { Component } from 'react';
// Database Ref
import Firebase from '../../Config/Firebase';
// Stylesheet
import '../View-Styles/views.scss';
// Componenents
import Post from '../../Components/Post/Post';
import EntryForm from '../../Components/EntryForm/EntryForm';
export class Gym extends Component {
constructor() {
super();
this.collection = 'Gym';
this.app = Firebase;
this.db = this.app.firestore().collection('Gym');
this.state = {
posts: []
};
this.addNote = this.addNote.bind(this);
};
componentDidMount() {
this.currentPosts = this.state.posts;
this.db.onSnapshot((snapshot) => {
snapshot.docs.forEach((doc) => {
this.currentPosts.push({
id: doc.id,
// title: doc.data().title,
body: doc.data().body
});
});
this.setState({
posts: this.currentPosts
});
});
};
addNote(post) {
// console.log('post content:', post );
this.db.add({
body: post
})
}
render() {
return (
<div className="view-body">
<div>
{
this.state.posts.map((post) => {
return(
<div className="post">
<Post key={post.id} postId={post.id} postTitle={post.title} postBody={post.body} />
</div>
)
})
}
</div>
<div className="entry-form">
<EntryForm addNote={this.addNote} collection={this.collection} />
</div>
</div>
)
};
};
export default Gym;
I am trying to get it to only add the new document to the list, rather than rendering another complete list with the new document. no error messages.
Your problem lies with your componentDidMount() function and the use of onSnapshot(). Each time an update to your collection occurs, any listeners attached with onSnapshot() will be triggered. In your listener, you add each document in the snapshot to the existing list. While this list starts off empty, on every subsequent change, the list is appended to with all of the documents in the collection (including the old ones, not just the changes).
There are two ways to handle the listener's snapshot when it comes in - either empty the existing list and recreate it on each change, or only handle the changes (new entries, deleted entries, etc).
As a side note: When using onSnapshot(), it is recommended to store the "unsubscribe" function that it returns (e.g. this.stopChangeListener = this.db.onSnapshot(...)). This allows you later to freeze the state of your list without receiving further updates from the server by calling someGym.stopChangeListener().
Recreate method
For simplicity, I'd recommend using this method unless you are dealing with a large number of items.
componentDidMount() {
this.stopChangeListener = this.db.onSnapshot((snapshot) => {
var postsArray = snapshot.docs.map((doc) => {
return {
id: doc.id,
// title: doc.data().title,
body: doc.data().body
});
});
this.currentPosts = postsArray;
this.setState({
posts: postsArray
});
});
};
Replicate changes method
This method is subject to race-conditions and opens up the possibility of desyncing with the database if handled incorrectly.
componentDidMount() {
this.stopChangeListener = this.db.onSnapshot((snapshot) => {
var postsArray = this.currentPosts.clone() // take a copy to work with.
snapshot.docChanges().forEach((change) => {
var doc = change.document;
var data = {
id: doc.id,
// title: doc.data().title,
body: doc.data().body
});
switch(change.type) {
case 'added':
// add new entry
postsArray.push(data)
break;
case 'removed':
// delete potential existing entry
var pos = postsArray.findIndex(entry => entry.id == data.id);
if (pos != -1) {
postsArray.splice(pos, 1)
}
break;
case 'modified':
// update potential existing entry
var pos = postsArray.findIndex(entry => entry.id == data.id);
if (pos != -1) {
postsArray.splice(pos, 1, data)
} else {
postsArray.push(data)
}
}
});
this.currentPosts = postsArray; // commit the changes to the copy
this.setState({
posts: postsArray
});
});
};
As a side note: I would also consider moving this.currentPosts = ... into the this.setState() function.
When you use onSnapshot() in Cloud Firestore,you can print only the added data. For your code, it should be something like:
snapshot.docChanges().forEach(function(change) {
if (change.type === "added") {
console.log("Newly added data: ", change.doc.data());
}
Also, Firestore does not load the entire collection everytime a new data is added, the documents are cached and will be reused when the collection changes again.
For more info, you can checkout this answer.

React native data not rendered after setstate

So i have been working with firebase as a backend in my react native application, i have tried to fetch data this way but i have nothing rendered, i have the activity indicator that went off, but i get that the data array is empty in the application screen, and when i do a console.log, i can see the data in the console, but nothing shows off in the application screen, please help me it's been days that i'm struggling.
export default class Leaderboard extends React.Component{
constructor(props){
super(props)
this.state = {
loading : true,
data : []
}
}
componentDidMount(){
firebase.firestore().collection('rankings').get()
.then(res => {
let rankArray = []
res.forEach(document => {
rankArray.push(document.data())
})
return rankArray;
}).then(res =>{
let data = []
res.forEach(item =>{
firebase.firestore().doc(item.idUser.path)
.get()
.then(doc =>{
let dataItem = {}
dataItem.id = doc.ref.path
dataItem.name = doc.data().fullname
dataItem.points = doc.data().points
dataItem.lc = 'Oran'
data.push(dataItem)
dataItem = {}
})
})
return data;
}).then(res =>this.setState({
loading : false,
data : res
}) ).catch(err => console.log(err))
}
render(){
if(this.state.loading){
return(
<View style = {styles.container}>
<ActivityIndicator size= 'large'></ActivityIndicator>
</View>
)
}else{
console.log(this.state.data)
return(
<View>
<Text>{this.state.data.length}</Text>
<FlatList
data={this.state.data}
renderItem={({item}) => <Text>{item.fullname}</Text>}
/>
</View>
)
}
}
}
The reason for this not working as expected is that you're trying to perform an asynchronous function call, per iteration of your res array inside of your forEach() callback:
// This is asynchronous
firebase.firestore().doc(item.idUser.path).get().then(doc =>{ ... })
Consider revising your code to use the Promise.all() method instead. This will ensure that each asynchronous for individual documents per-item in res array is completed, before setState() in the susequent .then() handler is invoked:
.then(res => {
let rankArray = []
res.forEach(document => {
rankArray.push(document.data())
})
return rankArray;
})
.then(res => {
// Use promise all to resolve each aync request, per item in the
// res array
return Promise.all(res.map(item => {
// Return promise from .get().then(..) for this item of res array.
return firebase.firestore()
.doc(item.idUser.path)
.get()
.then(doc => {
let dataItem = {}
dataItem.id = doc.ref.path
dataItem.name = doc.data().fullname
dataItem.points = doc.data().points
dataItem.lc = 'Oran'
// Return resolve dataItem to array that is relayed to next .then()
// handler (ie where you call this.setState())
return dataItem
})
}));
})
.then(res =>this.setState({
loading : false,
data : res
}))

Lifecycle hooks - Where to set state?

I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.
This is a module to get the data that I need for my app.
export const moviesData = {
popular_movies: [],
top_movies: [],
theaters_movies: []
};
export const queries = {
popular:
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
top_rated:
"https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
theaters:
"https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};
export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";
export function getData(arr, str) {
for (let i = 1; i < 11; i++) {
moviesData[arr].push(str + i);
}
}
The stateful component:
class App extends Component {
state = {
movies = [],
sortMovies: "popular_movies",
query: queries.popular,
sortValue: "Popularity"
}
}
// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
const { sortMovies, query } = this.state;
getData(sortMovies, query);
const data = await Promise.all(
moviesData[sortMovies].map(async movie => await axios.get(movie))
);
const movies = [].concat.apply([], data.map(movie => movie.data.results));
this.setState({ movies });
}
In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:
handleSortValue = value => {
let { sortMovies, query } = this.state;
if (value === "Top Rated") {
sortMovies = "top_movies";
query = queries.top_rated;
} else if (value === "Now Playing") {
sortMovies = "theaters_movies";
query = queries.theaters;
} else {
sortMovies = "popular_movies";
query = queries.popular;
}
this.setState({ sortMovies, query, sortValue: value });
};
Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.
I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don't know where else I should do this if not in this method.
I hope that I made myself clear of what I am trying to do here, if not please ask, I'm stuck here and any help is greatly appreciated. Thanks in advance.
The best lifecycle method for fetching data is componentDidMount(). According to React docs:
Where in the component lifecycle should I make an AJAX call?
You should populate data with AJAX calls in the componentDidMount() lifecycle method. This is so you can use setState() to update your component when the data is retrieved.
Example code from the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("https://api.example.com/items")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
);
}
}
}
Bonus: setState() inside componentDidMount() is considered an anti-pattern. Only use this pattern when fetching data/measuring DOM nodes.
Further reading:
HashNode discussion
StackOverflow question

Resources