Responsive component lifecycle methods do not start when navigating

I am having a problem when I installState in multiple components based on the same key in AsyncStorage. Since the state is set in the DidMount component, and these components are not necessarily unmounted and mounted in the navigation, the state value and the AsyncStorage value can go out of sync.

Here is the simplest example I could do.

Component A

A just installs the navigation and application.

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 to mount, and then sets the 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. The change 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 switch to C and then return to B, the state in B and the value in AsyncStorage are now out of sync. As far as I can tell, navigator.pop () does not start the component life cycle functions that I can use to tell B to update the value.

One solution that I know about but is not ideal is to get B to point to C and give C the inverse function to switch. This will work well if B and C will always be directly parents and children, but in a real application the navigation hierarchy can be much deeper.

Is there a way to call a function on a component after a navigation event or something else that I am missing?

+5
source share
5 answers

Go to the same problem as you. I hope this changes, or we get an extra event on the components to listen on (componentDidRenderFromRoute) or something like that. In any case, as I decided, this kept my parent component in scope, so the child protection panel could call a method on the component. I use: https://github.com/Kureev/react-native-navbar , but this is just a wrapper around the 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 click on the project component with prop data and bind the navigation bar component. CustomPrev is what change-native-navbar will replace by default. So in my press, I call pop and call the _sub method in my instance of ProjectList.

+2
source

I think the solution to this should be a wrapper around AsyncStorage and possibly using a "stream" architecture. https://github.com/facebook/flux using Dispatcher and Event Emitters - very similar to the flux stream example: https://github.com/facebook/flux/tree/master/examples/flux-chat

First and foremost. As stated in the AsyncStorage docs: https://facebook.imtqy.com/react-native/docs/asyncstorage.html

It is recommended that you use abstraction on top of AsyncStorage instead of AsyncStorage directly for something more than using light because it works on a global scale.

So, I believe that you should create your own domain repository that will wrap the general AsyncStorage and will do the following (see repositories in the chat example):

  • output specific methods (properties, perhaps?) for changing values ​​(any change should trigger β€œchange” events after the change to the asynchronous mode is completed)
  • output specific methods (properties, perhaps?) for reading values ​​(they can become properties that are read synchronously if the domain store caches values ​​after the change to the asynchronous mode is completed)
  • set the "register for change" method (so that components that should respond to the change can register)
  • in such a "change" event handler, the state of the component should be set as read from the repository (read property)
  • last but not least, I think that it’s best if, after reacting to the changes, you make changes to the repository through Dispatcher (part of the stream). Therefore, instead of components that call the "change" method directly in the "domain store", they generate "actions", and then the "domain store" must process the actions, updating its stored values ​​(and, accordingly, triggering change events).

At first this may seem redundant, but it solves a number of problems (including cascading updates, etc.) that will be obvious when the application grows larger - and it presents some reasonable abstractions that seem to make sense. probably, just up to point 1-4, without introducing a dispatcher, it should work anyway, but later it can lead to the problems described by Facebook (read more in the stream documents).

+1
source

If you use NavigatorIOS, it can pass the base navigator to each child component. This has several events that you can use from these components:

onDidFocus function

A new route for each scene will be called up after the transition is completed or after the initial installation

function onItemRef

Will be called with (ref, indexInStack, route) when the scene help changes

onWillFocus function

It emits a target route during installation and before each navigation transition

https://facebook.imtqy.com/react-native/docs/navigator.html#content

0
source

My solution is trying to add my own life cycle for the 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(); } }); 

You can then add componentDidFocusByNavigator() to your component to do something.

0
source

1) The main problem here is in your architecture - you need to create some kind of shell around AsyncStorage, which also generates events when some value is changed, for example, the class interface:

 class Storage extends EventEmitter { get(key) { ... } set(key, value) { ... } } 

In the WillMount component:

 storage.on('someValueChanged', this.onChanged); 

In the WillUnmount component:

 storage.removeListener('someValueChanged', this.onChanged); 

2) The architecture problem can also be fixed using, for example, redux with it the global state of the application and automatic re-rendering when it changes.

3) Another way (not event-based, therefore not ideal) is to add custom lifecycle methods such as componentDidAppear and componentDidDissapear. Here is an example of the 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 go from this component and override the componentDidAppear method (don't forget about OOP and super-execution of the call inside: super.componentdDidAppear() ).

0
source

All Articles