generated from chiffre-io/template-library
-
-
Notifications
You must be signed in to change notification settings - Fork 134
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add loader feature for React Routers and Remix
- Loading branch information
Showing
8 changed files
with
339 additions
and
12 deletions.
There are no files selected for viewing
This file contains 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 characters
This file contains 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 characters
This file contains 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 characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,10 @@ | ||
export { createSearchParamsCache } from './cache' | ||
export type { HistoryOptions, Nullable, Options, SearchParams } from './defs' | ||
export { | ||
createLoader, | ||
type LoaderFunction, | ||
type LoaderInput, | ||
type LoaderOptions | ||
} from './loader' | ||
export * from './parsers' | ||
export { createSerializer } from './serializer' |
This file contains 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 characters
This file contains 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 characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import { describe, expect, it } from 'vitest' | ||
import { createLoader } from './loader' | ||
import { parseAsInteger } from './parsers' | ||
|
||
describe('loader', () => { | ||
describe('sync', () => { | ||
it('parses a URL object', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load(new URL('http://example.com/?a=1&b=2')) | ||
expect(result).toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('parses a Request object', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load(new Request('http://example.com/?a=1&b=2')) | ||
expect(result).toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('parses a URLSearchParams object', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load(new URLSearchParams('a=1&b=2')) | ||
expect(result).toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('parses a Record<string, string | string[] | undefined> object', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load({ | ||
a: '1', | ||
b: '2' | ||
}) | ||
expect(result).toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('parses a URL string', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load('https://example.com/?a=1&b=2') | ||
expect(result).toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('parses a search params string', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load('?a=1&b=2') | ||
expect(result).toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('supports urlKeys', () => { | ||
const load = createLoader( | ||
{ | ||
urlKey: parseAsInteger | ||
}, | ||
{ | ||
urlKeys: { | ||
urlKey: 'a' | ||
} | ||
} | ||
) | ||
const result = load('?a=1') | ||
expect(result).toEqual({ | ||
urlKey: 1 | ||
}) | ||
}) | ||
}) | ||
|
||
describe('async', () => { | ||
it('parses a URL object', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load( | ||
Promise.resolve(new URL('http://example.com/?a=1&b=2')) | ||
) | ||
return expect(result).resolves.toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('parses a Request object', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load( | ||
Promise.resolve(new Request('http://example.com/?a=1&b=2')) | ||
) | ||
return expect(result).resolves.toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('parses a URLSearchParams object', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load(Promise.resolve(new URLSearchParams('a=1&b=2'))) | ||
return expect(result).resolves.toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('parses a Record<string, string | string[] | undefined> object', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load( | ||
Promise.resolve({ | ||
a: '1', | ||
b: '2' | ||
}) | ||
) | ||
return expect(result).resolves.toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('parses a URL string', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load(Promise.resolve('https://example.com/?a=1&b=2')) | ||
return expect(result).resolves.toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('parses a search params string', () => { | ||
const load = createLoader({ | ||
a: parseAsInteger, | ||
b: parseAsInteger | ||
}) | ||
const result = load(Promise.resolve('?a=1&b=2')) | ||
return expect(result).resolves.toEqual({ | ||
a: 1, | ||
b: 2 | ||
}) | ||
}) | ||
it('supports urlKeys', () => { | ||
const load = createLoader( | ||
{ | ||
urlKey: parseAsInteger | ||
}, | ||
{ | ||
urlKeys: { | ||
urlKey: 'a' | ||
} | ||
} | ||
) | ||
const result = load(Promise.resolve('?a=1')) | ||
return expect(result).resolves.toEqual({ | ||
urlKey: 1 | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains 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 characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import type { inferParserType, ParserMap, UrlKeys } from './parsers' | ||
|
||
export type LoaderInput = | ||
| URL | ||
| Request | ||
| URLSearchParams | ||
| Record<string, string | string[] | undefined> | ||
| string | ||
|
||
export type LoaderOptions<Parsers extends ParserMap> = { | ||
urlKeys?: UrlKeys<Parsers> | ||
} | ||
|
||
export type LoaderFunction<Parsers extends ParserMap> = ReturnType< | ||
typeof createLoader<Parsers> | ||
> | ||
|
||
export function createLoader<Parsers extends ParserMap>( | ||
parsers: Parsers, | ||
{ urlKeys = {} }: LoaderOptions<Parsers> = {} | ||
) { | ||
type ParsedSearchParams = inferParserType<Parsers> | ||
|
||
/** | ||
* Load & parse search params from (almost) any input. | ||
* | ||
* While loaders are typically used in the context of a React Router / Remix | ||
* loader function, it can also be used in Next.js API routes or | ||
* getServerSideProps functions, or even with the app router `searchParams` | ||
* page prop (sync or async), if you don't need the cache behaviours. | ||
*/ | ||
function loadSearchParams( | ||
input: LoaderInput, | ||
options?: LoaderOptions<Parsers> | ||
): ParsedSearchParams | ||
|
||
/** | ||
* Load & parse search params from (almost) any input. | ||
* | ||
* While loaders are typically used in the context of a React Router / Remix | ||
* loader function, it can also be used in Next.js API routes or | ||
* getServerSideProps functions, or even with the app router `searchParams` | ||
* page prop (sync or async), if you don't need the cache behaviours. | ||
* | ||
* Note: this async overload makes it easier to use against the `searchParams` | ||
* page prop in Next.js 15 app router: | ||
* | ||
* ```tsx | ||
* export default async function Page({ searchParams }) { | ||
* const parsedSearchParamsPromise = loadSearchParams(searchParams) | ||
* return ( | ||
* // Pre-render & stream the shell immediately | ||
* <StaticShell> | ||
* <Suspense> | ||
* // Stream the Promise down | ||
* <DynamicComponent searchParams={parsedSearchParamsPromise} /> | ||
* </Suspense> | ||
* </StaticShell> | ||
* ) | ||
* } | ||
* ``` | ||
*/ | ||
function loadSearchParams( | ||
input: Promise<LoaderInput>, | ||
options?: LoaderOptions<Parsers> | ||
): Promise<ParsedSearchParams> | ||
|
||
function loadSearchParams(input: LoaderInput | Promise<LoaderInput>) { | ||
if (input instanceof Promise) { | ||
return input.then(i => loadSearchParams(i)) | ||
} | ||
const searchParams = extractSearchParams(input) | ||
const result = {} as any | ||
for (const [key, parser] of Object.entries(parsers)) { | ||
const urlKey = urlKeys[key] ?? key | ||
const value = searchParams.get(urlKey) | ||
result[key] = parser.parseServerSide(value ?? undefined) | ||
} | ||
return result | ||
} | ||
return loadSearchParams | ||
} | ||
|
||
function extractSearchParams(input: LoaderInput): URLSearchParams { | ||
try { | ||
if (input instanceof Request) { | ||
if (input.url) { | ||
return new URL(input.url).searchParams | ||
} else { | ||
return new URLSearchParams() | ||
} | ||
} | ||
if (input instanceof URL) { | ||
return input.searchParams | ||
} | ||
if (input instanceof URLSearchParams) { | ||
return input | ||
} | ||
if (typeof input === 'object') { | ||
const entries = Object.entries(input) | ||
const searchParams = new URLSearchParams() | ||
for (const [key, value] of entries) { | ||
if (Array.isArray(value)) { | ||
for (const v of value) { | ||
searchParams.append(key, v) | ||
} | ||
} else if (value !== undefined) { | ||
searchParams.set(key, value) | ||
} | ||
} | ||
return searchParams | ||
} | ||
if (typeof input === 'string') { | ||
if ('canParse' in URL && URL.canParse(input)) { | ||
return new URL(input).searchParams | ||
} | ||
return new URLSearchParams(input) | ||
} | ||
} catch (e) { | ||
return new URLSearchParams() | ||
} | ||
return new URLSearchParams() | ||
} |
This file contains 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 characters
This file contains 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 characters