React component does not react to mobx observable data - reactjs

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>

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.

How to update the props of a component in a list of components

I'm trying to update a prop value of a component in a list of components. Following is an example of it.
I'm developing an app using ReactNative
...
constructor(props) {
state = {
components: [*list of components*],
}
componentDidMount() {
fetchingAPI().then(response => {
const components = [];
for (const data of response.data) {
components.push(<MyComponent numOfLike={data.numOfLike} />);
}
this.setState({components});
});
}
render() {
return (
...
{this.state.components}
...
);
}
When I want to update a component, I update the whole state named components like :
updateAComponent(index, newNumOfLike) {
const components = this.state.components;
components[index] = <MyComponent numOfLike={newNumOfLike} />
this.setState({components});
}
But, this method change the component, not update. right? I means the components state is updated but MyComponent in components[index] is changed.
So, if I want to update the MyComponent in components[index] using the way of update the props numOfLike directly, how can I do it?
addition :
What I did not mention is that the MyComponent has a Image tag in it. So if I use FlatList or array.prototype.map there are several issues.
If I update the state, the whole list will be re-rendered. So if there are many list item, the speed of updating is very slow.
Since there are Image tag in the list, if I update a list item, the whole Image tags blink since the list items are re-rendered.
In this situation
Is there way to re-render(update) only a component which I want to update? (target updating)
If there in no way to target updating, just let the whole list items(components) re-rendered when just a component is updated?
You can use setNativeProps, described in the direct manipulation documentation
components[index].setNativeProps(propsObj)
You can modify your componentDidMount function like this (so that there are no race around or async conditions in the code) -:
componentDidMount() {
fetchingAPI().then(response => {
this.setState({
components: this.state.components.concat(
response.data.map(i => <MyComponent numOfLike={i.numOfLike} />)
)});
});
}
Can you try with the FlatList?
eg:
...
constructor(props) {
state = {
componentsData: [],
}
componentDidMount() {
fetchingAPI().then(response => {
this.setState({componentsData: response.data});
});
}
_renderItems = ({ item, index }) => {
return(
<MyComponent numOfLike={item. numOfLike} />
)
}
render() {
return (
...
<FlatList
data={this.state.componentsData}
renderItem={this._renderItems}
keyExtractor={(item, index) => index.toString()}
extraData={this.state}
/>
...
);
}
Then when you want to update the list,
updateAComponent(index, newNumOfLike) {
const data = this.state.componentsData;
data[index].numOfLike = newNumOfLike
this.setState({componentsData: data});
}

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});
}

Cannot read property 'propertyName' of undefined

I'm working on a project in react-native where I have troubles of accessing an element inside an object array by passing it as a prop where I want it to be used. Requirement is to get the name property out and set it to a text inside a flatlist.
The structure of my object array as follow.
[
{
"media1":[
{"name":"Lynn"},
{"name":"Michelle"},
{"name":"Carter"}
]
},
{
"media2":[
{"price":"23"},
{"price":"76"},
{"price":"39"}
]
}
]
This is how is pass this object array as a prop where I want it to be used
return (
<View>
<AlbumDetail data = {this.state.allData}/>
</View>
);
This is where I want it to be used
const AlbumDetail = (props) => {
return (
<View>
{console.log(props.data[0])} //Working
{console.log(props.data[0].media1[0].name)} //Not working
// Requirement as bellow
<Text>{wants to set the "name" here}</Text>
<Text>{wants to set the "price" here}</Text>
</View>
);
};
How can I achieve this ??
You might want to place two missing comma's.
One after:
{"name":"Michelle"}
And one after
{"price":"76"}
AlbumDetail has no way to know it has a property called data. You need to write AlbumDetail function as a React.Component class.
You are passing a JSON object into AlbumDetail, you need to call JSON.parse(data) before use it. UPDATE: .then(resp => resp.json()) is used to parse json.
Place console.log before return. The object you returned should be pure JSX components.
The code below should solve your problem:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
const url =
'http://purelight-prod.appspot.com/api/user/v2/browse/homescreendata';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: undefined,
};
}
componentDidMount() {
fetch(url)
.then(resp => resp.json())
.then(respJson => {
this.setState({
data: respJson,
});
})
.catch(err => {
console.error(err);
});
}
render() {
return (
<View style={{ flex: 1 }}>
<TestView data={this.state.data} />
</View>
);
}
}
class TestView extends React.Component {
render() {
!!this.props.data && console.log(console.log(data[0].healer[0].healerid));
return (
<View>
<Text>Hello World!</Text>
</View>
);
}
}
Edit:
Use componentDidMount(), because we like to display something (loading icon, etc), and then update the View when data arrived.
This is an async task. The data has to be held until it arrived. I use !!this.props.data && ..., so it only displays when it is not undefined.
Since the API response is a relatively big package, it will be much easier to work with, if you use TypeScript and create an object class to parse it.
I don't think the API helper package provides correct response in your code.

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.

Resources