Expo + Next.js Router + React Navigation 🥳
A set of hooks that wrap the react-navigation
API that you're used to, and make it work with next/router
.
This library helps me use the Expo + Next.js integration without stressing about navigation.
This is a new library, PRs are very welcome!
Example
👾 Github Repo | 💻 Website | 📱 Open expo app directly | ☎️ Expo app website
Install
For react-navigation
v6
yarn add expo-next-react-navigation
For react-navigation
v5
Version 1.x supports v5.
yarn add expo-next-react-navigation@v5
For react-navigation
v4
yarn add expo-next-react-navigation@0.0.25
React navigation v4 is supported up to v0.0.25.
I'm probably going to stop releasing new versions for v4
, and v5
will soon be upgraded to the latest
tag.
Table of contents
Set up
Step 0. Install next with expo:
-
Init: expo init
(or npx create-next-app
)
-
Install: yarn add @expo/next-adapter
-
Install next: yarn add next
-
Configure: yarn next-expo
-
Start: yarn next dev
I recommend becoming familiar next
's architecture with expo
. Follow the Expo docs or see this article by Evan Bacon if you're curious.
Step 1. Edit/create next.config.js
yarn add next-compose-plugins next-fonts next-images next-transpile-modules
Step 2: edit next.config.js
to look something like this:
/* eslint-disable @typescript-eslint/no-var-requires */
const { withExpo } = require('@expo/next-adapter')
const withFonts = require('next-fonts')
const withImages = require('next-images')
const withPlugins = require('next-compose-plugins')
const withTM = require('next-transpile-modules')([
'expo-next-react-navigation',
// you can add other modules that need traspiling here
])
module.exports = withPlugins(
[withTM, withFonts, withImages, [withExpo, { projectRoot: __dirname }]],
{
// ...
}
)
All done! Run yarn next dev
& open http://localhost:3000 👻
You can add other packages that need transpiling to the transpileModules
array. See this post for details.
Usage
Replace the following instances in your code after installation and setup:
useNavigation
👉 useRouting
-import { useNavigation } from 'react-navigation-hooks'
+import { useRouting } from 'expo-next-react-navigation'
useLayoutEffect
-import { useLayoutEffect } from 'react-navigation-hooks'
+import { useLayoutEffect } from 'expo-next-react-navigation'
<TouchableOpacity />
👉 <Link />
-import { TouchableOpacity } from 'react-native'
+import { Link } from 'expo-next-react-navigation'
-<TouchableOpacity onPress={() => navigate({ routeName: 'chat' })}>
- <Text>Go</Text>
- </TouchableOpacity>
+<Link routeName="chat" params={{ roomId: 'hey!' }}>
+ Go
+</Link>
All set ⚡️
API
useRouting
React hook that wraps useNavigation
(from react-navigation) hook and useRouter
(from next-router).
It follows the same API as useNavigation
.
import { useRouting } from 'expo-next-react-navigation`
export default function App() {
const { navigate, push, getParam, goBack } = useRouting()
}
navigate
Only argument is a dictionary with these values. Unlike react-navigation
, this doesn't currently support a string as argument.
routeName
: string, requiredparams
: optional dictionaryweb
: Optional dictionary with added values for web, following the API from next/router
's Router.push
function.
path
: (optional) Fulfills the same value as pathname
from next/router
, overriding the routeName
field. If you set this to /cars
, it will navigate to /cars
instead of the routeName
field. As a result, it will load the file located at pages/cars.js
.as
: (optional) If set, the browser will show this value in the address bar. Useful if you want to show a pretty/custom URL in the address bar that doesn't match the actual path. Unlike the path
field, this does not affect which route you actually go to.shallow
: Update the path of the current page without rerunning getStaticProps, getServerSideProps or getInitialProps. Defaults to false
Example: Navigate to a user
export default function Home() {
const { navigate } = useRouting()
// goes to yourdomain.com/user?id=chris
const onPress = () =>
navigate({
routeName: 'user',
params: { id: 'chris' },
})
// 👇or this👇
// goes to `yourdomain.com/user/chris`
const navigateCleanLink = () =>
navigate({
routeName: 'user',
params: { id: 'chris' },
web: { as: `/user/chris` },
})
// 👇or this👇
// 'profile' path overrides 'user' on web, so it uses the pages/profile.js file
// even though it navigates to yourdomain.com/profile?id=chris?color=blue`
// ...it actually shows up as yourdomain.com/@chris in the URL bar.
const navigateCleanLinkWithParam = () =>
navigate({
routeName: 'user',
params: { id: 'chris', color: 'blue' }, // accessed with getParam in the next screen
web: { as: `/@chris`, path: 'profile' },
})
}
This follows the next pattern of dynamic routing. You'll need to create a pages/user/[id].js
file.
For more thoughts on how and when you should use the web
field, see Web Thoughts.
getParam
Same API as getParam
from react-navigation.
Similar to query
from next/router
, except that it's a function to grab the values.
pages/user/[id].js
Imagine you navigated to yourdomain.com/user/chris
on web using the example above.
export default function User() {
const { getParam } = useRouting()
const id = getParam('id') // chris
// do something with the id
}
useFocusEffect
See react navigation docs. On web, it simply replaces the focus effect with a normal effect hook. On mobile, it is the exact react navigation hook.
Make sure to use useCallback as seen in the example.
import { useFocusEffect } from 'expo-next-react-navigation'
export default ({ userId }) => {
useFocusEffect(
useCallback(() => {
const unsubscribe = API.subscribe(userId, user => setUser(user))
return () => unsubscribe()
}, [userId])
)
return <Profile userId={userId} />
}
Link
The following will use the chat
route in react navigation.
However, it will use the pages/room.js
file for nextjs. Also, it will show up as domain.com/messages
in the address bar.
Optionally accepts a nextLinkProps
prop dictionary and touchableOpacityProps
dictionary as well.
export default function Button() {
return (
<Link
routeName="chat"
params={{ roomId: '12' }}
web={{
path: '/room',
as: 'messages',
}}
>
Chat in room 12
</Link>
)
}
Required props:
Optional props
web
: A dictionary with the follwing options:
type Web = {
path?: string
as?: string
prefetch?: boolean
scroll?: boolean
replace?: boolean
shallow?: boolean
}
-
web
: dictionary, see useRouting().navigate
docs. On v1.0.5
+, you can also pass the prefetch
, replace
, and scroll
booleans here, from the next/link
component.
-
touchableOpacityProps
: extends React Native's TouchableOpacity
props.
-
nextLinkProps
: extends next/router
's Link props.
-
isText
: if false, you can set the children to be non-Text nodes. Defaults to true
. If true
, the children can be a string or a Text
node.
Other shout outs
nextjs-progressbar
I think this is an awesome package for adding a loading progress bar to your next
pages. It's super easy. Check it out.
Link: https://www.npmjs.com/package/nextjs-progressbar
yarn add nextjs-progressbar
or
npm i nextjs-progressbar
pages/_app.js
import React from 'react'
import App from 'next/app'
import NextNprogress from 'nextjs-progressbar'
class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return (
<>
<NextNProgress
color="#29D"
startPosition="0.3"
stopDelayMs="200"
height="3"
/>
<Component {...pageProps} />
</>
)
}
}
export default MyApp
Web Thoughts
The web
prop in the navigate
function and Link
component can help provide cleaner urls (user/mike
instead of user?id=mike
) on web.
Also, navigation patterns on mobile can be different than web, and this field can help you account for those situations.
For instance, imagine you have a tab navigator. Say the first tab has a nested stack navigator with an inbox screen and a chat room screen. If you navigate from a notifications tab to this tab, and a chat room screen was already open, you probably want that chat room to stay open on mobile. Only if you press the tab button a second time should it pop back to the inbox screen.
This may not be the case on web
. Web navigation patterns on web may lead you to want to open the inbox directly, instead of the open chat screen. This example could look something like this:
navigate({
routeName: 'inboxStack',
web: {
path: 'inbox',
},
})
I've also considered letting the web
field take a dynamic
parameter like this chat/:roomId
:
// goes to `yourdomain.com/chat/chris` and still passes `chris` as a `roomId` param
const navigateCleanLink = () =>
navigate({
routeName: 'chat',
params: { roomId: 'chris' },
web: { dynamic: `chat/[roomId]` },
})
// goes to yourdomain.com/chat?roomId=chris
const onPress = () =>
navigate({
routeName: 'chat',
params: { roomId: 'chris' },
})
But that's not added. For now, the same is achieved by doing this:
const roomId = 'chris'
const navigateToChatRoom = () =>
navigate({
routeName: 'chat',
params: { roomId },
web: { path: `chat/${roomId}` },
})
This would open the pages/chat/[roomId].js
file, with roomId
as a param.