Re render when data from async function is ready - reactjs

I have an async function that GET the notes from network, the problem that I have is that it tries to render an empty array of data, therefore I get this error saying that item.id is undefined because the array is empty. I tried to put a condition if (data.length === 0) return <Text>No Entries</Text> but then it does not re render anything, even though when I console.log(data) I can see the data has arrived. Is there any way to re render when data has arrived, or any other way around this?
export default class NoteList extends Component {
render() {
const { data} = this.props;
return (
<View style={styles.cardView}>
<FlatList
numColons={data.length}
data={data}
renderItem={({ item: { name, content, id } }) => {
return (
<View>
<NoteCard
name={name}
content={content}
/>
</View>
);
}}
keyExtractor={(item) => item.id}
/>
</View>
);
}
}
How to prevent this:
TypeError: TypeError: undefined is not an object (evaluating 'item.id')
I also get this error, but I think it is related to the management of the first problem.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory
leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s, the componentWillUnmount method,

The problem you have, lies in the parent where data is saved in the state. Therefor you get the warning about updating the state of an unmounted component. There are a lot of different ways to fix this. One way I like (because of the readability), is using a variable for when the component mounts. A simple example:
class News extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
news: [],
};
}
componentDidMount() {
this._isMounted = true;
axios
.get('https://hn.algolia.com/api/v1/search?query=react')
.then(result => {
if (this._isMounted) {
this.setState({
news: result.data.hits,
});
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
...
}
}
Now when the data is set, the NoteList component will automatically get updated. However what happens when api call fails. To prevent stale data, I like to use conditional rendering. Just like you suggested:
export default class NoteList extends Component {
render() {
const { data } = this.props;
if (data) {
return (
<View style={styles.cardView}>
<FlatList
numColons={data.length}
data={data}
renderItem={({ item: { name, content, id } }) => {
return (
<View>
<NoteCard name={name} content={content} />
</View>
);
}}
keyExtractor={item => item.id}
/>
</View>
);
}
}
}

A common way to do this in React is to keep track of when data is being fetched. This can be done e.g. by having a isFetching field in your state:
// This would be your default state
this.state = {
isFetching: false
};
Then, when you fire off the request (preferably in componentDidMount) you set isFetching to true using:
this.setState({ isFetching: true });
And finally, when the data arrives, you set it to false again:
this.setState({ isFetching: false });
Now, in your render function you can do something like this:
render () {
return (
<div className="something">
<h3>Some content</h3>
{this.state.isFetching ? <LoadingComponent /> : (
<ul>
{listItems}
</ul>
)}
</div>
)
}
By using state, you don't have to worry about telling your component to do something, instead it reacts to changes in the state and renders it accordingly.

Related

In React Native, how do you set the state of a component when you navigate back to it?

I'm trying to navigate from one component to another and then pass parameters back to the original component you started on.
When the data comes back from component B to component A, I want to update the state of component A with that data but I keep getting an error:
Invariant violation: Maximum update depth exceeded. React limits the
number of nested updates to prevent infinite loops.
Component A
componentDidUpdate() {
const { params } = this.props.navigation.state;
// this won't work as it creates an infinite loop
this.setState({
property: params.myproperty
});
}
render() {
return (
<TouchableOpacity onPress={() => navigate('B')}>
<Text>Go to B</Text>
</TouchableOpacity>
);
}
Component B
render() {
return (
<TouchableOpacity onPress={() => navigate('A', {myproperty: 'something'})}>
<Text>Back to A</Text>
</TouchableOpacity>
);
}
Should I be using a different lifecycle hook instead of componentDidUpdate? I've seen some people adding a condition around the setState(), but I'm not sure how that works.
componentDidUpdate() {
const { params } = this.props.navigation.state;
if(params.myproperty) {
this.setState({
property: params.myproperty
});
}
}
params.myproperty won't exist when the component first mounts, so that results in an undefined error. How can I set the state of component A when you navigate back to it?
You need to do this
Screen A:
this.props.navigation.navigate('ScreenB', {
onPressScreenAFun: (params) => {
this.screenAFun(params)
},
})
screenAFun = (params) => {
console.log(params)
}
Screen B:
screenBFun = (params) => {
const { onPressScreenAFun } = this.props.navigation.navigate.state.params
onPressScreenAFun(params)
this.props.navigation.goBack()
}
Instead of handling this.setstate in componentDidMount(), handle it in componentWillMount(), it won't create an infinite loop in this way.
For example:
componentWillMount() {
const { navigation } = this.props;
const myproperty = navigation.getParam('myproperty', '0');
this.setState({ property: myproperty});
}

How to update state in componentWillMount and use that value in the render function in React Native?

I am collecting images from a dispatch call made to an action and mapping the returned response (images) into an array. When the data is finally returned I am storing this map by setting the state of the imgArray property. Unfortunately, when I do this I get a warning 'Can only update a mounted or mounting component' and my imgArray property on state is not available or updated in the render function (thus no images are being displaced). How can I get rid of this warning and get my data to the render function by first storing the data in the state?
componentWillMount function:
componentWillMount() {
this.props.dispatch(handleLoadProduct(this.props.product.id)).then((data) => {
let images = data.response.images.map(img => {
return 'https://b2b.martinsmart.com/productimages/' + img.original;
});
this.setState({imgArray: images});
});
}
Even though the isLoading property is getting set to true in the reducer it still shows up as false here and then the render is getting called before all the data is loaded.
render function:
render() {
const {isLoading, product} = this.props.products;
if (isLoading) {
return <Loader isVisible={true}/>;
}
return (
<View ref={(pc) => this.productCard = pc} style={{height: '100%', backgroundColor: '#D6D6D6'}}>
<Header/>
<View style={styles.wrapper}>
<View style={{height: '100%', borderRadius: 7}}>
<View style={styles.container}>
{this.state.imgArray &&
<Animated.Image
{...this.imgPanResponder.panHandlers}
key={'image' + this.state.imgIndex}
source={{uri: this.state.imgArray[this.state.imgIndex]}}
style={{left: this.imgXPos, width: '100%', height: '100%'}}
resizeMethod={'resize'}
resizeMode={'contain'}
/>
}
here I uploaded a video demonstrating this: https://youtu.be/tAbaq2IS4vY
Here is my reducer case:
case types.HANDLE_LOAD_PRODUCT: {
return {
...state,
isLoading: true,
product:{}
};
}
Here is my action function:
export function handleLoadProduct(productId) {
return (dispatch, getState) => {
if (getState().app.hasNetworkConnection) {
dispatch({
type: types.HANDLE_LOAD_PRODUCT
});
return API.getProduct(productId).then((response) => {
return dispatch({
type: types.HANDLE_LOAD_PRODUCT_SUCCESS,
response
});
});
}
};
}
Here is how I connect my products from the reducer:
function mapStateToProps(state) {
const {products} = state;
return {
products
};
}
export default connect(mapStateToProps)(ProductCard);
To fix the images not showing up issue, switch from componentWillMount to componentDidMount. In general, use componentDidMount for doing API calls.
The warning, however, might be due to many reasons. Try to use refs. If you are switching between screens, that might cause the issue too!
Check also the isMounted post in the React docs. I guess the component simply unmounts and then the state changes. Try to play around with console.log()s on componentWillUnmount() and figure out if the component unmounts before the this.setState() is called to change the state. A lot is going on in your code example, that's why it's a bit hard to say exactly what is causing the warning to show up. But these should give you a good clue.

Undefined is not an object(evaluating 'this.state.contacts.filter') in React Native

I'm a newbie in react native and I encountered the following problem while working on a sample chat app in android emulator.
Its showing the error
Undefined is not an object(evaluating 'this.state.contacts.filter')
in the line of code
let filteredContacts = this.state.contacts.filter(
(contact) => {
return contact.name.indexOf(this.state.search) !== -1;
}
);
Here is the code I'm working on.
componentDidMount() {
this.setState({contacts : this.props.navigation.state.params.contacts});
render() {
let filteredContacts = this.state.contacts.filter(
(contact) => {
return contact.name.indexOf(this.state.search) !== -1;
}
);
return (
<View style={styles.listContainer}>
<TextInput style={styles.searchbar} placeholder="Search"
onChange={(search) => {this.setState({search: search, isContactSearch: true}); this.onSearchPress}} />
{!this.state.isContactSearch &&
<FlatList
keyExtractor={(item, index) => index}
renderItem={this.renderFlatListItem}
data={this.state.historyContactList}
ItemSeparatorComponent={this.renderSeparator}>
</FlatList>
}
{this.state.isContactSearch &&
<FlatList>
{filteredContacts.map((contact) => {
return <View key={contact.number}>{contact.name}</View>
})}
</FlatList>
}
</View>
);
}
The componentDidMount callback runs only after the first render, so the state you set there isn't available on the first render pass.
You can move the state initialization logic to constructor instead:
constructor(props) {
super(props);
this.state = {
contacts: props.navigation.state.params.contacts
};
}
Note that here we set the state using regular javascript assignment instead of setState, because the latter is asynchronous and not safe to call in the constructor. If you want to later modify the state, you'll need to use setState as you have now.
Since componentDidMount is called after your render method your initial state value is not initialized! You can do 2 things here,
1) Set an initial value for your state params in the constructor if this is a one time setup you need!
2) If you want the state to update before every render you can copy the same code into componentWillMount/componentWillReceiveProps

React Native Props Not Updating

I'm pretty new to React and I'm running into an issue with updating my props in a container. I'm updating my state using WebSockets and the props are being updated in my mapStateToProps function, but my componentWillReceiveProps is not being called despite that.
When the sockets emit, updateGameVariables calls an Action sending the emitted data, which then goes to my reducer which is using the Spread Operator to update state. And then mapStateToProps logs the proper data (which is updating).
Here is the main file I am dealing with (everything is being properly imported I just wanted to cut down on code):
class GameList extends Component {
constructor(props){
super(props);
this.ds = new ListView.DataSource({ rowHasChanged: (r1,r2) => r1 !== r2})
const { games } = props;
this.state = {
games: this.ds.cloneWithRows(games)
}
this.socket = SocketIOClient('http://localhost:9615',{ transports: ['websocket'], query: 'r_var=17' });
this.socket.on('appGames', function(results){
props.dispatch(updateGameVariables(results))
});
}
componentWillReceiveProps(nextProps){
this.setState({
games: this.ds.cloneWithRows(nextProps.games)
})
}
render() {
let { games } = this.state;
return (
<View style={styles.list}>
<ListView
dataSource={games}
renderRow={(rowData, sectionID, rowID) =>
<TouchableOpacity>
<GameIntro key={rowID} game={rowData} />
</TouchableOpacity>
}
>
</ListView>
</View>
)
}
}
function mapStateToProps({games}){
return {
games: games.games, // Array
// rand: Math.random()
}
}
function mapDispatchToProps(dispatch) {
let actions = bindActionCreators({ updateGameVariables });
return { ...actions, dispatch };
}
export default connect(mapStateToProps, mapDispatchToProps)(GameList)
And here is the GameIntro component that is being referenced.
export default props => {
let { game } = props;
return (
<View style={styles.itemContainer}>
<View style={styles.game_timerow}>
<Text style={styles.game_time}>{game.period} {game.minutes}:{game.seconds}</Text>
</View>
</View>
)
}
Also as a note, when I have the rand: Math.random() function uncommented everything updates properly. And so I feel like react simply isn't picking up on updates to the games array. Or I am just not understanding a core concept of React. Any help would be greatly appreciated!
It's likely that you have a mutation problem in your reducer code, and because of that React see the games array as the same, then decide to not update the rendering. It explains why
rand: Math.random()
help React to realize that there is update in the props object and trigger re-render.
http://redux.js.org/docs/recipes/reducers/ImmutableUpdatePatterns.html might help.

React component does not react to mobx observable data

I am using mobX for my react native project. Please consider this store class:
class Birds {
#observable listOne = [];
#observable fetchingListOne = false;
#observable fetchErrorOne = '';
#action setListOne = () => {
this.fetchingListOne = true;
api.getList()
.then((data) => {
this.listOne.replace(data);
this.fetchingListOne = false;
})
.catch((error) => {
this.fetchingListOne = false;
this.fetchErrorOne = error;
});
};
}
And this the react component:
#inject('BirdStore') #observer
export default class Flat extends React.Component {
componentDidMount() {
this.props.BirdStore.setListOne();
}
_renderHeader = () => {
return <Text style={styles.listHeaderText}>
Hello {this.props.BirdStore.listOne.length} is {this.props.BirdStore.fetchingListOne.toString()}
</Text>;
};
_renderItem = ({item}) => {
return <Text style={styles.item}>{item.name}</Text>
};
_renderFooter = () => {
if (this.props.BirdStore.fetchingListOne) {
return <ActivityIndicator/>
}
else {
return null
}
};
render() {
const dataSource = this.props.BirdStore.listOne.slice();
return (
<View style={styles.container}>
<Text>Fetching: {this.props.BirdStore.fetchingListOne.toString()}</Text>
<FlatList
style={styles.listContainer}
ListHeaderComponent={this._renderHeader}
data={dataSource}
renderItem={this._renderItem}
keyExtractor={(item, i) => item.id}
ListFooterComponent={this._renderFooter}
/>
</View>
)
}
}
From above it looks to me that:
When the Flat component mounts, it call the method of the store setListOne().
setListOne() sets fetchingListOne to true and makes an api call.
On the component side, when the fetchingListOne is true, the ActivityIndicator displays, and in the ListHeaderComponent it should display true.
On the store side, after successful/unsuccessful response, it sets fetchingListOne to false.
Finally on the component side, because fetchingListOne is set to false, ActivityIndicator should not display and in the ListHeaderComponent it should display false.
However, this is not what's happening. Here when the setListOne() method is called, after it sets the fetchingListOne to true, the component does not react to the changes made after api call. And the ActivityIndicator keeps displaying and in ListHeaderComponent its displaying true.
What am I doing wrong here? Could you please help me. Thank you
Update
I have added a Text component before the FlatList. Adding a Text component or console logging inside the component class's render method does makes the FlatList react to the changes. I don't know why this is happening though.
The problem you are running into here most probably, is that although Flat is an observer component, FlatList is not (it's an built-in component after all). In this setup _renderFooter and the others are part are rendered by render of FlatList, but not of FlatList. Hence they are not part of the lifecycle of Flat, but of FlatList and as such are not tracked by Mobx
There are two ways to fix this, both pretty simple:
1) declare _renderItem as observer component:
_renderItem = observer(({item}) =>
<Text style={styles.item}>{item.name}</Text>
);
2) use an inline anonymous Observer component:
_renderItem = ({item}) =>
<Observer>{
() => <Text style={styles.item}>{item.name}</Text>}
</Observer>

Resources