Skip to content

Instantly share code, notes, and snippets.

@nandorojo
Last active April 28, 2024 06:44
Show Gist options
  • Star 28 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save nandorojo/627ef0097fffa94f095bb4e94d9da4c7 to your computer and use it in GitHub Desktop.
Save nandorojo/627ef0097fffa94f095bb4e94d9da4c7 to your computer and use it in GitHub Desktop.
What I wish I knew when I started with Expo Web, React Navigation & Next.js

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.

Topics

  • 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 {}

Style quirks

1. Scroll issues

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:

2. Preload fonts on web

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!


Weird bugs

Reusing the same react-navigation stack across pages

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.


Local Testing

Use yarn, not npm

Look, yarn is just better. resolutions are useful. It looks better. It causes fewer problems. It allows for workspaces. Just use it.

Running Next.js on your local network

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.


next.config.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.


tsconfig.json

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"]
}

Fixing npm packages, quickly

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.


Monorepo

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

@xen-cyber
Copy link

great stuff

@Sriram-Prasanth
Copy link

Awesome man

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment