Migrating React Native app to React Hooks
React Hooks are slowly taking over the frontend world. They have straightforward syntax and you can:
- use them inside functional components
- mix them with the old syntax inside class components
Until recently the new syntax wasn’t available for React Native developers. Luckily with React Native 0.59, we can use hooks on mobile as well. If you want to start using them today, just follow along!
At Onfido we use React Native for our ‘Demo App’, which is a multi-platform application showcasing our SDK. It helps clients integrate with our identity check solutions. Its small codebase was the perfect place to battle-test React Native 0.59 as soon as it was released.
Updating to React Native 0.59
The update itself is mostly straightforward but requires a manual update of some of the Android project files. In addition, you should update your Babel preset (if used) and if you test your app using Detox, you need to be update to at least version 12.3.0
:
Updating Android files
To use React Native 0.59 you must update com.android.tools.build
to version 3.3.0 in your build.gradle
file. You must also update Gradle to 4.10.1
in both build.gradle
and gradle/wrapper/gradle-wrapper.properties
.
Updating iOS files
You don’t need to do anything 🎉.
Enabling ‘inline requires’ in Metro bundler
React Native 0.59 introduced an experimental feature called ‘inline requires’. When enabled, Metro will analyze your package in order to slice it into lazy-loadable packages. It should speed up the launch time of the app. In the case of our Onfido Demo App it didn’t make any difference — but in our case the app is relatively small.
Updating deprecated modules
Several native modules were extracted to the separate community repositories in order to reduce the size of the base React Native repository (as part of “Lean Core” effort). Depending on which module you use, updates to the community package might be as straightforward as adding a new dependency and linking it, or it might require much more manual work. This time 6 modules have been deprecated and will be removed from the core repository soon:
- AsyncStorage,
- ImageStorage (replaced with expo-file-system or react-native-fs),
- MaskedViewIOS
- NetInfo
- Slider,
- ViewPagerAndroid.
If you are using any of these, you will now see a deprecated warning when you try to use them. Next up some of the problems we had.
Issues with the migration
Migrating some code to the new community equivalents might be harder than you think. In our case we couldn’t update AsyncStorage yet because of unresolved issues with the package with iOS (recursive dependencies in Podfiles is one of them).
Not updating the package resulted in a strange issue we started encountering during our tests. Because the package has been deprecated, the app started showing a warning at the bottom of the screen. Unfortunately they were rendered during the Detox automated tests and it was covering the button our tests were trying to click.
To detect issues like that in failing tests you can add --record-videos=failing
to your detox test command. It will generate a video file for each failing flow so you can replay it and see what exactly happened.
Once we located the issue, we decided to turn warnings off in the testing environment. You can do this easily by setting the environmental variableIS_TESTING=1
. You can also disable specific warnings instead. I am not recommending using deprecated methods in general, but if there is no way to migrate out of them yet— that’s the way to go.
Creating ESLint linter rule
Even though hooks are really easy to use, there are several rules you have to follow. You can use them only inside the React Components and they cannot be invoked inside conditional logic (official Rules of Hooks explanation is a really good source if you want to learn why). If you make an error like that, it is really hard to debug it. Fortunately, there is a eslint-plugin-react-hooks
package that can notify you whenever you make a mistake like that.
The first rule makes sure you don’t make mistakes described above. Rule exhaustive-deps
is more experimental - it makes sure that all the variables used inside the useEffect
callback function will be included in the list of dependencies (second argument). For me, it works around 90% of the time. In the rest of the situations I, have to explicitly disable it for a specific effect because I know I don't want to rerun the function for change of a certain parameter - but having to add a comment makes these decisions more explicit.
React Hooks can be used alongside the regular stateful components, but if you decide to go full in, I recommend setting up linter options to prevent you from using class components anymore. This way you can enforce consistency in the project and make sure all other collaborators adapt to the convention.
Currently there’s no plugin dedicated to that, but we can simply implement it using generic no-restricted-syntax
to define custom rule.
This rule disallows inheriting from either Component
or React.Component
. It is helpful both while refactoring an app to React Hooks as well as preventing the team from adding new class-based components in the future. The selector
is an esquery syntax which is really comprehensive way to query AST's (Abstract Syntax Tree) nodes that ESLint calculates. You can read more about that in ESLint Developers Guide.
Not all lifecycle methods are covered
React Hooks are great but they are not the solution for all problems. They do not cover (at least not yet) all the component lifecycle methods. If you use methods like componentDidCatch
or getSnapshotBeforeUpdate
, you cannot rewrite your component to use only hooks just yet. If you want to implement linting anyway, you will have to explicitly disable the rule for such components.
Next Steps
Once you have started using hooks, you might want to discover lesser-known hooks like useMemo
, useReducer
or useLayoutEffect
. If you want to read more how they work under the hood, check out this great article.