I started using React Native in September 2018. I always forget some things when I build new apps, so I'll keep track of the gotchas on this post.
Some topics, such as navigation, will be fundamental to how I think about apps. Others, will be one-line helpers that make apps work more smoothly.
It's gotten to the point where I find my own answers from 6 months before on certain Github issues.
I'll keep adding over time as I think of more. If anyone thinks these topics would be useful, let me know and I'll elaborate.
I have made libraries to address a number of the topics here, from navigation to design.
- Style quirks (Little things you should do to make sure your app works consistently across platforms.)
- Local testing (How do I run this locally?)
- Fixing
npm
packages, quickly You (want to add a feature or fix an issue in a dependency. How do we do this without waiting for a PR?) - Monorepo (How do I code share easily?)
- Navigation (How do I create a consistent navigation experience that shares all code on web and native?)
- Responsive design (How do designs look on different screen sizes?
- Pseudo classes (hover states, focus states, touchable states)
- Authentication patterns (how should users sign in? What should they see when?)
- Data fetching (how do I fetch data? from where?)
- Real-time data fetching (I want to set up listeners to my database. How?)
- Theming (I want my whole app to have an intuitive design system. What's the best way?)
- Caching data (Should I use redux, or a cache? Should backend data go with other global states?)
- Global state management _ Types* (How should I keep track of my TypeScript types in React?
- SCHU (Schema, Class, Hook, UI) This is an acronym I made that describes the way I think about building features. YouTube tutorials show you how to do cool things, but the part I missed was a mental model on building robust features.
- Typescript Union typeguards
function isOnboarded(user: User): user is OnboardedUser {}
Okay this has plagued me forever. Why doesn't scrolling work properly? The reason is that a ScrollView's parent needs a fixed height on web. Otherwise, it'll just grow past it.
The fail-safe way to get this to work is to have this at the root of your screen:
import { View, StyleSheet, ScrollView } from 'react-native'
export default function Screen() {
return (
<View style={StyleSheet.absoluteFill}>
<ScrollView />
</View>
)
}
Notice the absolute fill! This is crucial. If you want another view inside of the ScrollView
, make sure you give it a flex: 1
.
import { StyleSheet, ScrollView } from 'react-native'
import { View } from 'dripsy'
export default function Screen() {
return (
<View style={StyleSheet.absoluteFill}>
<View sx={{ flex: 1 }}>
<ScrollView />
</View>
</View>
)
}
If you're using react-navigation
stacks on web, you'll need to edit the cardStyle
, instead of setting absolute fill.
Set cardStyle
on any screens to { flex: 1 }
.
<Stack.Navigator
screenOptions={{
cardStyle: {
flex: 1
}
}}
/>
This is necessary since React Navigation wraps your screen with a Card
, and if you don't make it stretch its container, then your ScrollView
won't have a fixed height parent.
If you don't edit the cardStyle
here, there will be weird window scrolling, and onScroll
events might not fire from the ScrollView
in your screen. Or, you'll have to set fixed heights or something.
Also, if you ever edit a specific screen's cardStyle
, don't forget to add the flex: 1
, since they won't merge.
I got the work-around here.
Put this in your brain forever: you're using a ScrollView
in react-native-web
, it must be wrapped in a View
with flex: 1
(or with a fixed height.) Otherwise, it'll simply window scroll!
How did I figure this out? Well I saw this line of code on react-navigation
's Stack repo:
This is especially important for icon sets.
For using @expo/vector-icons
, you'll need to grab the actual .ttf
files from all the fonts you use.
In your /public
folder (when using Next.js), create a fonts
folder. Paste any fonts you use, including the font files from the expo vector icons (see these in @expo/vector-icons/build/vendor/react-native-vector-icons/Fonts
.
Copy-paste the files into that folder. Next, in pages/_document.js
, we'll want to pre-load the fonts so that they load quickly. If we don't do this for the icons, they will flicker and it'll be weird.
First, create a CSS style for each font using CSS @fontface
(this is in pages/_document.js
. You should have already copied the document from the expo-next adapter, so you can just append the fontface to the bottom of its custom CSS string.
@font-face {
font-family: 'ionicons';
src: url('/fonts/Ionicons.ttf');
}
@font-face {
font-family: 'entypo';
src: url('/fonts/Entypo.ttf');
}
Do the same for any custom fonts. Notice that the font-family
string is lowercase. To find the right name for a given icon font, before doing this, just inspect the HTML of an icon, and see what its font-family
value is in the CSS panel on Chrome.
Next, we'll need to import them. Import Head
from next/head
, and inside of Html
, paste this for each font:
<Head>
<link rel="preload" href="/fonts/Ionicons.ttf" as="font" crossOrigin="" />
<link rel="preload" href="/fonts/Entypo.ttf" as="font" crossOrigin="" />
</Head>
Fonts will now load in efficiently. Do the same for any custom fonts inside of Head
!
You can't use the same react-navigation stack on two different Next.js pages. I often make the same stack, and on different pages, just render a different initialRouteName
. For some reason, if I do this and navigate between pages, it doesn't update the stack. Adding a custom key
didn't solve this either.
The solution is to wrap each page in a single-page Stack. That is, take the stack you want to use in both places, and make it the only screen of another stack.
In reality, I kinda regret using react-navigation at all on web. A future abstraction will create my own stacks for web using only shallow Next.js routing for modals, and will open modal screens on native. I've discussed this at length on Twitter. It will likely be a custom solution in combination with expo-next-react-navigation
, which I'll try to share publicly when I figure it out. But I'm so deep into using react-navigation
on web (to a fault) that it's hard to change my mental model.
Look, yarn is just better. resolutions
are useful. It looks better. It causes fewer problems. It allows for workspaces
. Just use it.
Running Next apps on your local network can let any device on your WiFi access your dev app, besides just localhost:3000
. It's quite useful.
Create a local.js
file.
#!/usr/bin/env node
'use strict'
const { networkInterfaces } = require('os')
const nets = networkInterfaces()
const results = {} // or just '{}', an empty object
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
// skip over non-ipv4 and internal (i.e. 127.0.0.1) addresses
if (net.family === 'IPv4' && !net.internal) {
if (!results[name]) {
results[name] = []
}
results[name].push(net.address)
}
}
}
const ipAddress = results.en0 && results.en0[0]
if (ipAddress) {
console.log(
`🥳 Server running on local wifi: http://${ipAddress}:3000
Assuming you didn't run on a different port, this should now be running there.`
)
}
Add a local
script in package.json
: node local.js
.
Put withSourceMaps
first, if you're using it. After that, withTM
, then withBundleAnalyzer
, withFonts
, withExpo
, etc.
Make sure to put any monorepo folders into transpileModules
.
Make strictNullChecks: true
from the start.
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"jsx": "preserve",
"lib": ["dom", "esnext"],
"moduleResolution": "node",
"noEmit": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"strict": false,
"target": "esnext",
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "CommonJS",
"isolatedModules": false,
"strictNullChecks": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"packages/next-pages/_error.js",
"node_modules/react-native-redash/lib/typescript/v1/index.d.ts",
"packages/**/*.tsx",
"node_modules/@your_mono_repo_folder/**/*"
],
"exclude": ["node_modules"]
}
I will never write another app with Expo / React / Next.js / React Native without using patch-package
. This might be one of the most important node packages out there.
Use patch-package
. Use it. It lets you edit an npm
package locally, and keep those edits throughout your commits.
I still recommend sending PRs to your favorite open source libs to help them. But don't get lost in npm/git branch hell. Just use patch-package
until your PR gets merged, and move on with your life.
There are still a lot of bugs I encounter with this, such as some issues with Fast Refresh. I'm no expert with things like webpack, so I have no clue what I did to cause that.
But having a monorepo is a game changer. It makes it easy to extract things into their own packages down the line, too.
Here's an example of an Expo + Next.js monorepo I made, along with instructions on how to recreate it: https://github.com/nandorojo/expo-next-monorepo
...other things here later
Awesome man