Appearance
编程式导航
概述
编程式导航是指通过JavaScript代码而非用户点击链接来控制页面跳转。React Router v6提供了useNavigate Hook来实现编程式导航,支持前进、后退、替换历史记录等多种导航方式。
useNavigate Hook详解
基础用法
jsx
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const [credentials, setCredentials] = useState({ email: '', password: '' });
const handleLogin = async (e) => {
e.preventDefault();
try {
const user = await loginAPI(credentials);
// 登录成功后导航到仪表板
navigate('/dashboard');
} catch (error) {
console.error('Login failed:', error);
}
};
const handleCancel = () => {
// 返回上一页
navigate(-1);
};
const handleSignUp = () => {
// 导航到注册页面
navigate('/register');
};
return (
<form onSubmit={handleLogin}>
<input
type="email"
value={credentials.email}
onChange={(e) => setCredentials(prev => ({
...prev,
email: e.target.value
}))}
placeholder="Email"
/>
<input
type="password"
value={credentials.password}
onChange={(e) => setCredentials(prev => ({
...prev,
password: e.target.value
}))}
placeholder="Password"
/>
<div className="form-actions">
<button type="submit">Login</button>
<button type="button" onClick={handleCancel}>Cancel</button>
<button type="button" onClick={handleSignUp}>Sign Up</button>
</div>
</form>
);
}导航选项
jsx
function NavigationExamples() {
const navigate = useNavigate();
const examples = {
// 基础导航
basicNavigation: () => {
navigate('/dashboard');
},
// 替换当前历史记录条目
replaceNavigation: () => {
navigate('/dashboard', { replace: true });
},
// 带状态的导航
navigationWithState: () => {
navigate('/profile', {
state: {
from: 'dashboard',
user: currentUser,
timestamp: Date.now()
}
});
},
// 相对导航
relativeNavigation: () => {
navigate('../parent'); // 上一级
navigate('child'); // 子级
navigate('./sibling'); // 同级
},
// 历史记录导航
historyNavigation: () => {
navigate(-1); // 后退一页
navigate(-2); // 后退两页
navigate(1); // 前进一页
},
// 带查询参数的导航
navigationWithQuery: () => {
navigate('/search?q=react&category=books&page=1');
},
// 使用URLSearchParams构建查询参数
navigationWithSearchParams: () => {
const params = new URLSearchParams();
params.set('category', 'electronics');
params.set('minPrice', '100');
params.set('maxPrice', '500');
navigate(`/products?${params.toString()}`);
}
};
return (
<div className="navigation-examples">
<h2>导航示例</h2>
<div className="example-buttons">
<button onClick={examples.basicNavigation}>
Basic Navigation
</button>
<button onClick={examples.replaceNavigation}>
Replace Navigation
</button>
<button onClick={examples.navigationWithState}>
Navigation with State
</button>
<button onClick={examples.relativeNavigation}>
Relative Navigation
</button>
<button onClick={examples.historyNavigation}>
History Navigation
</button>
<button onClick={examples.navigationWithQuery}>
Navigation with Query
</button>
<button onClick={examples.navigationWithSearchParams}>
Navigation with SearchParams
</button>
</div>
</div>
);
}条件导航
基于认证状态的导航
jsx
// 认证相关的导航逻辑
function useAuthNavigation() {
const navigate = useNavigate();
const location = useLocation();
const { user, isLoading } = useAuth();
const navigateAfterLogin = useCallback((user) => {
// 获取登录前尝试访问的页面
const intendedPath = location.state?.from || '/dashboard';
// 根据用户角色决定导航目标
if (user.role === 'admin') {
navigate('/admin/dashboard', { replace: true });
} else if (user.needsOnboarding) {
navigate('/onboarding', { replace: true });
} else {
navigate(intendedPath, { replace: true });
}
}, [navigate, location.state]);
const navigateAfterLogout = useCallback(() => {
// 清除敏感页面的历史记录
navigate('/', { replace: true });
}, [navigate]);
const requireAuth = useCallback((callback) => {
return (...args) => {
if (!user) {
navigate('/login', {
state: { from: location.pathname },
replace: true
});
return;
}
callback(...args);
};
}, [user, navigate, location.pathname]);
return {
navigateAfterLogin,
navigateAfterLogout,
requireAuth
};
}
// 使用认证导航
function ProtectedComponent() {
const { requireAuth } = useAuthNavigation();
const handleSensitiveAction = requireAuth(() => {
// 这个函数只有在用户已认证时才会执行
performSensitiveOperation();
});
return (
<button onClick={handleSensitiveAction}>
Sensitive Action
</button>
);
}
// 基于权限的导航
function usePermissionNavigation() {
const navigate = useNavigate();
const { user, permissions } = useAuth();
const navigateWithPermissionCheck = useCallback((path, requiredPermission, options = {}) => {
if (!user) {
navigate('/login', {
state: { from: path },
replace: true
});
return false;
}
if (requiredPermission && !permissions.includes(requiredPermission)) {
navigate('/access-denied', {
state: {
requiredPermission,
attemptedPath: path
},
replace: options.replace
});
return false;
}
navigate(path, options);
return true;
}, [navigate, user, permissions]);
return { navigateWithPermissionCheck };
}业务逻辑导航
jsx
// 购物流程导航
function useCheckoutNavigation() {
const navigate = useNavigate();
const { cart, user } = useStore();
const startCheckout = useCallback(() => {
// 检查购物车
if (!cart || cart.items.length === 0) {
navigate('/cart', {
state: { message: 'Your cart is empty' }
});
return;
}
// 检查登录状态
if (!user) {
navigate('/login', {
state: {
from: '/checkout',
message: 'Please login to continue checkout'
}
});
return;
}
// 检查配送地址
if (!user.defaultAddress) {
navigate('/checkout/shipping', {
state: { step: 'address' }
});
return;
}
// 进入结账流程
navigate('/checkout');
}, [navigate, cart, user]);
const proceedToPayment = useCallback(async (shippingData) => {
try {
// 保存配送信息
await saveShippingInfo(shippingData);
navigate('/checkout/payment', {
state: { shippingConfirmed: true }
});
} catch (error) {
console.error('Failed to save shipping info:', error);
// 可以显示错误而不导航
}
}, [navigate]);
const completeOrder = useCallback(async (orderData) => {
try {
const order = await createOrder(orderData);
// 订单创建成功,导航到成功页面
navigate('/checkout/success', {
state: {
orderId: order.id,
orderNumber: order.orderNumber
},
replace: true // 防止用户返回到支付页面
});
} catch (error) {
navigate('/checkout/error', {
state: {
error: error.message,
orderData
}
});
}
}, [navigate]);
return {
startCheckout,
proceedToPayment,
completeOrder
};
}
// 表单提交导航
function useFormNavigation() {
const navigate = useNavigate();
const handleFormSubmit = useCallback(async (formData, config = {}) => {
const {
successPath = '/success',
errorPath = null,
keepFormData = false,
showSuccessMessage = true
} = config;
try {
const result = await submitForm(formData);
const navigationState = {
success: true,
message: showSuccessMessage ? 'Form submitted successfully' : null,
result
};
if (keepFormData) {
navigationState.formData = formData;
}
navigate(successPath, {
state: navigationState,
replace: config.replaceHistory
});
} catch (error) {
if (errorPath) {
navigate(errorPath, {
state: {
error: error.message,
formData: keepFormData ? formData : null
}
});
} else {
// 留在当前页面显示错误
console.error('Form submission failed:', error);
}
}
}, [navigate]);
return { handleFormSubmit };
}导航拦截和确认
离开页面确认
jsx
import { useBeforeUnload, useBlocker } from 'react-router-dom';
function FormWithUnsavedChanges() {
const navigate = useNavigate();
const [formData, setFormData] = useState({ title: '', content: '' });
const [isDirty, setIsDirty] = useState(false);
const [isSaving, setIsSaving] = useState(false);
// 阻止浏览器刷新/关闭时丢失未保存的数据
useBeforeUnload(
useCallback((event) => {
if (isDirty && !isSaving) {
event.preventDefault();
return (event.returnValue = 'You have unsaved changes. Are you sure you want to leave?');
}
}, [isDirty, isSaving]),
{ capture: true }
);
// 阻止路由导航
const blocker = useBlocker(
({ currentLocation, nextLocation }) => {
return isDirty &&
!isSaving &&
currentLocation.pathname !== nextLocation.pathname;
}
);
const handleFieldChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
setIsDirty(true);
};
const handleSave = async () => {
setIsSaving(true);
try {
await saveFormData(formData);
setIsDirty(false);
navigate('/success');
} catch (error) {
console.error('Save failed:', error);
} finally {
setIsSaving(false);
}
};
const handleDiscard = () => {
setFormData({ title: '', content: '' });
setIsDirty(false);
navigate('/');
};
// 处理导航确认
const handleNavigationConfirm = (shouldNavigate) => {
if (shouldNavigate) {
setIsDirty(false); // 允许导航
blocker.proceed();
} else {
blocker.reset();
}
};
return (
<div className="form-page">
<form>
<input
type="text"
value={formData.title}
onChange={(e) => handleFieldChange('title', e.target.value)}
placeholder="Title"
/>
<textarea
value={formData.content}
onChange={(e) => handleFieldChange('content', e.target.value)}
placeholder="Content"
rows={10}
/>
<div className="form-actions">
<button type="button" onClick={handleSave} disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save'}
</button>
<button type="button" onClick={handleDiscard}>
Discard
</button>
</div>
{isDirty && (
<div className="unsaved-warning">
You have unsaved changes.
</div>
)}
</form>
{/* 导航确认对话框 */}
{blocker.state === 'blocked' && (
<div className="navigation-confirm-modal">
<div className="modal-content">
<h3>Unsaved Changes</h3>
<p>You have unsaved changes. Are you sure you want to leave?</p>
<div className="modal-actions">
<button onClick={() => handleNavigationConfirm(true)}>
Leave without saving
</button>
<button onClick={() => handleNavigationConfirm(false)}>
Stay on page
</button>
</div>
</div>
</div>
)}
</div>
);
}
// 通用的离开确认Hook
function useUnsavedChangesWarning(hasUnsavedChanges, message = 'You have unsaved changes.') {
const blocker = useBlocker(
({ currentLocation, nextLocation }) => {
return hasUnsavedChanges &&
currentLocation.pathname !== nextLocation.pathname;
}
);
useBeforeUnload(
useCallback((event) => {
if (hasUnsavedChanges) {
event.preventDefault();
return (event.returnValue = message);
}
}, [hasUnsavedChanges, message]),
{ capture: true }
);
return blocker;
}导航历史管理
jsx
// 自定义历史管理Hook
function useNavigationHistory() {
const navigate = useNavigate();
const location = useLocation();
const [history, setHistory] = useState([]);
const [currentIndex, setCurrentIndex] = useState(-1);
// 记录导航历史
useEffect(() => {
setHistory(prev => {
const newHistory = [...prev.slice(0, currentIndex + 1), location.pathname];
setCurrentIndex(newHistory.length - 1);
return newHistory;
});
}, [location.pathname]);
const goBack = useCallback(() => {
if (currentIndex > 0) {
const previousPath = history[currentIndex - 1];
navigate(previousPath);
}
}, [navigate, history, currentIndex]);
const goForward = useCallback(() => {
if (currentIndex < history.length - 1) {
const nextPath = history[currentIndex + 1];
navigate(nextPath);
}
}, [navigate, history, currentIndex]);
const canGoBack = currentIndex > 0;
const canGoForward = currentIndex < history.length - 1;
const getHistoryInfo = () => ({
history,
currentIndex,
currentPath: history[currentIndex],
canGoBack,
canGoForward
});
return {
goBack,
goForward,
canGoBack,
canGoForward,
history,
getHistoryInfo
};
}
// 导航历史组件
function NavigationHistory() {
const {
goBack,
goForward,
canGoBack,
canGoForward,
history,
getHistoryInfo
} = useNavigationHistory();
const historyInfo = getHistoryInfo();
return (
<div className="navigation-history">
<div className="history-controls">
<button onClick={goBack} disabled={!canGoBack}>
← Back
</button>
<button onClick={goForward} disabled={!canGoForward}>
Forward →
</button>
<span className="history-position">
{historyInfo.currentIndex + 1} of {history.length}
</span>
</div>
<div className="history-list">
<h4>Navigation History:</h4>
<ol>
{history.map((path, index) => (
<li
key={index}
className={index === historyInfo.currentIndex ? 'current' : ''}
>
{path}
</li>
))}
</ol>
</div>
</div>
);
}复杂导航场景
多步骤向导导航
jsx
// 多步骤表单导航
function useWizardNavigation(steps, currentStep) {
const navigate = useNavigate();
const [completedSteps, setCompletedSteps] = useState(new Set());
const goToStep = useCallback((stepIndex) => {
const step = steps[stepIndex];
if (!step) return;
navigate(`/wizard/${step.path}`, {
state: {
stepIndex,
totalSteps: steps.length
}
});
}, [navigate, steps]);
const nextStep = useCallback(() => {
const currentIndex = steps.findIndex(step => step.path === currentStep);
const nextIndex = currentIndex + 1;
if (nextIndex < steps.length) {
// 标记当前步骤为完成
setCompletedSteps(prev => new Set([...prev, currentIndex]));
goToStep(nextIndex);
}
}, [currentStep, steps, goToStep]);
const previousStep = useCallback(() => {
const currentIndex = steps.findIndex(step => step.path === currentStep);
const prevIndex = currentIndex - 1;
if (prevIndex >= 0) {
goToStep(prevIndex);
}
}, [currentStep, steps, goToStep]);
const jumpToStep = useCallback((stepIndex) => {
// 只能跳转到已完成的步骤或下一步
const currentIndex = steps.findIndex(step => step.path === currentStep);
if (stepIndex <= currentIndex + 1 || completedSteps.has(stepIndex)) {
goToStep(stepIndex);
return true;
}
return false;
}, [currentStep, steps, completedSteps, goToStep]);
const canGoNext = useCallback(() => {
const currentIndex = steps.findIndex(step => step.path === currentStep);
return currentIndex < steps.length - 1;
}, [currentStep, steps]);
const canGoPrevious = useCallback(() => {
const currentIndex = steps.findIndex(step => step.path === currentStep);
return currentIndex > 0;
}, [currentStep, steps]);
return {
nextStep,
previousStep,
jumpToStep,
goToStep,
canGoNext: canGoNext(),
canGoPrevious: canGoPrevious(),
completedSteps
};
}
// 向导组件
function SetupWizard() {
const { pathname } = useLocation();
const currentStep = pathname.split('/').pop();
const steps = [
{ path: 'welcome', title: 'Welcome', component: WelcomeStep },
{ path: 'account', title: 'Account Setup', component: AccountStep },
{ path: 'preferences', title: 'Preferences', component: PreferencesStep },
{ path: 'verification', title: 'Verification', component: VerificationStep },
{ path: 'complete', title: 'Complete', component: CompleteStep }
];
const {
nextStep,
previousStep,
jumpToStep,
canGoNext,
canGoPrevious,
completedSteps
} = useWizardNavigation(steps, currentStep);
const currentStepIndex = steps.findIndex(step => step.path === currentStep);
const currentStepData = steps[currentStepIndex];
return (
<div className="setup-wizard">
{/* 进度指示器 */}
<div className="wizard-progress">
{steps.map((step, index) => (
<div
key={step.path}
className={`progress-step ${
index === currentStepIndex ? 'current' :
completedSteps.has(index) ? 'completed' : 'pending'
}`}
onClick={() => jumpToStep(index)}
style={{ cursor: completedSteps.has(index) || index <= currentStepIndex + 1 ? 'pointer' : 'default' }}
>
<span className="step-number">{index + 1}</span>
<span className="step-title">{step.title}</span>
</div>
))}
</div>
{/* 步骤内容 */}
<div className="wizard-content">
<Routes>
{steps.map(step => (
<Route
key={step.path}
path={step.path}
element={
<step.component
onNext={nextStep}
onPrevious={previousStep}
canGoNext={canGoNext}
canGoPrevious={canGoPrevious}
/>
}
/>
))}
</Routes>
</div>
</div>
);
}
// 向导步骤组件示例
function AccountStep({ onNext, onPrevious, canGoNext, canGoPrevious }) {
const [accountData, setAccountData] = useState({
username: '',
email: '',
password: ''
});
const [isValid, setIsValid] = useState(false);
useEffect(() => {
const valid = accountData.username &&
accountData.email &&
accountData.password.length >= 8;
setIsValid(valid);
}, [accountData]);
const handleNext = () => {
if (isValid) {
// 保存账户数据
saveAccountData(accountData);
onNext();
}
};
return (
<div className="wizard-step">
<h2>Account Setup</h2>
<div className="step-content">
<input
type="text"
value={accountData.username}
onChange={(e) => setAccountData(prev => ({
...prev,
username: e.target.value
}))}
placeholder="Username"
/>
<input
type="email"
value={accountData.email}
onChange={(e) => setAccountData(prev => ({
...prev,
email: e.target.value
}))}
placeholder="Email"
/>
<input
type="password"
value={accountData.password}
onChange={(e) => setAccountData(prev => ({
...prev,
password: e.target.value
}))}
placeholder="Password"
/>
</div>
<div className="step-actions">
<button
onClick={onPrevious}
disabled={!canGoPrevious}
>
Previous
</button>
<button
onClick={handleNext}
disabled={!canGoNext || !isValid}
>
Next
</button>
</div>
</div>
);
}动态导航路径
jsx
// 基于数据动态决定导航目标
function useDynamicNavigation() {
const navigate = useNavigate();
const { user } = useAuth();
const navigateBasedOnUserType = useCallback((baseAction) => {
const userRoutes = {
'premium': `/premium/${baseAction}`,
'business': `/business/${baseAction}`,
'enterprise': `/enterprise/${baseAction}`,
'free': `/free/${baseAction}`
};
const targetPath = userRoutes[user.type] || `/free/${baseAction}`;
navigate(targetPath);
}, [navigate, user]);
const navigateToUserDashboard = useCallback(() => {
const dashboardRoutes = {
'admin': '/admin/dashboard',
'manager': '/manager/dashboard',
'employee': '/employee/dashboard',
'customer': '/customer/dashboard'
};
const dashboardPath = dashboardRoutes[user.role] || '/dashboard';
navigate(dashboardPath);
}, [navigate, user]);
const navigateWithFallback = useCallback((preferredPath, fallbackPath) => {
// 检查用户是否有访问首选路径的权限
if (hasAccessToPath(user, preferredPath)) {
navigate(preferredPath);
} else {
navigate(fallbackPath);
}
}, [navigate, user]);
return {
navigateBasedOnUserType,
navigateToUserDashboard,
navigateWithFallback
};
}
// 条件导航组件
function ConditionalNavigationButton({ children, targetPath, condition, fallbackPath }) {
const navigate = useNavigate();
const handleClick = () => {
if (condition) {
navigate(targetPath);
} else if (fallbackPath) {
navigate(fallbackPath);
} else {
console.warn('Navigation condition not met and no fallback provided');
}
};
return (
<button onClick={handleClick} disabled={!condition && !fallbackPath}>
{children}
</button>
);
}
// 使用
function Dashboard() {
const { user } = useAuth();
return (
<div className="dashboard">
<h1>Welcome, {user.name}!</h1>
<div className="dashboard-actions">
<ConditionalNavigationButton
targetPath="/admin/users"
condition={user.role === 'admin'}
fallbackPath="/access-denied"
>
Manage Users
</ConditionalNavigationButton>
<ConditionalNavigationButton
targetPath="/premium/features"
condition={user.plan === 'premium'}
fallbackPath="/upgrade"
>
Premium Features
</ConditionalNavigationButton>
</div>
</div>
);
}导航事件和监听
导航事件处理
jsx
// 导航事件监听Hook
function useNavigationEvents() {
const location = useLocation();
const navigate = useNavigate();
const [navigationHistory, setNavigationHistory] = useState([]);
useEffect(() => {
const navigationEvent = {
path: location.pathname,
search: location.search,
hash: location.hash,
timestamp: Date.now(),
referrer: document.referrer
};
setNavigationHistory(prev => [...prev, navigationEvent]);
// 发送导航事件到分析服务
analytics.track('navigation', navigationEvent);
// 更新页面标题
updatePageTitle(location.pathname);
// 滚动到顶部(可选)
window.scrollTo(0, 0);
}, [location]);
const getNavigationStats = useCallback(() => {
const stats = navigationHistory.reduce((acc, event) => {
acc[event.path] = (acc[event.path] || 0) + 1;
return acc;
}, {});
return {
totalNavigations: navigationHistory.length,
uniquePaths: Object.keys(stats).length,
mostVisited: Object.entries(stats)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
};
}, [navigationHistory]);
return {
navigationHistory,
getNavigationStats
};
}
// 页面标题管理
function updatePageTitle(pathname) {
const titleMap = {
'/': 'Home - My App',
'/dashboard': 'Dashboard - My App',
'/profile': 'Profile - My App',
'/settings': 'Settings - My App'
};
// 动态标题生成
const generateTitle = (path) => {
// 处理动态路由
if (path.startsWith('/users/')) {
return 'User Profile - My App';
}
if (path.startsWith('/products/')) {
return 'Product Detail - My App';
}
return titleMap[path] || 'My App';
};
document.title = generateTitle(pathname);
}
// 导航监听组件
function NavigationListener() {
const location = useLocation();
const { user } = useAuth();
useEffect(() => {
// 记录页面访问
logPageVisit(location.pathname, user?.id);
// 检查页面权限
if (!hasPageAccess(user, location.pathname)) {
navigate('/access-denied');
}
// 加载页面特定的数据
loadPageData(location.pathname);
}, [location, user]);
return null; // 这是一个工具组件,不渲染内容
}导航状态管理
jsx
// 导航状态管理
function useNavigationState() {
const [isNavigating, setIsNavigating] = useState(false);
const [navigationError, setNavigationError] = useState(null);
const navigate = useNavigate();
const navigateWithLoading = useCallback(async (path, options = {}) => {
setIsNavigating(true);
setNavigationError(null);
try {
// 预加载路由数据(如果需要)
if (options.preload) {
await preloadRouteData(path);
}
navigate(path, options);
} catch (error) {
setNavigationError(error.message);
console.error('Navigation failed:', error);
} finally {
setIsNavigating(false);
}
}, [navigate]);
const navigateWithRetry = useCallback(async (path, options = {}, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
await navigateWithLoading(path, options);
return true;
} catch (error) {
if (i === maxRetries - 1) {
setNavigationError('Navigation failed after multiple attempts');
return false;
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}, [navigateWithLoading]);
return {
isNavigating,
navigationError,
navigateWithLoading,
navigateWithRetry
};
}
// 导航状态显示
function NavigationStatus() {
const { isNavigating, navigationError } = useNavigationState();
if (navigationError) {
return (
<div className="navigation-error">
<p>Navigation failed: {navigationError}</p>
</div>
);
}
if (isNavigating) {
return (
<div className="navigation-loading">
<p>Loading...</p>
</div>
);
}
return null;
}实战应用
应用1:电商结账流程
jsx
function CheckoutFlow() {
const navigate = useNavigate();
const { cart, user } = useStore();
const [checkoutData, setCheckoutData] = useState({
shipping: null,
payment: null,
billing: null
});
const steps = [
{ path: 'shipping', title: 'Shipping Information', component: ShippingStep },
{ path: 'payment', title: 'Payment Method', component: PaymentStep },
{ path: 'review', title: 'Order Review', component: ReviewStep },
{ path: 'complete', title: 'Order Complete', component: CompleteStep }
];
const proceedToNextStep = useCallback(async (currentStep, stepData) => {
// 更新结账数据
setCheckoutData(prev => ({ ...prev, [currentStep]: stepData }));
switch (currentStep) {
case 'shipping':
// 验证配送信息后进入支付步骤
if (await validateShippingInfo(stepData)) {
navigate('/checkout/payment');
} else {
// 显示验证错误
setValidationError('Invalid shipping information');
}
break;
case 'payment':
// 验证支付信息
if (await validatePaymentMethod(stepData)) {
navigate('/checkout/review');
} else {
setValidationError('Invalid payment information');
}
break;
case 'review':
// 提交订单
try {
const order = await submitOrder({
...checkoutData,
items: cart.items
});
navigate('/checkout/complete', {
state: { orderId: order.id },
replace: true
});
} catch (error) {
navigate('/checkout/error', {
state: { error: error.message }
});
}
break;
}
}, [navigate, checkoutData, cart]);
return (
<div className="checkout-flow">
<CheckoutProgress steps={steps} />
<div className="checkout-content">
<Routes>
{steps.map(step => (
<Route
key={step.path}
path={step.path}
element={
<step.component
data={checkoutData[step.path]}
onProceed={(data) => proceedToNextStep(step.path, data)}
onBack={() => navigate(-1)}
/>
}
/>
))}
<Route path="error" element={<CheckoutError />} />
</Routes>
</div>
</div>
);
}应用2:文档编辑器导航
jsx
function DocumentEditor() {
const { documentId } = useParams();
const navigate = useNavigate();
const [document, setDocument] = useState(null);
const [isDirty, setIsDirty] = useState(false);
const [autoSaving, setAutoSaving] = useState(false);
// 自动保存
useEffect(() => {
if (isDirty && !autoSaving) {
const timer = setTimeout(async () => {
setAutoSaving(true);
try {
await saveDocument(document);
setIsDirty(false);
} catch (error) {
console.error('Auto-save failed:', error);
} finally {
setAutoSaving(false);
}
}, 2000);
return () => clearTimeout(timer);
}
}, [isDirty, document, autoSaving]);
const handleSaveAndNavigate = useCallback(async (targetPath) => {
if (isDirty) {
try {
await saveDocument(document);
setIsDirty(false);
navigate(targetPath);
} catch (error) {
console.error('Save failed:', error);
// 询问用户是否继续
if (confirm('Failed to save document. Continue anyway?')) {
navigate(targetPath);
}
}
} else {
navigate(targetPath);
}
}, [navigate, document, isDirty]);
const handleNewDocument = () => {
handleSaveAndNavigate('/documents/new');
};
const handleOpenDocument = (docId) => {
handleSaveAndNavigate(`/documents/${docId}`);
};
const handleCloseDocument = () => {
handleSaveAndNavigate('/documents');
};
const navigateWithConfirmation = useCallback((path, message) => {
if (isDirty) {
if (confirm(message || 'You have unsaved changes. Continue?')) {
navigate(path);
}
} else {
navigate(path);
}
}, [navigate, isDirty]);
return (
<div className="document-editor">
<header className="editor-header">
<div className="editor-nav">
<button onClick={handleCloseDocument}>
← Back to Documents
</button>
<h1>{document?.title || 'Untitled Document'}</h1>
<div className="editor-status">
{autoSaving && <span>Auto-saving...</span>}
{isDirty && !autoSaving && <span>Unsaved changes</span>}
{!isDirty && !autoSaving && <span>All changes saved</span>}
</div>
</div>
<div className="editor-actions">
<button onClick={handleNewDocument}>
New Document
</button>
<button onClick={() => navigateWithConfirmation(
'/documents/templates',
'Switch to templates? Unsaved changes will be lost.'
)}>
Templates
</button>
<button onClick={() => navigateWithConfirmation(
'/settings',
'Go to settings? Unsaved changes will be lost.'
)}>
Settings
</button>
</div>
</header>
<div className="editor-content">
<DocumentEditorComponent
document={document}
onChange={(doc) => {
setDocument(doc);
setIsDirty(true);
}}
/>
</div>
</div>
);
}总结
编程式导航是React Router的强大功能:
- useNavigate Hook:核心的编程式导航工具
- 导航选项:支持替换历史、传递状态等选项
- 条件导航:基于业务逻辑的智能导航
- 导航拦截:useBlocker和useBeforeUnload防止意外离开
- 历史管理:自定义历史记录管理和追踪
- 复杂场景:支持多步骤流程、向导等复杂导航需求
掌握编程式导航能够创建更加智能和用户友好的导航体验。