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.
This gist 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:
type Query = {
bookingId: string
template: 'story' | 'square'
}
The values of
Query
must be primitives, since you can't use nested fields in a URL with next router.
Next, we're going to generate our useParam
function:
type Query = {
bookingId: string
template: 'story' | 'square'
}
const { useParam } = createParams<Query>()
This usage of a factory is similar to react-navigation
's createStackNavigator
. It allows us to have great TypeScript safety.
Now that we've created our useParams
function, call it in your component:
type Query = {
bookingId: string
template: 'story' | 'square'
}
const { useParam } = createParams<Query>()
export function App() {
const [bookingId, setBookingId] = useParams('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.
With React state, we pass an initial value like this:
const [selected, setSelected] = useState(true)
With useParam
we achieve the same thing with the initial
property:
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:
- the query param (in this case,
template
) isundefined
- 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.
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:
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:
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
:
type Query = {
bookingId: string
template: 'story' | 'square'
}
const { useParam } = createParams<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:
- the query param (in this case,
template
) is notundefined
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.
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:
type Query = {
bookingId: string
template: 'story' | 'square'
}
const { useParam } = createParams<Query>()
const [bookingId, setBookingId] = useParam('bookingId', {
stringify: (bookingId) => {
// if we call setBookingId('123')
// URL will be ?bookingId=artist-123
return `artist-${bookingId}`
},
parse: (bookingIdFromUrl) => {
return bookingIdFromUrl.replate('artist-', '')
}
})