Last active
February 17, 2025 08:31
-
-
Save nandorojo/052887f99bb61b54845474f324aa41cc to your computer and use it in GitHub Desktop.
Revisions
-
nandorojo revised this gist
Dec 8, 2021 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -110,7 +110,7 @@ export function createParam< const { parse, initial, stringify = (value: ParsedType) => `${value}` } = maybeConfig || {} const [nativeState, setNativeState] = useState<ParsedType | InitialValue>( router?.getParam(name as string) ?? (initial as InitialValue) ) const router = useRouting() -
nandorojo revised this gist
Nov 2, 2021 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -110,7 +110,7 @@ export function createParam< const { parse, initial, stringify = (value: ParsedType) => `${value}` } = maybeConfig || {} const [nativeState, setNativeState] = useState<ParsedType | InitialValue>( (initial as InitialValue) ?? router?.getParam(name as string) ) const router = useRouting() -
nandorojo revised this gist
Nov 2, 2021 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -125,7 +125,7 @@ export function createParam< hasSetState.current = true const { pathname, query } = Router const newQuery = { ...query } if (value != null && (value as any) !== '') { newQuery[name as string] = stableStringify.current(value) } else { delete newQuery[name as string] -
nandorojo revised this gist
Sep 9, 2021 . 1 changed file with 8 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -123,15 +123,20 @@ export function createParam< const setState = useCallback( (value: ParsedType) => { hasSetState.current = true const { pathname, query } = Router const newQuery = { ...query } if (value != null) { newQuery[name as string] = stableStringify.current(value) } else { delete newQuery[name as string] } const willChangeExistingParam = query[name as string] && newQuery[name as string] const action = willChangeExistingParam ? Router.replace : Router.push action( { pathname, query: newQuery, -
nandorojo revised this gist
Sep 9, 2021 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -160,10 +160,10 @@ export function createParam< }, [stableParse, webParam]) if (Platform.OS !== 'web') { return [nativeState, setNativeState] } return [state, setState] } return { -
nandorojo revised this gist
Sep 9, 2021 . 1 changed file with 4 additions and 6 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -12,6 +12,7 @@ function useStable<T>(value: T) { return ref } type Config< Required extends boolean, ParsedType, @@ -30,9 +31,8 @@ type Config< type Params< Props extends Record<string, unknown> = Record<string, string>, Name extends keyof Props = keyof Props, NullableUnparsedParsedType extends Props[Name] | undefined = | Props[Name] | undefined, ParseFunction extends | undefined @@ -52,9 +52,8 @@ type Params< type Returns< Props extends Record<string, unknown> = Record<string, string>, Name extends keyof Props = keyof Props, NullableUnparsedParsedType extends Props[Name] | undefined = | Props[Name] | undefined, ParseFunction extends | undefined @@ -77,9 +76,8 @@ export function createParam< >() { function useParam< Name extends keyof Props, NullableUnparsedParsedType extends Props[Name] | undefined = | Props[Name] | undefined, ParseFunction extends | undefined -
nandorojo revised this gist
Sep 9, 2021 . 1 changed file with 10 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,8 +1,16 @@ import { useCallback, useMemo, useRef, useState, useEffect } from 'react' import { useRouting } from 'expo-next-react-navigation' import { Platform } from 'react-native' import Router from 'next/router' function useStable<T>(value: T) { const ref = useRef(value) useEffect(function update() { ref.current = value }, [value]) return ref } type Config< Required extends boolean, -
nandorojo revised this gist
Sep 9, 2021 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -136,7 +136,7 @@ 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` -
nandorojo revised this gist
Sep 9, 2021 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -80,7 +80,7 @@ There is might appear to be an edge case here. What happens if you call `setTemp 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 -
nandorojo revised this gist
Sep 9, 2021 . 1 changed file with 3 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -78,7 +78,9 @@ However, on web, this might not aways be the initial value. This is because the 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 tbe `parse` field. ### Parsing values -
nandorojo revised this gist
Sep 9, 2021 . 1 changed file with 4 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -76,6 +76,10 @@ However, on web, this might not aways be the initial value. This is because the 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. The solution is 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. -
nandorojo revised this gist
Sep 9, 2021 . 1 changed file with 4 additions and 4 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -27,7 +27,7 @@ type Query = { 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. @@ -42,7 +42,7 @@ type Query = { template: 'story' | 'square' } const { useParam } = createParams<Query>() export function App() { const [bookingId, setBookingId] = useParams('bookingId') @@ -111,7 +111,7 @@ type Query = { template: 'story' | 'square' } const { useParam } = createParams<Query>() const [template, setTemplate] = useParam('template', { initial: 'story', @@ -152,7 +152,7 @@ type Query = { template: 'story' | 'square' } const { useParam } = createParams<Query>() const [bookingId, setBookingId] = useParam('bookingId', { stringify: (bookingId) => { -
nandorojo revised this gist
Sep 9, 2021 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,8 +1,8 @@ # Expo + Next.js Query Params State 🦦 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. -
nandorojo revised this gist
Sep 9, 2021 . 1 changed file with 2 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,5 @@ # Expo + Next.js Query Params State 🦦 A typical use-case on web for maintaining state is the URL's query parameters. URL as state is especially useful on Next.js, since `next/router` will re-render your page with shallow navigation. -
nandorojo created this gist
Sep 9, 2021 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,165 @@ A typical use-case on web for maintaining state is the URL's query parameters. 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: ```ts 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: ```ts type Query = { bookingId: string template: 'story' | 'square' } const { useParams } = createParams<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 `useParams` function, call it in your component: ```ts type Query = { bookingId: string template: 'story' | 'square' } const { useParams } = 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. ### Initial value With React state, we pass an initial value like this: ```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' }) ``` 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`) ### 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' } ``` 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' }) ``` 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 { useParams } = 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 are satisfied: 1. the query param (in this case, `template`) is not `undefined` ## 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 { useParams } = 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-', '') } }) ``` This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,166 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { useRouting } from 'expo-next-react-navigation' import { Platform } from 'react-native' import Router from 'next/router' import useStable from '@beatgig/design/hooks/use-stable' type Config< Required extends boolean, ParsedType, InitialValue > = (Required extends false ? { parse?: (value?: string) => ParsedType } : { parse: (value?: string) => ParsedType }) & { stringify?: (value: ParsedType) => string initial: InitialValue } type Params< Props extends Record<string, unknown> = Record<string, string>, Name extends keyof Props = keyof Props, NullableUnparsedParsedType extends Props[Name] | null | undefined = | Props[Name] | null | undefined, ParseFunction extends | undefined | ((value?: string) => NonNullable<NullableUnparsedParsedType>) = ( value?: string ) => NonNullable<NullableUnparsedParsedType>, InitialValue = NullableUnparsedParsedType | undefined, ParsedType = InitialValue extends undefined ? NullableUnparsedParsedType : ParseFunction extends undefined ? NullableUnparsedParsedType : NonNullable<NullableUnparsedParsedType> > = NonNullable<ParsedType> extends string ? [name: Name, config: Config<false, ParsedType, InitialValue>] | [name: Name] : [name: Name, config: Config<true, ParsedType, InitialValue>] type Returns< Props extends Record<string, unknown> = Record<string, string>, Name extends keyof Props = keyof Props, NullableUnparsedParsedType extends Props[Name] | null | undefined = | Props[Name] | null | undefined, ParseFunction extends | undefined | ((value?: string) => NonNullable<NullableUnparsedParsedType>) = ( value?: string ) => NonNullable<NullableUnparsedParsedType>, InitialValue = NullableUnparsedParsedType | undefined, ParsedType = InitialValue extends undefined ? NullableUnparsedParsedType : ParseFunction extends undefined ? NullableUnparsedParsedType : NonNullable<NullableUnparsedParsedType> > = readonly [ state: ParsedType | InitialValue, setState: (value: ParsedType) => void ] export function createParam< Props extends Record<string, unknown> = Record<string, string> >() { function useParam< Name extends keyof Props, NullableUnparsedParsedType extends Props[Name] | null | undefined = | Props[Name] | null | undefined, ParseFunction extends | undefined | ((value?: string) => NonNullable<NullableUnparsedParsedType>) = ( value?: string ) => NonNullable<NullableUnparsedParsedType>, InitialValue = NullableUnparsedParsedType | undefined, ParsedType = InitialValue extends undefined ? NullableUnparsedParsedType : ParseFunction extends undefined ? NullableUnparsedParsedType : NonNullable<NullableUnparsedParsedType> >( ...[name, maybeConfig]: Params< Props, Name, NullableUnparsedParsedType, ParseFunction, InitialValue, ParsedType > ): Returns< Props, Name, NullableUnparsedParsedType, ParseFunction, InitialValue, ParsedType > { const { parse, initial, stringify = (value: ParsedType) => `${value}` } = maybeConfig || {} const [nativeState, setNativeState] = useState<ParsedType | InitialValue>( initial as InitialValue ) const router = useRouting() const stableStringify = useStable(stringify) const stableParse = useStable(parse) const initialValue = useRef(initial) const hasSetState = useRef(false) const setState = useCallback( (value: ParsedType) => { hasSetState.current = true const { pathname, query, asPath } = Router const newQuery = { ...query } if (value !== null) { newQuery[name as string] = stableStringify.current(value) } else { delete newQuery[name as string] } Router.replace( { pathname, query: newQuery, }, undefined, { shallow: true, } ) }, [name, stableStringify] ) const webParam: string | undefined = router.getParam(name as string) const state = useMemo<ParsedType>(() => { let state: ParsedType if (webParam === undefined && !hasSetState.current) { state = initialValue.current as any } else if (stableParse.current) { state = stableParse.current?.(webParam) } else { state = webParam as any } return state }, [stableParse, webParam]) if (Platform.OS !== 'web') { return [nativeState, setNativeState] as const } return [state, setState] as const } return { useParam, } }