Skip to content

Commit 77d2da1

Browse files
committed
feat: Link function as a child
Implements #50
1 parent b407dbc commit 77d2da1

File tree

4 files changed

+80
-61
lines changed

4 files changed

+80
-61
lines changed

docs/src/pages/docs/api.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export type ReactLocationOptions<TGenerics> = {
1919
basepath?: string
2020
stringifySearch?: SearchSerializer
2121
parseSearch?: SearchParser
22-
options?: RouterOptions<TGenerics>
2322
}
2423

2524
// These options are also available to pass to the <Router /> component
@@ -71,6 +70,11 @@ export type RouterProps<TGenerics> = {
7170
defaultLinkPreloadMaxAge?: number
7271
defaultLoaderMaxAge?: number
7372
useErrorBoundary?: boolean
73+
defaultElement?: SyncOrAsyncElement<TGenerics>
74+
defaultErrorElement?: SyncOrAsyncElement<TGenerics>
75+
defaultPendingElement?: SyncOrAsyncElement<TGenerics>
76+
defaultPendingMs?: number
77+
defaultPendingMinMs?: number
7478
// An array of route match objects that have been both _matched_ and _loaded_. See the [SRR](#ssr) section for more details
7579
initialMatches?: Match<TGenerics>[]
7680
}

examples/basic/src/index.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,7 @@ type LocationGenerics = MakeGenerics<{
8080
//
8181

8282
// Set up a ReactLocation instance
83-
const location = new ReactLocation<LocationGenerics>({
84-
options: {
85-
defaultPendingElement: <Spinner />,
86-
// searchFilter: (prev, next) => { // TODO: implement this
87-
// return {
88-
// ...prev,
89-
// ...next,
90-
// };
91-
// },
92-
},
93-
});
83+
const location = new ReactLocation<LocationGenerics>();
9484

9585
// Build our routes. We could do this in our component, too.
9686
const routes: Route<LocationGenerics>[] = [
@@ -129,6 +119,16 @@ const routes: Route<LocationGenerics>[] = [
129119
users: await fetchUsers(),
130120
};
131121
},
122+
// searchFilters: [ // TODO: Coming soon!
123+
// // Keep the usersView search param around while in this route
124+
// (prev, next) => ({
125+
// ...next,
126+
// usersView: {
127+
// ...prev.usersView,
128+
// ...next.usersView,
129+
// },
130+
// }),
131+
// ],
132132
children: [
133133
{
134134
path: ":userId",
@@ -252,6 +252,7 @@ function App() {
252252
<Router
253253
location={location}
254254
routes={routes}
255+
defaultPendingElement={<Spinner />}
255256
defaultLinkPreloadMaxAge={defaultLinkPreloadMaxAge}
256257
defaultLoaderMaxAge={defaultLoaderMaxAge}
257258
defaultPendingMs={defaultPendingMs}

packages/react-location/size-plugin.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/react-location/src/index.tsx

Lines changed: 62 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212

1313
export { createHashHistory, createBrowserHistory, createMemoryHistory }
1414

15-
//
15+
// Types
1616

1717
declare global {
1818
const __DEV__: boolean
@@ -43,7 +43,7 @@ export type UseGeneric<
4343
? Partial<Maybe<TGenerics[TGeneric], DefaultGenerics[TGeneric]>>
4444
: Maybe<TGenerics[TGeneric], DefaultGenerics[TGeneric]>
4545

46-
export type ReactLocationOptions<TGenerics> = {
46+
export type ReactLocationOptions = {
4747
// The history object to be used internally by react-location
4848
// A history will be created automatically if not provided.
4949
history?: BrowserHistory | MemoryHistory | HashHistory
@@ -52,7 +52,6 @@ export type ReactLocationOptions<TGenerics> = {
5252
basepath?: string
5353
stringifySearch?: SearchSerializer
5454
parseSearch?: SearchParser
55-
options?: RouterOptions<TGenerics>
5655
}
5756

5857
type SearchSerializer = (searchObj: Record<string, any>) => string
@@ -91,6 +90,7 @@ export type RouteBasic<TGenerics extends PartialGenerics = DefaultGenerics> = {
9190
pendingMs?: number
9291
// _If the `pendingElement` is shown_, the minimum duration for which it will be visible.
9392
pendingMinMs?: number
93+
searchFilters?: SearchFilter<TGenerics>[]
9494
// An array of child routes
9595
children?: Route<TGenerics>[]
9696
import?: never
@@ -114,6 +114,11 @@ export type MatchLocation<TGenerics extends PartialGenerics = DefaultGenerics> =
114114

115115
export type SearchPredicate<TSearch> = (search: TSearch) => any
116116

117+
export type SearchFilter<TGenerics> = (
118+
prev: UseGeneric<TGenerics, 'Search'>,
119+
next: UseGeneric<TGenerics, 'Search'>,
120+
) => UseGeneric<TGenerics, 'Search'>
121+
117122
export type SyncOrAsyncElement<
118123
TGenerics extends PartialGenerics = DefaultGenerics,
119124
> = React.ReactNode | AsyncElement<TGenerics>
@@ -195,6 +200,7 @@ export type BuildNextOptions<
195200
hash?: Updater<string>
196201
from?: Partial<Location<TGenerics>>
197202
key?: string
203+
__searchFilters?: SearchFilter<TGenerics>[]
198204
}
199205

200206
export type NavigateOptions<TGenerics> = BuildNextOptions<TGenerics> & {
@@ -208,7 +214,7 @@ export type PromptProps = {
208214
}
209215

210216
export type LinkProps<TGenerics extends PartialGenerics = DefaultGenerics> =
211-
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & {
217+
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'children'> & {
212218
// The absolute or relative destination pathname
213219
to?: string | number | null
214220
// The new search object or a function to update it
@@ -223,6 +229,9 @@ export type LinkProps<TGenerics extends PartialGenerics = DefaultGenerics> =
223229
activeOptions?: ActiveOptions
224230
// If set, will preload the linked route on hover and cache it for this many milliseconds in hopes that the user will eventually navigate there.
225231
preload?: number
232+
children?:
233+
| React.ReactNode
234+
| ((state: { isActive: boolean }) => React.ReactNode)
226235
}
227236

228237
type ActiveOptions = {
@@ -283,7 +292,15 @@ export type FilterRoutesFn = <
283292
routes: Route<TGenerics>[],
284293
) => Route<TGenerics>[]
285294

286-
//
295+
export type RouterPropsType<
296+
TGenerics extends PartialGenerics = DefaultGenerics,
297+
> = RouterProps<TGenerics>
298+
299+
export type RouterType<TGenerics extends PartialGenerics = DefaultGenerics> = (
300+
props: RouterProps<TGenerics>,
301+
) => JSX.Element
302+
303+
// Source
287304

288305
const LocationContext = React.createContext<ReactLocation<any>>(null!)
289306
const MatchesContext = React.createContext<Match<any>[]>(null!)
@@ -307,17 +324,6 @@ export class ReactLocation<
307324
basepath: string
308325
stringifySearch: SearchSerializer
309326
parseSearch: SearchParser
310-
options: {
311-
filterRoutes: FilterRoutesFn
312-
defaultLinkPreloadMaxAge: number
313-
defaultLoaderMaxAge: number
314-
useErrorBoundary: boolean
315-
defaultElement?: SyncOrAsyncElement<TLocationGenerics>
316-
defaultErrorElement?: SyncOrAsyncElement<TLocationGenerics>
317-
defaultPendingElement?: SyncOrAsyncElement<TLocationGenerics>
318-
defaultPendingMs?: number
319-
defaultPendingMinMs?: number
320-
}
321327

322328
current: Location<TLocationGenerics>
323329
destroy: () => void
@@ -329,18 +335,11 @@ export class ReactLocation<
329335
listeners: ListenerFn[] = []
330336
isTransitioning: boolean = false
331337

332-
constructor(options?: ReactLocationOptions<TLocationGenerics>) {
338+
constructor(options?: ReactLocationOptions) {
333339
this.history = options?.history || createDefaultHistory()
334340
this.basepath = options?.basepath || '/'
335341
this.stringifySearch = options?.stringifySearch ?? defaultStringifySearch
336342
this.parseSearch = options?.parseSearch ?? defaultParseSearch
337-
this.options = {
338-
...options?.options,
339-
filterRoutes: options?.options?.filterRoutes ?? ((d) => d),
340-
defaultLinkPreloadMaxAge: options?.options?.defaultLinkPreloadMaxAge ?? 0,
341-
defaultLoaderMaxAge: options?.options?.defaultLoaderMaxAge ?? 0,
342-
useErrorBoundary: options?.options?.useErrorBoundary ?? false,
343-
}
344343

345344
this.current = this.parseLocation(this.history.location)
346345

@@ -373,13 +372,21 @@ export class ReactLocation<
373372
}
374373

375374
const pathname = resolvePath(from.pathname, `${dest.to ?? '.'}`)
375+
376376
const updatedSearch =
377-
dest.search === true
377+
(dest.search === true
378378
? from.search
379-
: functionalUpdate(dest.search, from.search)
380-
const search = updatedSearch
381-
? replaceEqualDeep(from.search, updatedSearch)
382-
: {}
379+
: functionalUpdate(dest.search, from.search)) ?? {}
380+
381+
const filteredSearch = dest.__searchFilters?.length
382+
? dest.__searchFilters.reduce(
383+
(prev, next) => next(prev, updatedSearch),
384+
updatedSearch,
385+
)
386+
: updatedSearch
387+
388+
const search = replaceEqualDeep(from.search, filteredSearch)
389+
383390
const searchStr = this.stringifySearch(search)
384391
let hash = functionalUpdate(dest.hash, from.hash)
385392
hash = hash ? `#${hash}` : ''
@@ -445,6 +452,7 @@ export class ReactLocation<
445452
previousLocation?: Location<TGenerics>,
446453
): Location<TGenerics> {
447454
const parsedSearch = this.parseSearch(location.search)
455+
448456
return {
449457
pathname: location.pathname,
450458
searchStr: location.search,
@@ -456,14 +464,6 @@ export class ReactLocation<
456464
}
457465
}
458466

459-
export type RouterPropsType<
460-
TGenerics extends PartialGenerics = DefaultGenerics,
461-
> = RouterProps<TGenerics>
462-
463-
export type RouterType<TGenerics extends PartialGenerics = DefaultGenerics> = (
464-
props: RouterProps<TGenerics>,
465-
) => JSX.Element
466-
467467
export function Router<TGenerics extends PartialGenerics = DefaultGenerics>({
468468
children,
469469
location,
@@ -485,16 +485,11 @@ function RouterInner<TGenerics extends PartialGenerics = DefaultGenerics>({
485485
if (!matchCacheRef.current) matchCacheRef.current = {}
486486
const matchCache = matchCacheRef.current
487487

488-
const options = {
489-
...location.options,
490-
...rest,
491-
}
492-
493488
const [state, setState] = React.useState<RouterState<TGenerics>>({
494489
transition: {
495490
status: 'ready',
496491
location: location.current,
497-
matches: options.initialMatches ?? [],
492+
matches: rest.initialMatches ?? [],
498493
},
499494
})
500495

@@ -573,7 +568,7 @@ function RouterInner<TGenerics extends PartialGenerics = DefaultGenerics>({
573568
const router = React.useMemo(
574569
(): Router<TGenerics> => ({
575570
...state,
576-
...options,
571+
...rest,
577572
__: {
578573
matchCache,
579574
setState: setState,
@@ -631,7 +626,7 @@ function RouterInner<TGenerics extends PartialGenerics = DefaultGenerics>({
631626
})
632627
})
633628

634-
matchLoader.loadData({ maxAge: options.defaultLoaderMaxAge })
629+
matchLoader.loadData({ maxAge: rest.defaultLoaderMaxAge })
635630
matchLoader.startPending()
636631
} catch (err) {
637632
console.error(err)
@@ -1132,6 +1127,23 @@ export type LinkType<TGenerics extends PartialGenerics = DefaultGenerics> = (
11321127
props: LinkProps<TGenerics>,
11331128
) => JSX.Element
11341129

1130+
// export function useParentMatches<TGenerics>()
1131+
// {
1132+
// const router = useRouter<TGenerics>()
1133+
// const match = useMatch<TGenerics>()
1134+
// const matchIndex = router.transition.matches.findIndex(d => d.id === match.id)
1135+
// return router.transition.matches.slice(0, matchIndex)
1136+
// }
1137+
1138+
// export function useSearchFilters<TGenerics>() {
1139+
// const router = useRouter<TGenerics>()
1140+
// const match = useMatch<TGenerics>()
1141+
// const matchIndex = router.transition.matches.findIndex(
1142+
// (d) => d.id === match.id,
1143+
// )
1144+
// return router.transition.matches.slice(0, matchIndex)
1145+
// }
1146+
11351147
export const Link = React.forwardRef(function Link<
11361148
TGenerics extends PartialGenerics = DefaultGenerics,
11371149
>(
@@ -1175,6 +1187,7 @@ export const Link = React.forwardRef(function Link<
11751187
search,
11761188
hash,
11771189
from: { pathname: match.pathname },
1190+
// __searchFilters:
11781191
})
11791192

11801193
// The click handler
@@ -1229,14 +1242,14 @@ export const Link = React.forwardRef(function Link<
12291242
const hashTest = activeOptions?.includeHash ? hashIsEqual : true
12301243

12311244
// The final "active" test
1232-
const isCurrent = pathTest && hashTest
1245+
const isActive = pathTest && hashTest
12331246

12341247
// Get the active props
12351248
const {
12361249
style: activeStyle = {},
12371250
className: activeClassName = '',
12381251
...activeRest
1239-
} = isCurrent ? getActiveProps() : {}
1252+
} = isActive ? getActiveProps() : {}
12401253

12411254
return (
12421255
<a
@@ -1254,7 +1267,8 @@ export const Link = React.forwardRef(function Link<
12541267
[className, activeClassName].filter(Boolean).join(' ') || undefined,
12551268
...rest,
12561269
...activeRest,
1257-
children,
1270+
children:
1271+
typeof children === 'function' ? children({ isActive }) : children,
12581272
}}
12591273
/>
12601274
)

0 commit comments

Comments
 (0)