React Native Props Not Updating - reactjs

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.

Related

Re render when data from async function is ready

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.

Binding data from a promise to a react component is inaccessible

I have two classes, one being a child component called article. I want to pass data to the article component after receiving data in the parent, which is my app.js. However, I am unable to drill down into the data from the response that I am setting in the App constructor--the data I am trying to access is undefined, but I am able to print the full response out fine, including the data that I am unable to drill down into. I would like to tie the articles array from the following JSON to my respective article components.
Data is being pulled from: https://newsapi.org/v2/top-headlines?sources=google-news&apiKey=edd0276dc8344c2abaeb40a3f6fb439f
class Article extends Component {
constructor(props) {
super(props);
this.props.title = props.title;
this.props.description = props.description;
this.props.url = props.url;
}
render() {
let pic = {
uri: 'https://cdn.vox-cdn.com/thumbor/M8Nb8mKymSEN59T2iDIe5XXiNTw=/0x146:2040x1214/fit-in/1200x630/cdn.vox-cdn.com/uploads/chorus_asset/file/11738447/VRG_ILLO_2761_Spacecraft_Sidebyside.jpg'
};
return (
<View style={styles.article}>
<Header/>
<Text>Two rockets launched within 15 minutes of each other Wednesday morning</Text>
<Image source={pic} style={{ height: 250}}/>
<Text numberOfLines={3}>Early in the morning on July 24th, rocket enthusiasts will have the lucky experience of being able to watch two launches at roughly the same time. Around 7:30AM ET, SpaceX is slated to launch one of its Falcon 9 rockets from the California coast, while Europe…</Text>
</View>
);
}
}
export default class App extends Component {
constructor(props) {
super(props);
let url = 'https://newsapi.org/v2/top-headlines?sources=google-news&apiKey=edd0276dc8344c2abaeb40a3f6fb439f';
this.state = fetch(url)
.then(function(response) {
response.text().then(function(text) {
console.log(text, "TEXT!!!");
return text;
});
});
}
render() {
return (
<ScrollView>
<Article/>
</ScrollView>
);
}
}
You shouldn't really be doing your fetch in the constructor.
Here's a working example to get you started:
import React from "react";
import ReactDOM from "react-dom";
const Article = ({ title, desc, url }) => {
return (
<React.Fragment>
<h1>{title}</h1>
<p>{desc}</p>
<img src={url} />
</React.Fragment>
);
};
let url =
"https://newsapi.org/v2/top-headlines?sources=google-news&apiKey=edd0276dc8344c2abaeb40a3f6fb439f";
class App extends React.Component {
state = {
articles: []
};
componentDidMount() {
fetch(url)
.then(res => res.json())
.then(data => {
this.setState({
articles: data.articles
});
});
}
render() {
if (this.state.articles.length === 0) {
return "Loading..";
}
const firstArticle = this.state.articles[0];
const { title, description, url } = firstArticle;
return <Article title={title} desc={description} url={url} />;
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Working CodeSandbox here.
A couple things wrong about this:
the return type of a Promise<T> is a Promise<T> unless you await, in which case it's T.
you don't assign to this.state, state changes in React are abstracted away via this.setState
Following,
this.state = fetch(url)...
should be changed to
fetch(url)
.then(response => response.text())
.then(text => this.setState({ text }))
pass in this.state.text (or rename it) as props to the child
edit: as Colin pointed out in the comments below, assigning to this.state is fine in the constructor (which you are doing). However, fetching data is better suited for post-mount lifecycle methods like componentDidMount

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.

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>

How to properly use mapDispatchToProps function in sub component?

I'm making simple Todo Application using React Native + Redux following Youtube.
Adding Todo works well. so I took next step, trying to deleting todo got problem. The Video is little bit old, so the version and platform(Mine is Android) is different. so the way of it little different... (ES5/ES6 etc.)
Anyway... I want to send action to dispatcher using mapDispatchToProps's function, onDeleteTodo, but it's not working.
First I tried to connect the component to store, so Added line TodoItem = connect(mapStateToProps, mapDispatchToProps)(TodoItem);. but the error still left.
Something wrong... but I can't find, How can I fix it?
Thanks in advance... below is my code.
import React, {Component} from 'react';
import {
StyleSheet,
Text,
View,
TextInput,
ScrollView,
TouchableOpacity
} from 'react-native';
import {connect} from 'react-redux';
import {addTodo, deleteTodo} from '../actions';
class TodoItem extends Component {
render() {
return (
// ***************************************
// Below line (onPress prop) is problem.
// when I trying to save todo,
// Error "undefined is not a function (evaluating 'this.props.onDeleteTodo(this.props.id)')
<TouchableOpacity onPress={this.props.onDeleteTodo(this.props.id)}>
<View style={styles.todoContainer}>
<Text style={styles.todoText}>
{this.props.text}
</Text>
</View>
</TouchableOpacity>
)
}
}
TodoItem = connect(
mapStateToProps,
mapDispatchToProps
)(TodoItem);
class Main extends Component {
constructor(props) {
super(props);
this.state = {
newTodoText: ""
}
}
render() {
var renderTodos = () => {
return this.props.todos.map((todo) => {
return (
<TodoItem text={todo.text} key={todo.id} id={todo.id}/>
)
})
};
return (
<View style={styles.wrapper}>
<View style={styles.topBar}>
<Text style={styles.title}>
To-Do List
</Text>
</View>
<View style={styles.inputWrapper}>
<TextInput
onChange={(event) => {
this.setState({
newTodoText: event.nativeEvent.text
});
}}
value={this.state.newTodoText}
returnKeyType="done"
placeholder="New Todo"
onSubmitEditing={
() => {
if(this.state.newTodoText && this.state.newTodoText != ''){
this.props.onAddTodo(this.state.newTodoText);
this.setState({
newTodoText: ''
});
}
}
}
underlineColorAndroid='transparent'
style={styles.input}/>
</View>
<ScrollView
automaticallyAdjustContentInsets={false}>
{renderTodos()}
</ScrollView>
</View>
);
}
}
const mapStateToProps = (state) => {
return {
todos: state.todos
}
};
const mapDispatchToProps = (dispatch) => {
return {
onAddTodo: (todo) => {
dispatch(addTodo(todo))
},
onDeleteTodo: (id) => {
dispatch(deleteTodo(id))
}
}
};
Main = connect(
mapStateToProps,
mapDispatchToProps
)(Main);
export default Main
If you write yoru code like this onPress={ this.props.onDeleteTodo(this.props.id) } then you are passing to the onPress property anything that is returned by the function this.props.onDeleteTodo. In other words, this.props.onDeleteTodo is executed when the component is rendering.
If you want to pass this function (and not it's returned value) then you need to write onPress={ this.props.onDeleteTodo.bind(this, this.props.id) }. This way you are passing this function with this as a context and this.props.id as it's first argument. More about this method here: Use of the JavaScript 'bind' method
I found the solution... but I don't know why it works.
change prop to callback function
onPress={this.props.onDeleteTodo(this.props.id)}
==>
onPress={ () => { this.props.onDeleteTodo(this.props.id) } }
: Is onPress prop only receive callback function? I don't know.
Move connect statement to below of const mapStateToProps and mapDispatchToProps
: Is const ... variable only can reference below its declaration? I don't know also.
I don't really know If I've understood your code...
By the way, if you are importing any function from somewhere, I think that you don't have to use dispatch method, since deleteTodo is not a property method.
Try again without dispatch(), moreover, try to call directly deleteTodo() method.
EDIT: in onPress event write this -> onPress={() => deleteTodo(this.props.id)}
It should call the method onces the event is triggered
And let me know if it works!

Resources