ui-next: navigation

This commit is contained in:
Baoshuo committed 2026-05-10 01:12:47 +08:00
1 parent efc9294db9
commit f71d641571
6 files changed
+156 -16

No files matched your search

+5 -1
View File
@@ -1,7 +1,11 @@
// Components
export { Link, type LinkProps } from './src/components/link';
// Context
export { type PageData, usePageData } from './src/context/page-data';
export { type NavigationState, type PageData, useNavigationState, usePageData } from './src/context/page-data';
// Hooks
export { useNavigate } from './src/hooks/use-navigate';
export { useUrl } from './src/hooks/use-url';
// Registry
-10
View File
@@ -1,10 +0,0 @@
import { usePageData } from './context/page-data';
import { Component } from './registry';
const AppInner = Component('page:app', () => {
const data = usePageData();
return <div>app, page:{data.name}</div>;
});
export default AppInner;
+18
View File
@@ -0,0 +1,18 @@
import { Link } from './components/link';
import { usePageData } from './context/page-data';
import { Component } from './registry';
const AppInner = Component('page:app', () => {
const data = usePageData();
return (
<>
<div>app, page:{data.name}</div>
<div>
<Link to="homepage">homepage</Link> <Link to="problem_main">problem_main</Link>
</div>
</>
);
});
export default AppInner;
+41
View File
@@ -0,0 +1,41 @@
import React, { useCallback, useMemo } from 'react';
import { useNavigate } from '../hooks/use-navigate';
import { useUrl } from '../hooks/use-url';
export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
/** Pre-built href. Use this or `to`, not both. */
href?: string;
/** Route name to resolve via the route map. */
to?: string;
/** Params for route resolution when `to` is given. */
params?: Record<string, string>;
}
function isModifiedEvent(e: React.MouseEvent): boolean {
return e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey;
}
export const Link: React.FC<React.PropsWithChildren<LinkProps>> = ({ href, to, params, onClick, children, ...rest }) => {
const buildUrl = useUrl();
const navigate = useNavigate();
const resolvedHref = useMemo(() => (to ? buildUrl(to, params) : (href ?? '#')), [buildUrl, href, to, params]);
const handleClick = useCallback(
(e: React.MouseEvent<HTMLAnchorElement>) => {
onClick?.(e);
if (e.defaultPrevented || isModifiedEvent(e)) return;
// Let the browser handle external links and javascript/mailto URIs
if (!resolvedHref.startsWith('/') && !resolvedHref.startsWith(window.location.origin)) return;
e.preventDefault();
navigate(resolvedHref);
},
[onClick, resolvedHref, navigate],
);
return (
<a href={resolvedHref} onClick={handleClick} {...rest}>
{children}
</a>
);
};
+33 -5
View File
@@ -1,3 +1,5 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, type ReactNode, useContext, useMemo, useState } from 'react';
export interface PageData {
@@ -9,11 +11,17 @@ export interface PageData {
[key: string]: any;
}
type PageDataSetter = (data: PageData) => void;
type PageDataSetter = React.Dispatch<React.SetStateAction<PageData>>;
interface PageDataContextValue {
data: PageData;
setData: PageDataSetter;
loading: boolean;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
error: Error | null;
setError: React.Dispatch<React.SetStateAction<Error | null>>;
}
const PageDataContext = createContext<PageDataContextValue | null>(null);
@@ -24,14 +32,34 @@ interface PageDataProviderProps {
export function PageDataProvider({ initial, children }: PageDataProviderProps) {
const [data, setData] = useState<PageData>(initial);
const value = useMemo(() => ({ data, setData }), [data]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const value = useMemo(() => ({ data, setData, loading, setLoading, error, setError }), [data, loading, error]);
return <PageDataContext.Provider value={value}>{children}</PageDataContext.Provider>;
}
// eslint-disable-next-line react-refresh/only-export-components
export function usePageData(): PageData {
function usePageDataContext(): PageDataContextValue {
const ctx = useContext(PageDataContext);
if (!ctx) throw new Error('usePageData must be used within PageDataProvider');
return ctx.data;
return ctx;
}
export function usePageData(): PageData {
return usePageDataContext().data;
}
export interface NavigationState {
loading: PageDataContextValue['loading'];
error: PageDataContextValue['error'];
}
export function useNavigationState(): NavigationState {
const { loading, error } = usePageDataContext();
return { loading, error };
}
export function useNavigationControls() {
const { setData, setLoading, setError } = usePageDataContext();
return { setData, setLoading, setError };
}
@@ -0,0 +1,59 @@
import { useCallback } from 'react';
import { type PageData, useNavigationControls } from '../context/page-data';
function buildReqUrl(url: string): string {
try {
const u = new URL(url, window.location.origin);
u.searchParams.set('_ui_next', '1');
return u.pathname + u.search + u.hash;
} catch {
const sep = url.includes('?') ? '&' : '?';
return `${url}${sep}_ui_next=1`;
}
}
function isSameOrigin(url: string): boolean {
try {
return new URL(url, window.location.origin).origin === window.location.origin;
} catch {
return false;
}
}
export function useNavigate() {
const { setData, setLoading, setError } = useNavigationControls();
const navigate = useCallback(async (url: string) => {
if (!isSameOrigin(url)) {
window.location.href = url;
return;
}
setLoading(true);
setError(null);
try {
const fetchUrl = buildReqUrl(url);
const res = await fetch(fetchUrl, { headers: { Accept: 'application/json', 'x-hydro-inject': 'uicontext,usercontext' } });
if (res.redirected) {
window.location.href = res.url;
return;
}
if (!res.ok) {
throw new Error(`Navigation failed: ${res.status} ${res.statusText}`);
}
const data: PageData = await res.json();
console.log('[Hydro] data received:', data);
setData((prev) => (data.HYDRO_INJECTED ? data : { ...prev, ...data }));
history.pushState(null, '', url);
} catch (e) {
const err = e instanceof Error ? e : new Error(String(e));
setError(err);
console.error('[Hydro] navigation error:', err);
// Fall back to hard navigation on unexpected errors
window.location.href = url;
} finally {
setLoading(false);
}
}, [setData, setLoading, setError]);
return navigate;
}