Skip to main content

useParam

tsx
import { createParam } from 'solito'
 
const { useParam } = createParam()
tsx
import { createParam } from 'solito'
 
const { useParam } = createParam()

useParam is a hook that lets you read screen parameters on both Next.js and React Native. On Native, it reads React Navigation params, and on Web, it reads query params from next/router.

useParam reads both query parameters and dynamic route parameters. A Next.js dynamic route might look like /artists/[slug].tsx, where slug is a dynamic route. useParam('slug') works there too. On the native side, your linking config would have a URL with /artists/:slug.

It also lets you update the parameter, using query parameters on Web, and React state on iOS/Android.

Video


Learn how to use useParam from Fernando Rojo, creator of Solito, in his Next.js Conf 2021 talk.

Quick look

tsx
import { createParam } from 'solito'
import { Text } from 'react-native'
 
// first, generate the hook
const { useParam } = createParam()
 
export const UserName = () => {
const [username, setUsername] = useParam('username')
 
return <Text>{username}</Text>
}
tsx
import { createParam } from 'solito'
import { Text } from 'react-native'
 
// first, generate the hook
const { useParam } = createParam()
 
export const UserName = () => {
const [username, setUsername] = useParam('username')
 
return <Text>{username}</Text>
}

You'll use useParam() instead of useRoute().params from React Navigation, and instead of useRouter().query from Next.js.

TypeScript

tsx
type Query = { username: string }
 
const { useParam } = createParam<Query>()
 
export const UserName = () => {
const [username, setUsername] = useParam('username')
 
return <Text>{username}</Text>
}
tsx
type Query = { username: string }
 
const { useParam } = createParam<Query>()
 
export const UserName = () => {
const [username, setUsername] = useParam('username')
 
return <Text>{username}</Text>
}

Create custom hooks

Rather than writing useParam directly into your components, you should wrap them into your own hooks.

For example, create your own useUsername hook:

tsx
import { createParam } from 'solito'
 
type Query = { username: string }
 
const { useParam } = createParam<Query>()
 
export const useUsername = () => {
const [username, setUsername] = useParam('username')
 
return {
username,
setUsername,
}
}
tsx
import { createParam } from 'solito'
 
type Query = { username: string }
 
const { useParam } = createParam<Query>()
 
export const useUsername = () => {
const [username, setUsername] = useParam('username')
 
return {
username,
setUsername,
}
}

And if you want to get all the parameters for a given screen, you could do this:

tsx
import { createParam } from 'solito'
 
type UserScreenParams = { username: string; referredBy?: string }
 
const { useParam } = createParam<UserScreenParams>()
 
export const useUserScreenParams = () => {
const [username] = useParam('username')
const [referredBy] = useParam('referredBy')
 
return {
username,
referredBy,
}
}
tsx
import { createParam } from 'solito'
 
type UserScreenParams = { username: string; referredBy?: string }
 
const { useParam } = createParam<UserScreenParams>()
 
export const useUserScreenParams = () => {
const [username] = useParam('username')
const [referredBy] = useParam('referredBy')
 
return {
username,
referredBy,
}
}

Then, use the hook in your screen:

tsx
export const User = () => {
const { username, referredBy } = useUserScreenParams()
// You can fetch the user here
const user = useUser({ username })
return (
<View>
<Text>{username}</Text>
</View>
)
}
tsx
export const User = () => {
const { username, referredBy } = useUserScreenParams()
// You can fetch the user here
const user = useUser({ username })
return (
<View>
<Text>{username}</Text>
</View>
)
}

Compare to React Navigation Usage

Let's compare what this hook does compared to React Navigation, to help you easily migrate.

Before, with React Navigation

js
import { useRoute } from '@react-navigation/native'
 
const useUsername = () => {
const route = useRoute()
const username = route.params?.username
return username
}
js
import { useRoute } from '@react-navigation/native'
 
const useUsername = () => {
const route = useRoute()
const username = route.params?.username
return username
}

After, with Solito

tsx
import { createParam } from 'solito'
 
const { useParam } = createParam()
 
export const useUsername = () => {
const [username] = useParam('username')
 
return username
}
tsx
import { createParam } from 'solito'
 
const { useParam } = createParam()
 
export const useUsername = () => {
const [username] = useParam('username')
 
return username
}

Compare to Next.js Usage

Before, with Next.js

ts
import { useRouter } from 'next/router'
 
const useUsername = () => {
const router = useRouter()
const username = router?.query.username
return username
}
ts
import { useRouter } from 'next/router'
 
const useUsername = () => {
const router = useRouter()
const username = router?.query.username
return username
}

After, with Solito

tsx
import { createParam } from 'solito'
 
const { useParam } = createParam()
 
export const useUsername = () => {
const [username] = useParam('username')
 
return username
}
tsx
import { createParam } from 'solito'
 
const { useParam } = createParam()
 
export const useUsername = () => {
const [username] = useParam('username')
 
return username
}

Motivation

A typical use-case on Web for maintaining React State is your URL's query parameters. It lets users refresh pages & share links without losing their spot in your app.

URL-as-state is especially useful on Next.js, since next/router will re-render your page with shallow navigation.

useParam lets you leverage the power of URL-as-state, while providing a fallback to React state for usage in React Native apps.

It's essentially a replacement for useState.

First, create the schema for your query parameters:

ts
type Query = {
bookingId: string
template: 'story' | 'square'
}
ts
type Query = {
bookingId: string
template: 'story' | 'square'
}

The values of Query must be primitives, such as strings/booleans/numbers, since you can't use nested fields in a URL with next router.

Next, we're going to generate our useParam function:

ts
import { createParam } from 'solito'
 
type Query = {
bookingId: string
template: 'story' | 'square'
}
 
const { useParam } = createParam<Query>()
ts
import { createParam } from 'solito'
 
type Query = {
bookingId: string
template: 'story' | 'square'
}
 
const { useParam } = createParam<Query>()

This usage of a factory is similar to react-navigation's createStackNavigator. It allows us to have great TypeScript safety.

Usage

Now that we've created our useParam function, call it in your component:

ts
type Query = {
bookingId: string
template: 'story' | 'square'
}
 
const { useParam } = createParam<Query>()
 
export function App() {
const [bookingId, setBookingId] = useParam('bookingId')
}
ts
type Query = {
bookingId: string
template: 'story' | 'square'
}
 
const { useParam } = createParam<Query>()
 
export function App() {
const [bookingId, setBookingId] = useParam('bookingId')
}

Whenever you call setBookingId, it will update the query parameter in the URL. To remove the query parameter, call setBookingId(null).

On native, this will function as normal React State.

Initial value

With React state, we pass an initial value like this:

ts
const [selected, setSelected] = useState(true)
ts
const [selected, setSelected] = useState(true)

With useParam we achieve the same thing with the initial property:

ts
const [template, setTemplate] = useParam('template', {
initial: 'story',
})
ts
const [template, setTemplate] = useParam('template', {
initial: 'story',
})

However, on web, this might not aways be the initial value. This is because the initial value itself could be set from the URL on the first navigation.

initial gets used on Web when these two cases are satisfied:

  1. the query param (in this case, template) is undefined
  2. you haven't called the set state function yet (in this case, setTemplate)

There is might appear to be an edge case here. What happens if you call setTemplate(null)? This will remove the query parameter from the URL, so we're left with an empty state. But it also won't fall back to the initial field, since this wouldn't match the React state behavior.

Can we find a way to provide a fallback value on Web in this case, to make sure that our URL isn't the only source of truth?

The solution lies with the parse field.

Parsing values

One issue with having state in URLs is, users have an API to inject whatever state they want into your app.

This could break in many ways.

Take our Query type we wrote earlier:

ts
type Query = {
bookingId: string
template: 'story' | 'square'
}
ts
type Query = {
bookingId: string
template: 'story' | 'square'
}

Our template is a required field that accepts square or story.

A naive approach would use it like this:

ts
const [template, setTemplate] = useParam('template', {
initial: 'story',
})
ts
const [template, setTemplate] = useParam('template', {
initial: 'story',
})

There are two problems here: what if the URL doesn't have template? Or worse, what if it does have template, but it doesn't match one of the types you specified?

Enter parse:

ts
type Query = {
bookingId: string
template: 'story' | 'square'
}
 
const { useParam } = createParam<Query>()
 
const [template, setTemplate] = useParam('template', {
initial: 'story',
parse: (templateFromUrl) => {
if (templateFromUrl === 'story' || templateFromUrl === 'square') {
return templateFromUrl
}
return 'story'
},
})
ts
type Query = {
bookingId: string
template: 'story' | 'square'
}
 
const { useParam } = createParam<Query>()
 
const [template, setTemplate] = useParam('template', {
initial: 'story',
parse: (templateFromUrl) => {
if (templateFromUrl === 'story' || templateFromUrl === 'square') {
return templateFromUrl
}
return 'story'
},
})

parse is the final piece of the puzzle. It lets you ensure that any state you're using from your URL is "safe".

It's also strictly typesafe, which is an added bonus.

The argument it receives will always be a string

parse gets run when this case is satisfied:

  1. the query param (in this case, template) is not undefined
warning

The parse function will only run on Web, not on Native. To configure a parse function on native, you must use the parse option from your React Navigation linking config.

Strict Types

This hook has great strict types.

The state value it returns will always be State | undefined, unless you pass both an initial value and parse. That way, we know that on both Web and native, we're always using values which match our state.

Stringify

It's possible you'll want to customize the way that the query param is stored in the URL.

If so, you can use the stringify property:

ts
type Query = {
bookingId: string
template: 'story' | 'square'
}
const { useParam } = createParam<Query>()
const [bookingId, setBookingId] = useParam('bookingId', {
stringify: (bookingId) => {
// if we call setBookingId('123')
// URL will be ?bookingId=artist-123
return `artist-${bookingId}`
},
parse: (bookingIdFromUrl) => {
// remember that URL params can be arrays
if (Array.isArray(bookingIdFromUrl)) {
return bookingIdFromUrl[0]?.replace('artist-', '')
}
return bookingIdFromUrl?.replace('artist-', '')
},
initial: '',
})
ts
type Query = {
bookingId: string
template: 'story' | 'square'
}
const { useParam } = createParam<Query>()
const [bookingId, setBookingId] = useParam('bookingId', {
stringify: (bookingId) => {
// if we call setBookingId('123')
// URL will be ?bookingId=artist-123
return `artist-${bookingId}`
},
parse: (bookingIdFromUrl) => {
// remember that URL params can be arrays
if (Array.isArray(bookingIdFromUrl)) {
return bookingIdFromUrl[0]?.replace('artist-', '')
}
return bookingIdFromUrl?.replace('artist-', '')
},
initial: '',
})