I am having an issue where I setState in multiple components based on the same key in AsyncStorage. Since the state is set in componentDidMount, and these components don't necessarily unmount and mount on navigation, the state value and the AsyncStorage value can get out of sync.
Here is the simplest example I could make.
Component A
A just sets up the navigation and app.
var React = require('react-native');
var B = require('./B');
var {
AppRegistry,
Navigator
} = React;
var A = React.createClass({
render() {
return (
<Navigator
initialRoute={{
component: B
}}
renderScene={(route, navigator) => {
return <route.component navigator={navigator} />;
}} />
);
}
});
AppRegistry.registerComponent('A', () => A);
Component B
B reads from AsyncStorage on mount, and then sets to state.
var React = require('react-native');
var C = require('./C');
var {
AsyncStorage,
View,
Text,
TouchableHighlight
} = React;
var B = React.createClass({
componentDidMount() {
AsyncStorage.getItem('some-identifier').then(value => {
this.setState({
isPresent: value !== null
});
});
},
getInitialState() {
return {
isPresent: false
};
},
goToC() {
this.props.navigator.push({
component: C
});
},
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>
{this.state.isPresent
? 'Value is present'
: 'Value is not present'}
</Text>
<TouchableHighlight onPress={this.goToC}>
<Text>Click to go to C</Text>
</TouchableHighlight>
</View>
);
}
});
module.exports = B;
Component C
C reads the same value from AsyncStorage as B, but allows you to change the value. Changing toggles both the value in state and in AsyncStorage.
var React = require('react-native');
var {
AsyncStorage,
View,
Text,
TouchableHighlight
} = React;
var C = React.createClass({
componentDidMount() {
AsyncStorage.getItem('some-identifier').then(value => {
this.setState({
isPresent: value !== null
});
});
},
getInitialState() {
return {
isPresent: false
};
},
toggle() {
if (this.state.isPresent) {
AsyncStorage.removeItem('some-identifier').then(() => {
this.setState({
isPresent: false
});
})
} else {
AsyncStorage.setItem('some-identifier', 'some-value').then(() => {
this.setState({
isPresent: true
});
});
}
},
goToB() {
this.props.navigator.pop();
},
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>
{this.state.isPresent
? 'Value is present'
: 'Value is not present'}
</Text>
<TouchableHighlight onPress={this.toggle}>
<Text>Click to toggle</Text>
</TouchableHighlight>
<TouchableHighlight onPress={this.goToB}>
<Text>Click to go back</Text>
</TouchableHighlight>
</View>
);
}
});
module.exports = C;
If you toggle in C and then return to B, the state in B and value in AsyncStorage are now out of sync. As far as I can tell, navigator.pop() does not trigger any component lifecycle functions I can use to tell B to refresh the value.
One solution I am aware of, but isn't ideal, is to make B's state a prop to C, and give C a callback prop to toggle it. That would work well if B and C would always be directly parent and child, but in a real app, the navigation hierarchy could be much deeper.
Is there anyway to trigger a function on a component after a navigation event, or something else that I'm missing?
Ran into the same issue as you. I hope this gets changed, or we get an extra event on components to listen to (componentDidRenderFromRoute) or something like that. Anyways, how I solved it was keeping my parent component in scope, so the child nav bar can call a method on the component. I'm using: https://github.com/Kureev/react-native-navbar, but it's simply a wrapper around Navigator:
class ProjectList extends Component {
_onChange(project) {
this.props.navigator.push({
component: Project,
props: { project: project },
navigationBar: (<NavigationBar
title={project.title}
titleColor="#000000"
style={appStyles.navigator}
customPrev={<CustomPrev onPress={() => {
this.props.navigator.pop();
this._sub();
}} />}
/>)
});
}
}
I'm pushing a project component with prop data, and attached my navigation bar component. The customPrev is what the react-native-navbar will replace with its default. So in its on press, i invoke the pop, and call the _sub method on my ProjectList instance.
I think the solution to that should be wrapper around the AsyncStorage and possibly using "flux" architecture. https://github.com/facebook/flux with the help of Dispatcher and Event Emitters - very similar to flux chat example: https://github.com/facebook/flux/tree/master/examples/flux-chat
First and most important. As noted in AsyncStorage docs: https://facebook.github.io/react-native/docs/asyncstorage.html
It is recommended that you use an abstraction on top of AsyncStorage
instead of AsyncStorage directly for anything more than light usage
since it operates globally.
So I believe you should build your own specific "domain" storage which will wrap the generic AsyncStorage and will do the following (see storages in the chat example):
expose specific methods (properties maybe?) for changing the values (any change should trigger "change" events after async change finishes)
expose specific methods (properties maybe?) for reading the values (those could become properties read synchronously as long as the domain storage caches the values after the async change finishes)
expose a "register for change" method (so that components that need to respond to change can register)
in such "change" event handler the component's state should be set as read from the storage (reading the property)
last but not least - I think it's best if following react's patterns, you make changes to the storage's through Dispatcher (part of flux). So rather than components calling the "change" method directly to the "domain storage", they generate "actions" and then the "domain" storage should handle the actions by updating it's stored values (and triggering change events consequently)
That might seem like an overkill first, but It solves a number of problems (including cascading updates and the like - which will be apparent when the app becomes bigger - and it introduces some reasonable abstractions that seem to make sense. You could probably to just points 1-4 without introducing the dispatcher as well - should still work but can later lead to the problems described by Facebook (read the flux docs for more details).
1) The main problem here is in your architecture - you need to create some wrapper around AsyncStorage that also generates events when some value is changed, example of class interface is:
class Storage extends EventEmitter {
get(key) { ... }
set(key, value) { ... }
}
In componentWillMount:
storage.on('someValueChanged', this.onChanged);
In componentWillUnmount:
storage.removeListener('someValueChanged', this.onChanged);
2) Architecture problem can be also fixed by using for example redux + react-redux, with it's global app state and automatic re-rendering when it is changed.
3) Other way (not event-based, so not perfect) is to add custom lifecycle methods like componentDidAppear and componentDidDissapear. Here is example BaseComponent class:
import React, { Component } from 'react';
export default class BaseComponent extends Component {
constructor(props) {
super(props);
this.appeared = false;
}
componentWillMount() {
this.route = this.props.navigator.navigationContext.currentRoute;
console.log('componentWillMount', this.route.name);
this.didFocusSubscription = this.props.navigator.navigationContext.addListener('didfocus', event => {
if (this.route === event.data.route) {
this.appeared = true;
this.componentDidAppear();
} else if (this.appeared) {
this.appeared = false;
this.componentDidDisappear();
}
});
}
componentDidMount() {
console.log('componentDidMount', this.route.name);
}
componentWillUnmount() {
console.log('componentWillUnmount', this.route.name);
this.didFocusSubscription.remove();
this.componentDidDisappear();
}
componentDidAppear() {
console.log('componentDidAppear', this.route.name);
}
componentDidDisappear() {
console.log('componentDidDisappear', this.route.name);
}
}
So just extend from that component and override componentDidAppear method (Don't forget about OOP and call super implementation inside: super.componentdDidAppear()).
If you use NavigatorIOS then it can pass an underlying navigator to each child component. That has a couple of events you can use from within those components:
onDidFocus function
Will be called with the new route of each scene
after the transition is complete or after the initial mounting
onItemRef function
Will be called with (ref, indexInStack, route)
when the scene ref changes
onWillFocus function
Will emit the target route upon mounting and
before each nav transition
https://facebook.github.io/react-native/docs/navigator.html#content
My solution is trying to add my custom life cycle for component.
this._navigator.addListener('didfocus', (event) => {
let component = event.target.currentRoute.scene;
if (component.getWrappedInstance !== undefined) {
// If component is wrapped by react-redux
component = component.getWrappedInstance();
}
if (component.componentDidFocusByNavigator !== undefined &&
typeof(component.componentDidFocusByNavigator) === 'function') {
component.componentDidFocusByNavigator();
}
});
Then you can add componentDidFocusByNavigator() in your component to do something.
Related
Expected behaviour of this component is like this: I press it, selectedOpacity() function is called, state is updated so it now renders with opacity=1.
But for some reason, after calling this.setState, it is not being re-rendered. I have to click this component again to make it re-render and apply changes of opacity from state.
export default class Category extends Component {
state = {
opacity: 0.5
}
selectedOpacity() {
// some stuff
this.setState({opacity: 1})
}
render() {
return(
<TouchableOpacity style={[styles.container, {opacity: this.state.opacity}]} onPress={() => {
this.selectedOpacity();
}}>
</TouchableOpacity>
)
}
I think what you are missing is binding of selectedOpacity(), else this would be undefined in it AFAIK.
Also better move the assignment of state to a constructor().
constructor(props) {
super(props);
this.state = {};
this.selectedOpacity = this.selectedOpacity.bind(this);
}
Also change to the following because creating an arrow function inside render affects performance.
onPress={this.selectedOpacity}
Change selectedOpacity to arrow function:
selectedOpacity = () => {
this.setState({opacity: 1})
}
Then:
onPress={this.selectedOpacity}
Edit: The react documentation says its experimental and the syntax is called public class field syntax.
Try change onpress to
onPress={() => this.selectedOpacity()}
I noticed I cannot access to a nested js object.
this.DB.get(2) returns an object in the form
{
id:1,
name: "my title",
Obj:{en:"english",fr:"french"}
}
This is the component
export default class Item extends Component {
state = {
Item:{}
}
constructor(props) {
super(props);
}
componentDidMount(){
this.DB = $DB()
this.setState({
Item: this.DB.get(2)
})
}
render() {
const { params } = this.props.navigation.state;
const id = params ? params.id : null;
const Item = this.state.Item
return (
<View style={{flex:1}}>
Number field: {Item.id} |
String field: {Item.name} |
Object field: <FlatList data={Object.entries(Item.Obj)} />
</View>
);
}
}
The problem: I cannot access to Item.Obj.
I got it, setState is async, I'm not sure this is causing the issue though. Anyway: is there a clean way for render the component in setState callback?
edit: I've just rebuilt (still in debug mode) and now it works, I do not changed anything, just added some console.log() around.
Anyway, I don't feel so safe. setState() is async and still I see around the web this function used wildly as it would be sync.
If the data I want to render are in the state, is there a way to render the component always after the state update?
Another doubt: you see the Component, it just does an access to a big JS object and shows some data and that's all. Do I really need to pass through the component state?
What do you think if I would move those 2 lines inside the render() method?
const DB = $DB()
const Item = DB.get(2)
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>
I'm really struggling to understand how to read and set this.state inside of functions called by WebView when doing specific operations. My end goal is to:
Show a activity indicator when the user clicks a link inside the webview
Perform certain actions based on the URL the user is clicking on
I'm very new to React, but from what I've learned, I should use () => function to bind this from the main object to be accessible inside the function.
This works on onLoad={(e) => this._loading('onLoad Complete')} and I can update the state when the page loaded the first time.
If I use onShouldStartLoadWithRequest={this._onShouldStartLoadWithRequest} I can see that it works and my console.warn() is shown on screen. this.state is of course not available.
However if I change it to onShouldStartLoadWithRequest={() => this._onShouldStartLoadWithRequest} the function doesn't seem to be executed at all, and neither this.state (commented in the code below) or console.warn() is run.
Any help is appreciated!
import React, { Component} from 'react';
import {Text,View,WebView} from 'react-native';
class Question extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
debug: 'Debug header'
};
}
render() {
return (
<View style={{flex:1, marginTop:20}}>
<Text style={{backgroundColor: '#f9f', padding: 5}}>{this.state.debug}</Text>
<WebView
source={{uri: 'http://stackoverflow.com/'}}
renderLoading={this._renderLoading}
startInLoadingState={true}
javaScriptEnabled={true}
onShouldStartLoadWithRequest={this._onShouldStartLoadWithRequest}
onNavigationStateChange = {this._onShouldStartLoadWithRequest}
onLoad={(e) => this._loading('onLoad Complete')}
/>
</View>
);
}
_loading(text) {
this.setState({debug: text});
}
_renderLoading() {
return (
<Text style={{backgroundColor: '#ff0', padding: 5}}>_renderLoading</Text>
)
}
_onShouldStartLoadWithRequest(e) {
// If I call this with this._onShouldStartLoadWithRequest in the WebView props, I get "this.setState is not a function"
// But if I call it with () => this._onShouldStartLoadWithRequest it's not executed at all,
// and console.warn() isn't run
//this.setState({debug: e.url});
console.warn(e.url);
return true;
}
}
module.exports = Question;
To access correct this (class context) inside _onShouldStartLoadWithRequest, you need to bind it with class context, after binding whenever this method will get called this keyword inside it will point to react class.
Like this:
onShouldStartLoadWithRequest={this._onShouldStartLoadWithRequest.bind(this)}
or use arrow function like this:
onShouldStartLoadWithRequest={this._onShouldStartLoadWithRequest}
_onShouldStartLoadWithRequest = (e) => {...}
Or like this:
onShouldStartLoadWithRequest={(e) => this._onShouldStartLoadWithRequest(e)}
Check this answer for more details: Why is JavaScript bind() necessary?
I am new to React and I find it interesting. I have a button and clicking on that button will set some state inside that class as true and based on that the title of the button will change.
I want to change some other React component in some other class based on this state change. How can I do that ?
For example :
var Comp1 = React.createClass({
getInitialState: function() {
return { logged_in: false }
},
handleClick: function() {
this.setState({logged_in: !this.state.logged_in});
},
render: function() {
return <button onClick={this.handleClick}>
{this.state.logged_in ? "SIGN OUT" : "SIGN IN"}
</button>;
}
})
Here is my second component which is rendered separately.
var Comp2 = React.createClass({
render: function(){
return <div>
{Comp1.state.logged_in ? "You are in" : "You need a login"}
</div>;
}
})
This is one component. I want to use the state of this component in another component either by passing the stat (though I am not using both the component in composite format, so they are independent by nature) or based on change in the state I can define another sate in the second component.
You need to provide a shared object that can handle the state that these two objects represent. It is going to be easier to pick a standard model for doing this, like redux or flux, than to come up with your own. Learning one of these patterns is crucial to development in React. I personally recommend redux.
This is a very stripped down version of what Flux would look like. It is not intended to be a real solution, just to provide a glimpse of the pattern. If you just want to skip to the code, here is a codepen.
The shared object is typically referred to as a store. Stores hold state, and provide methods to subscribe to changes in that state. Mutating this state would usually by done by calling publishing an event with dispatcher that notifies the store, but for simplicity I have included a publish function on the store itself
let loginStore = {
isLoggedIn: false,
_subscribers: new Set(),
subscribeToLogin(callback) {
this._subscribers.add(callback);
},
unsubscribeToLogin(callback) {
this._subscribers.delete(callback);
},
publishLogin(newState) {
this.isLoggedIn = newState;
this._subscribers.forEach(s => s(this.isLoggedIn));
}
};
Once a pub/sub system is in place, the components will need to subcribe to it. The login button mutates this state, so it will also publish.
class LoginButton extends React.Component {
constructor(...args) {
super(...args);
this.state = {isLoggedIn: loginStore.isLoggedIn};
}
update = (isLoggedIn) => {
this.setState({isLoggedIn});
}
componentDidMount() {
loginStore.subscribeToLogin(this.update);
}
componentDidUnmount(){
loginStore.unsubscribeToLogin(this.update);
}
handleClick = () => {
loginStore.publishLogin(!this.state.isLoggedIn);
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isLoggedIn ? "SIGN OUT" : "SIGN IN"}
</button>
);
}
}
class LoginHeader extends React.Component {
constructor(...args) {
super(...args);
this.state = {isLoggedIn: loginStore.isLoggedIn};
}
update = (isLoggedIn) => {
this.setState({isLoggedIn});
}
componentDidMount() {
loginStore.subscribeToLogin(this.update);
}
componentDidUnmount(){
loginStore.unsubscribeToLogin(this.update);
}
render() {
return (
<div>{this.state.isLoggedIn ? "You are in" : "You need a login"}</div>
);
}
}
You'll notice the second component does not refer to the state of the first component, but the state of the store. As you mentioned, since they do not have a reference to each other, and do not have a common parent, they cannot depend directly on each other for the loggedIn state.