Custom Lifecycle Methods
Yesterday, while building a React Native app, I ran into an issue: there’s no consistent way to tell “a view has appeared” on the screen. We rely on this event to determine when to refresh data for a particular page, so I spent some time researching a reusable solution. And, like a tree falling in the woods, what good is it to keep it to myself? It must be shared with the world.
The Background
We’re building a native version of our Emoji Pictionary bot, which means for this first version, we’re hewing as close as possible to duplicating iMessage, with an initial overview of all messages threads, and the ability to drilldown into a message.
We’re using react-native-router-flux for routing between scenes and react-native-gifted-chat, along with good ol’ redux to tie it together.
The Problem
Every time a message thread appears, we want to dispatch an action to load new messages in that thread.
Unfortunately, there’s no consistent lifecycle method indicating when that event should occur. componentDidMount, the closest analogue, is called once, when a component is initially loaded by react-native-router-flux, but not on subsequent views.
Navigation state is kept in the store, so the component needs to stay alerted of those changes in order to trigger a fetchMessages action.
Finally, React Native provides AppState, a tool that exposes event listeners for the app changing to background state or active state. These event listeners need to trigger fetchMessages actions as well.
We need a solution that’s generic enough to be reusable across components, so we’re not rewriting code for each new component.
The Solution!
To recap, three events should trigger a fetchMessages action:
- The initial component mounting
- Navigation between scenes
- Returning to the app from the Home Screen, or from a different app
The solution we hit upon is a wrapper around connect that calls a custom lifecycle method on a given component, combining each of these three events into a single method.
Let’s step through the code and see how we got there. Any screw ups or questions, feel free to post in the comments!
The groundwork
The first thing to do is to set up the custom connect function. Here’s the bare minimum:
The next thing is to wrap the component in a new component, like so:
connect accepts a fourth argument, options:
return connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
...options,
withRef: true,
},
)(component);
[withRef = false] (Boolean): If true, stores a ref to the wrapped component instance and makes it available via getWrappedInstance() method. Defaults to false.
We need that wrapped instance to be able to access custom lifecycle methods on our components.
A quick summary
To recap: we now have access to our component and its lifecycle methods, along with an interface for holding the logic to determine when we should trigger a lifecycle method.
The rest of this article discusses our specific implementation needs for triggering lifecycle methods. I imagine your use case will be specific to your app’s needs, but the general principles will probably look the same.
Hooking up the lifecycle methods
Let’s update WrapperComponent to maintain a reference to the instance of the wrapped component:
refHandler saves the instance of the component to an internal variable, so we can call its lifecycle methods later on.
1) componentWillMount
Let’s add this lifecycle method, componentWillAppear. The first thing is to listen to componentWillMount.
If you try this, you probably won’t see the lifecycle method called. This is because WrapperComponent’s componentDidMount method is being called prior to WrappedComponent being mounted.
We can solve this by setting a bit of internal state and updating the refHandler:
2) Transitioning between scenes
Let’s next listen for transitions between scenes. react-native-router-flux stores navigation state in the store, as can be seen in this gif:
To get at this state, we’ll first need to set up a custom reducer. Mine looks like this:
And the new and improved patchedConnect looks like … drumroll please …
A few things to point out here:
const componentName = component.name;
We need a reference to the currently wrapped component’s name, to compare against whatever the active route is. If you’re uglifying your code for production, you’ll need to explicitly store a name reference on the component instead.
const mapStateToProps = ({ router }) => {
const {
scene,
} = router; return {
activeComponent: (scene || {}).title,
};
};
This is not the same mapStateToProps that gets passed to WrappedComponent; this is Wrapper-specific. It pops off the relevant bits of state (in this case, the current scene) that we need to calculate whether to call componentWillAppear or not.
isComponentActive(activeComponent) {
return activeComponent === componentName;
}willComponentBecomeActive(nextProps) {
return this.props.activeComponent !== nextProps.activeComponent && this.isComponentActive(nextProps.activeComponent);
}
Checks that the currently active component property is not identical to the next active component property (this would indicate that we’re receiving a new props payload, instead of a navigation event having occurred). If that check passes, make sure we’re actually in the right component by checking against the name.
componentWillReceiveProps(nextProps) {
if (this.willComponentBecomeActive(nextProps)) {
this.componentWillAppear();
}
}
componentWillReceiveProps is called on WrapperComponent every time props change. If the prop change triggers the active state, we want to call the lifecycle method.
3) Background to Foreground
The last step is to listen for the app moving from the background to the foreground.
React Native provides AppState, a module which provides event listeners for whenever the state changes.
Here’s how that looks:
We add the event listener in componentWillMount and remove it on componentWillUnmount. The listener itself sets state on the WrapperComponent, and checks to see if there’s a change.
Conclusion
Ah, at last, the final implementation for your copypasta pleasure!
There you have it. We have a reusable abstraction for handling scene change across all our components.