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.
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.
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.
“Games Overview Component mounted” only shows up once, indicating that Games is mounted a single time.
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.
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 first thing to do is to set up the custom connect function. Here’s the bare minimum:
This sets up the scaffolding for our yet-to-be-written custom connect logic
The next thing is to wrap the component in a new component, like so:
This wraps our component in a custom Wrapper Component, which will store our logic
connect accepts a fourth argument, options:
[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.
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.
Let’s update WrapperComponent to maintain a reference to the instance of the wrapped component:
Save a reference to the wrapped component’s instance
refHandler saves the instance of the component to an internal variable, so we can call its lifecycle methods later on.
Let’s add this lifecycle method, componentWillAppear. The first thing is to listen to componentWillMount.
componentWillMount calls componentWillAppear on the wrapped component
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:
refHandler and componentWillMount both call componentWillAppear
The lifecycle method componentWillAppear gets called on your component. Yippee!
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:
REACT_NATIVE_ROUTER_FLUX_FOCUS events in the store
To get at this state, we’ll first need to set up a custom reducer. Mine looks
Reducer for listening to state changes in react-native-router-flux
And the new and improved patchedConnect looks like … drumroll please …
Connect listens to navigation changes in the store
A few things to point out here:
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.
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.
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 is called on WrapperComponent every time props change. If the prop change triggers the active state, we want to call the lifecycle method.
The lifecycle method is called on every navigation change
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:
When returning from home, componentWillAppear is called
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.
Ah, at last, the final implementation for your copypasta pleasure!
Full implementation of custom connect for componentWillAppear
There you have it. We have a reusable abstraction for handling scene change across all our components.