Skip to content

test(rsc): add navigation example #567

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6e552c8
Copied sample over from vite-plugins
grahammendick Jul 13, 2025
4be80b7
chore: cleanup unused
hi-ogawa Jul 14, 2025
73efed8
fix: tweak deps optimization
hi-ogawa Jul 14, 2025
7afc7cf
chore: cleanup
hi-ogawa Jul 14, 2025
55d8b13
chore: cleanup
hi-ogawa Jul 14, 2025
6451c74
Overrode vite hydrate for custom nav/context
grahammendick Jul 14, 2025
d001dc0
Ignored ts error
grahammendick Jul 14, 2025
215c9ca
Removed redundant form state
grahammendick Jul 14, 2025
8977949
Renamed for clarity
grahammendick Jul 14, 2025
3716eba
Used jsx for simplicity
grahammendick Jul 14, 2025
af43b06
Tweaked format
grahammendick Jul 14, 2025
33a8401
Changed for consistency with other nav rsc samples
grahammendick Jul 14, 2025
c99ea1f
Implemented rsc fetch
grahammendick Jul 18, 2025
e522409
Changed for consistency
grahammendick Jul 18, 2025
bb532b4
Implemented hmr path
grahammendick Jul 18, 2025
f5fd71f
Moved for clarity
grahammendick Jul 18, 2025
fbe0bbf
Changed for consistency
grahammendick Jul 18, 2025
ca31fcc
Swapped for clarity
grahammendick Jul 18, 2025
de4dd35
Removed internal imports
grahammendick Jul 18, 2025
8c69e5e
Changed to match other navigation rsc samples
grahammendick Jul 18, 2025
846c737
Removed for simplicity
grahammendick Jul 18, 2025
d0a4b59
Prevented error for favicon/well-known requests
grahammendick Jul 18, 2025
c8bfd82
Removed redundant param
grahammendick Jul 18, 2025
8a8b362
Merge remote-tracking branch 'upstream/main' into navigation-vite
grahammendick Jul 21, 2025
f37e50f
Merge branch 'main' into navigation-vite
hi-ogawa Jul 22, 2025
3ad94b4
chore: rename example
hi-ogawa Jul 22, 2025
d654680
Merge branch 'navigation-vite' of https://github.com/grahammendick/vi…
grahammendick Jul 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/plugin-rsc/examples/navigation/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@vitejs/plugin-rsc-examples-navigation",
"private": true,
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@vitejs/plugin-rsc": "latest",
"navigation": "^6.3.0",
"navigation-react": "^4.12.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "latest",
"vite": "^7.0.2"
}
}
Binary file not shown.
29 changes: 29 additions & 0 deletions packages/plugin-rsc/examples/navigation/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { SceneView } from 'navigation-react'
import NavigationProvider from './NavigationProvider'
import HmrProvider from './HmrProvider'
import People from './People'
import Person from './Person'

const App = async ({ url }: any) => {
return (
<html>
<head>
<title>Navigation React</title>
</head>
<body>
<NavigationProvider url={url}>
<HmrProvider>
<SceneView active="people">
<People />
</SceneView>
<SceneView active="person" refetch={['id']}>
<Person />
</SceneView>
</HmrProvider>
</NavigationProvider>
</body>
</html>
)
}

export default App;
38 changes: 38 additions & 0 deletions packages/plugin-rsc/examples/navigation/src/Filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client'
import { startTransition, useOptimistic } from 'react'
import { RefreshLink, useNavigationEvent } from 'navigation-react'

const Filter = () => {
const { data, stateNavigator } = useNavigationEvent()
const { name } = data
const [optimisticName, setOptimisticName] = useOptimistic(
name || '',
(_, newName) => newName,
)
return (
<div>
<div>
<label htmlFor="name">Name</label>
<input
id="name"
value={optimisticName}
onChange={({ target: { value } }) => {
startTransition(() => {
setOptimisticName(value)
stateNavigator.refresh({ ...data, name: value, page: null })
})
}}
/>
</div>
Page size
<RefreshLink navigationData={{ size: 5, page: null }} includeCurrentData>
5
</RefreshLink>
<RefreshLink navigationData={{ size: 10, page: null }} includeCurrentData>
10
</RefreshLink>
</div>
)
}

export default Filter
34 changes: 34 additions & 0 deletions packages/plugin-rsc/examples/navigation/src/Friends.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { RefreshLink, useNavigationEvent } from 'navigation-react'
import { getFriends } from './data'
import Gender from './Gender'

const Friends = async () => {
const {
data: { show, id, gender },
} = useNavigationEvent()
const friends = show ? await getFriends(id, gender) : null
return (
<>
<RefreshLink
navigationData={{ show: !show }}
includeCurrentData
>{`${!show ? 'Show' : 'Hide'} Friends`}</RefreshLink>
{show && (
<>
<Gender />
<ul>
{friends?.map(({ id, name }) => (
<li key={id}>
<RefreshLink navigationData={{ id }} includeCurrentData>
{name}
</RefreshLink>
</li>
))}
</ul>
</>
)}
</>
)
}

export default Friends
35 changes: 35 additions & 0 deletions packages/plugin-rsc/examples/navigation/src/Gender.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client'
import { startTransition } from 'react'
import { useNavigationEvent } from 'navigation-react'
import { useOptimistic } from 'react'

const Gender = () => {
const { data, stateNavigator } = useNavigationEvent()
const { gender } = data
const [optimisticGender, setOptimisticGender] = useOptimistic(
gender || '',
(_, newGender) => newGender,
)
return (
<div>
<label htmlFor="gender">Gender</label>
<select
id="gender"
value={optimisticGender}
onChange={({ target: { value } }) => {
startTransition(() => {
setOptimisticGender(value)
stateNavigator.refresh({ ...data, gender: value })
})
}}
>
<option value=""></option>
<option value="male">Male</option>
<option value="female">Female</option>
<option value="other">Other</option>
</select>
</div>
)
}

export default Gender
42 changes: 42 additions & 0 deletions packages/plugin-rsc/examples/navigation/src/HmrProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client'
import { useContext, useEffect } from 'react'
import { BundlerContext, useNavigationEvent } from 'navigation-react'

const HmrProvider = ({ children }: any) => {
const { setRoot, deserialize } = useContext(BundlerContext)
const { stateNavigator } = useNavigationEvent()
useEffect(() => {
const onHmrReload = () => {
const {
stateContext: {
state,
data,
crumbs,
nextCrumb: { crumblessUrl },
},
} = stateNavigator
const root = deserialize(
stateNavigator.historyManager.getHref(crumblessUrl),
{
method: 'put',
headers: { 'Content-Type': 'application/json' },
body: {
crumbs: crumbs.map(({ state, data }) => ({
state: state.key,
data,
})),
state: state.key,
data,
},
},
)
stateNavigator.historyManager.stop()
setRoot(root)
}
import.meta.hot?.on("rsc:update", onHmrReload);
return () => import.meta.hot?.off("rsc:update", onHmrReload);
})
return children
}

export default HmrProvider
23 changes: 23 additions & 0 deletions packages/plugin-rsc/examples/navigation/src/NavigationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client'
import { useMemo } from 'react'
import { StateNavigator, HTML5HistoryManager } from 'navigation'
import { NavigationHandler } from 'navigation-react'
import stateNavigator from './stateNavigator'

const historyManager = new HTML5HistoryManager()

const NavigationProvider = ({ url, children }: any) => {
const clientNavigator = useMemo(() => {
historyManager.stop()
const clientNavigator = new StateNavigator(stateNavigator, historyManager)
clientNavigator.navigateLink(url)
return clientNavigator
}, [])
return (
<NavigationHandler stateNavigator={clientNavigator}>
{children}
</NavigationHandler>
)
}

export default NavigationProvider
64 changes: 64 additions & 0 deletions packages/plugin-rsc/examples/navigation/src/Pager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { RefreshLink, useNavigationEvent } from 'navigation-react'

const Pager = ({ totalRowCount }: { totalRowCount: number }) => {
const {
data: { page, size },
} = useNavigationEvent()
const lastPage = Math.ceil(totalRowCount / size)
return (
<div>
<ul>
{totalRowCount ? (
<>
<li>
<RefreshLink
navigationData={{ page: 1 }}
includeCurrentData
disableActive
>
First
</RefreshLink>
</li>
<li>
<RefreshLink
navigationData={{ page: Math.max(page - 1, 1) }}
includeCurrentData
disableActive
>
Previous
</RefreshLink>
</li>
<li>
<RefreshLink
navigationData={{ page: Math.min(lastPage, page + 1) }}
includeCurrentData
disableActive
>
Next
</RefreshLink>
</li>
<li>
<RefreshLink
navigationData={{ page: lastPage }}
includeCurrentData
disableActive
>
Last
</RefreshLink>
</li>
</>
) : (
<>
<li>First</li>
<li>Previous</li>
<li>Next</li>
<li>Last</li>
</>
)}
</ul>
Total Count {totalRowCount}
</div>
)
}

export default Pager
51 changes: 51 additions & 0 deletions packages/plugin-rsc/examples/navigation/src/People.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { searchPeople } from './data'
import {
NavigationLink,
RefreshLink,
useNavigationEvent,
} from 'navigation-react'
import Filter from './Filter'
import Pager from './Pager'

const People = async () => {
const {
data: { name, page, size, sort },
} = useNavigationEvent()
const { people, count } = await searchPeople(name, page, size, sort)
return (
<>
<h1>People</h1>
<Filter />
<table>
<thead>
<tr>
<th>
<RefreshLink
navigationData={{ sort: sort === 'asc' ? 'desc' : 'asc' }}
includeCurrentData
>
Name
</RefreshLink>
</th>
<th>Date of Birth</th>
</tr>
</thead>
<tbody>
{people.map(({ id, name, dateOfBirth }) => (
<tr key={id}>
<td>
<NavigationLink stateKey="person" navigationData={{ id: id }}>
{name}
</NavigationLink>
</td>
<td>{dateOfBirth}</td>
</tr>
))}
</tbody>
</table>
<Pager totalRowCount={count} />
</>
)
}

export default People
34 changes: 34 additions & 0 deletions packages/plugin-rsc/examples/navigation/src/Person.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
SceneView,
NavigationBackLink,
useNavigationEvent,
} from 'navigation-react'
import { getPerson } from './data'
import Friends from './Friends'

const Person = async () => {
const { data } = useNavigationEvent()
const { name, dateOfBirth, email, phone } = await getPerson(data.id)
return (
<>
<h1>Person</h1>
<div>
<NavigationBackLink distance={1}>Person Search</NavigationBackLink>
<div>
<h2>{name}</h2>
<div>Date of Birth</div>
<div>{dateOfBirth}</div>
<div>Email</div>
<div>{email}</div>
<div>Phone</div>
<div>{phone}</div>
</div>
<SceneView active="person" name="friends">
<Friends />
</SceneView>
</div>
</>
)
}

export default Person
Loading