Welcome to the 2nd part of my blog series on my experience of converting an existing react-native app to react-native-web. In “Part 1” we have already setup our mono-repo structure and have ported and compiled our mobile app in this new architecture. Now we are all set to actually get our react-native code to compile for web.
Part 1 can be found here.
TL;DR
In this part I will describe how we got our react-native-web app to compile for the web, and start on the real journey of dependency management and writing wrappers and other necessities on our way to getting whole app working on the web as well.
We will discuss info.plist, icons, fonts, babel and webpack config, library wrappers, routing issues, native libraries replacement(basically getting web app compiled and running).
First things first
info.plist
In “Part 1”, i think i didn’t mention that you also need to update your info.plist file(for iOS, we didn’t had any android builds yet, there might be extra steps for that) with help of your old existing info.plist file from old react-native app.
icons
You also want to have your app icons transferred obviously, as in part 1 we re-built whole ios folder. So, for icons, you can take this approach. And for web, replace the packages/web/public/favicon.icofile and adjust index.html in the same folder if needed. I personally replaced icon files manually though.
Actual web compilation work
Give web compilation a shot
At this point, you should try to run the web app with yarn workspace web start, as all the journey from here is mostly hit and trial.
If your app compiles successfully and works fine in the browser, you are damn lucky! And of-course you are done. Otherwise, keep reading.
I was not that lucky, there were numerous hits and trials and as a matter of fact, at the time of writing, we are still in the process of conversion.
Babel config updates in config-override.js
For the web part, as we are using CRA and react-app-rewired, packages/web/config-overrides.js is the file which will be used to override CRA’s default configurations. As our existing react-native app was built without considering CRA conventions, so we need to adjust babel and webpack config to make it successfully compile. I encourage you to take your time and read through the react-app-rewired and get sense of how it works, how the config-override.js file is all setup. And make initial adjustments as per your project needs.
Do keep git committing everything that worked, or you will end up with a mess. Also I encourage you to take notes for your README file and keep updating it for the team.
Tell babel to transpile specific node_modules
Now after initial adjustments(if any) in config-override.js file, if you try re-compiling your web app(yarn workspace web start), you may see some typical babel related errors, not recognizing JSX in some node_modules and similar issues, mostly in node_modules i.e 3rd party libraries.
Reason for this that in our config-override.js file we have not told it yet to transpile these libraries. So what we are going to do, is to put an entry for each of these erroring libraries in the appIncludes const in this config file and try compiling again.
This time we may see another library complaining, we will have to repeat this for each library until there are no more of these issues left. Here is how my appIncludes looks like:
const appIncludes = [
// Add every directory that needs to be compiled by Babel during the build.
resolveApp('src'),
resolveApp('../components/src'),
resolveApp('../../node_modules/react-native-color-matrix-image-filters'),
resolveApp('../../node_modules/react-native-htmlview'),
resolveApp('../../node_modules/react-native-loading-spinner-overlay'),
resolveApp('../../node_modules/@react-native-community/async-storage'),
resolveApp('../../node_modules/react-native-cookies'),
resolveApp('../../node_modules/react-native-router-flux'),
resolveApp('../../node_modules/react-navigation-deprecated-tab-navigator'),
resolveApp('../../node_modules/react-native-actionsheet'),
...
]
I have omitted a lot of entries, but you get the idea.
Go ahead and note this process down in your README, as you will need this for most of the libraries you decide to add later on as well.
Disable strictExportPresence
This wasted a lot lot of my time, maybe because of my lack of extensive experience with babel or webpack.
Add following line in the same file just before the return config line, this is due to the errors of missing exports.
config.module.strictExportPresence = false
If these errors were coming from my code, i would have fixed them, but these were from some of the external dependencies my app was using.
NOTE: There might be some other babel plugins and/or configs you want to change, you will have to do that in the same file and figure it out, can’t help much with that.
Start replacing non-compatiable libraries
You may still not get the successful compilation or you may get some errors in the browser console after the compilation, some libraries might need replacement or removal as they are not web compatible. To get something running ASAP i decided to remove one library and another one was easy to replace.
remove react-native-color-matrix-image-filters
We were using some Greyscale filters from this library, it turned out, this library uses native libraries to achieve some of its work and the greyscale filter on an image on one page was not worth the replacement efforts, so we just removed it. We did took a note to get something web compatible back inn later on.
You will have to lookout for similar library issues in your app. I have put this one up as an example.
Replace react-native-youtube
Similar was the case with react-native-youtube library, this was causing compilation failures, this was also being used on only 1 page, we replaced this with react-youtube and my both mobile and web apps were compiling fine, although this new one doesn’t seem compatible with native builds(as i can see some divs etc being used in it) so we took a note in our backlog.
Later you will see how we can write wrappers around some libraries which we had to use different ones for mobile and web, current goal was to make things compile first.
Once you get the initial screens all coming up and you seem to navigate fine, now the time is to worry about web navigation issues and other console errors.
Navigation issues
This is a bit tricky, as we are compiling our native app to be able to run on web, react-native-web‘s job is to do just that, compile the app written with RN’s API to compile to web compatible version, nothing else!
Depending on which router you use, this part can be a bit tricky, i will explain my approach according to my setup, you may need different approach(or maybe, nothing different at all for routing, if you are already using some web compatible router).
Issue: missing urls
We were able to navigate almost fine, pages were loading, there were some scrolling issues when using bottom navigation bars on web, but otherwise it was working just alright. Major issue was missing urls, it was just staying on https://localhost:4000 no matter which page I am on.
This is a deal breaker for a web app, because in web apps, users like to bookmark and directly go to certain pages. Which is not possible with current behavior.
Routing library considerations
We were using the react-native-router-flux library, which was a nice choice for routing in RN, but we were using v4.0.6 of this library which is based off of react-navigation v2. Flux is just a wrapper around this to give easier to use interface for routing management. So if we want to check if our router library is compatible with web, we have to see docs of underlaying library. Turned out it was not, officially.
They have work going on for official web support in v3 which is out, but web support in that is not quiet mature yet, though rapidly developing. We wanted to give it a try, we tried the current latest(4.1.0-beta.5) version of react-native-router-flux which uses react-navigation v3 under the hood, but no use.
Maybe by the time you read this article, that is mature enough for your use case, give it a shot!
At this stage, you can and should explore different options of routers for RNW app, you can consider using different routers for mobile and web. But what we decided was to replace the routing library with something else at a later time and fix other issues first. At the time of writing, we are in process of replacing it with react-router so let’s leave for another part, hopefully part 3 will contain more insights into our process of conversion.
Native analytics libraries issues
We had some native analytics libraries configured, which had mostly native code to do all the low level interaction with OS events and stuff, these libraries obviously were causing issues.
So, we decided to restricted them to mobile only(and put some replacement web analytics in future).
How? here’s how:
- Make this new file:
packages/mobile/src/AppMobile.js, this will be new entry point for mobile app, and will host the analytics setups code. - Move the analytics libraries from
packages/components/package.jsontopackages/mobile/package.json, andyarn install. - Tell mobile to use
AppMobileinstead ofAppas entry point inpackages/mobile/index.js. - Setup
AppMobile.jsfile to import and renderAppfromcomponents/src, and initialize native tracking libraries, will look something like:
import React, {Component} from 'react';
import App from 'components/src/App'
// import the needed analytics libraries
export default class AppMobile extends Component {
render() {
constructor(props) {
super(props);
// setup analytic libraries here
}
return (
<App/>
);
}
}
Obviously above is how we did it, you can take route of your choice.
And once again, we decided to worry about web tracking libraries later, we took a note in our backlog and moved on.
Library wrappers, the real deal
I have posted some wrappers in this repository, i will just explain here that what are these wrappers and how to create your own ones for your own needs.
What and Why: Wrappers are just wrappers! 😀 They are supposed to provide same interface for both RN(mobile) and RNW(web) usage. Although they might be using different libraries for each platform under the hood, but they provide same interface so you can import once and forget about the rest.
Structure: As you can see in this repository, I have a folder for each wrapper, this folder, contains one file for mobile and one for web specific use. Let’s take circularProgress as an example, it has a js file and a web.js file, appropriate file will automatically be loaded as per environment target when we import something like import AnimatedCircularProgress from './wrappers/circularProgress/animatedCircularProgress';.
This naming structure also works for any other component and js files in general, this is a handy trick in RNW development, we have a couple of components using this trick other than wrappers.
Keep interface same: Obviously, most important thing is to keep interface of these wrappers same. Which means, no matter which platform specific file is used, all these platform specific files in one wrapper should accept one interface. Prop names, function names, parameter names/count etc everything should be same.
You may find some wrappers online hidden deep in github issues or SO questions, some you will write yourself, i encourage you to share your wrappers with world if you write any, as RNW community is not that mature yet and deserves all the support it can get.
Image upload
On the RN side, we had a pretty good setup with image upload with fixed size cropping functionality and everything, unfortunately we didn’t find a ready made replacement for this which would work on web with same or similar API. For web there were different libraries for uploading and cropping. That too with different interfaces.
Obviously we would need to write a wrapper for this as well, our RN app was using react-native-image-crop-picker, for web we made a minimal wrapper which lacks cropping for now, and uses react-dropzone. This wrapper is not included in the repo i shared above, as its code was highly coupled with our use case and needs some cleanup before I can share it. And it was basically more of a component instead of a wrapper. But you got the idea, and you can write your own wrappers.
Modal implementation
Problem: RNW project supports limited set of essential components from RN(which you can see on their readme), and as RN team is also splitting up components in individual repositories for a goal of thinner core and faster evolution of all the components, RNW has also stopped introducing support of new components. One such sort of essential component is Modal.
Replacement: RNW doesn’t provide out fo the box support for modal api of RN, so we had to look for alternatives. One such alternative which we ended up using is modal-enhanced-react-native-web.
Small trick: One trick I found in some github discussion, was to alias this modal. So for web whenever a modal is imported from RN, it will get this library’s implementation, and for mobile/native builds, original implementation will be used. This is actually pretty nice, and requires only 1 extra line in your config-override.js file.
config.resolve.alias["react-native-web/dist/exports/Modal"] = "modal-enhanced-react-native-web"
Place this before return config and you are all set! Now web will use this replacement implementation of Modal and native will keep using original implementation, win win.
Gotcha!: There is one issue left though, if you were just doing some R&D sort of thing or personal project, this web replacement would had been good enough, but since we are building customer facing app, UX is key to success. modal-enhanced-react-native-web, as much as i have used(which is not a lot), produces mobile like(iOS like mostly) modals on web, which are not very good looking to start with. You can obviously find ways to work around this and have some styling and structure override for modals so they get better UX on web.
Fix web fonts
We were using some custom fonts, which were appearing fine on mobile, just like in old app. But were not being applied on web app. Issue was simple, we had to define font-face in css. Just open your packages/web/public/index.css and define your fonts, something like:
@font-face {
font-family: Font-Name;
src: url('Font-File.ttf') format('truetype');
}
@font-face {
font-family: Font-Bold;
src: url('Font-Bold.ttf') format('truetype');
}
Just replace Font-Name and Font-Bold of-course. And not to mention, you should have these font files in the same folder, or you can use relative paths in url.
After this, you can use these font with something like this in your stylesheets, (or in CSS the regular way):
fontFamily: 'Font-Name'
OR
fontFamily: 'Font-Bold'
—————————————————————————
Well, this is it for today, I think we were able to cover some important aspects here. If there are any typos in the post, or if you have any suggestions or some experience to share, please do share through comments or email.
Next?
In the next part, I will cover the responsiveness problems, and how so far we are handling them(although not a lot has yet been done on that) and navigation issues in detail and our journey of how we are replacing(or by that time, replaced) our navigation library.
I am actually thinking to have smaller posts, separate for responsiveness and routing, and as journey is still going on, anything else that comes up, i may split them in smaller posts. Its a lot easier to compile and lot easier to read i think.
Till next time, take care and happy sharing!
One thought on “Part 2 — Converting react native app to react-native-web (react PWA) in monorepo architecture”