这个问题发生在使用Vite构建的React 18上. 我正在try 在表单提交成功完成时触发导航返回到"/home"路径. 我使用的是react-router 6(参见下面的package.json).
问题: 在功能组件中调用导航("/home")会更改浏览器的地址栏,但应用程序不会导航.即使地址栏已正确更新,它仍然保留在表单页面上.
奇怪的是,如果我突出显示地址栏并按ENTER键,它会像预期的那样转到主页. 因此,路由可以工作,但导航("/home"不能.
UPDATE-按照要求,我正在重写这篇文章,试图包括一个最小的可重现的例子.在这种情况下,这是极具挑战性的,因为此问题涉及应用程序的父/子层次 struct 中的多个组件.我正在尽我最大的努力,并试图省略与该问题无关的代码.
UPDATE 2-通过反复试验,我已经让导航("/home")在某些情况下工作,并且我已经确定了最终决定导航("/home")是否工作的一件事.提交处理程序在RootLayout.tsx(父组件)中设置状态.我的理解是,当父对象中的状态发生变化时,会重新呈现所有子对象.我注意到的模式是这样的:
- 当在表单提交过程中父布局组件上的状态更改时,导航("/home")不起作用.地址栏会更改,但页面保持不变.
- 如果我注释掉表单提交过程中的状态设置,则导航("/home")正常工作.
Question:是否可能导航("/home")正被父状态更改中断,并且EditForm.tsx正在重新呈现,而不是导航到/home?
假设我对上述问题的回答是正确的,我是否应该……
- 要不要把"乔布斯"一词改为参考?这似乎是ref的错误用例,但它将阻止RootLayout组件中的状态更改,并可能允许Navigate()在子对象中正常工作.
- 是否在设置‘JOB’状态之前添加一些任意延迟,从而允许导航()首先完成?我讨厌这个主意.
App.tsx
import { useEffect, useState } from 'react';
import './App.css';
import { createBrowserRouter, RouterProvider, Outlet } from 'react-router-dom';
import { useMsal } from "@azure/msal-react";
import RootLayout from './components/RootLayout';
import HomePage from './components/HomePage';
import ManageFeeds from './components/ManageFeeds';
import EditFeed from './components/EditFeed';
import { UserFeed } from './models';
import { AppService, ServiceResult } from './services/AppService';
function App(props: any) {
const { instance, accounts } = useMsal();
const [jobs, setJobs] = useState<Job[]>([]);
const [userFeeds, setUserFeeds] = useState<UserFeed[]>();
const [activeUserFeed, setActiveUserFeed] = useState<UserFeed>();
const appService = new AppService(instance, accounts[0], [props.msalConfig.scopes]);
const getAccessToken = async () => {
const msalResponse = await instance.acquireTokenSilent({
scopes: [props.msalConfig.scopes],
account: accounts[0]
});
return msalResponse.accessToken;
}
const reloadUserFeeds = async () => {
const result = await appService.getFeeds();
if (result.success) {
setUserFeeds(result.data);
if (result.data!.length > 0) {
setActiveUserFeed(result.data![0]);
}
} else {
// problem loading data
}
}
const handleEditFeed = async (userFeed: UserFeed): Promise<ServiceResult> => {
// set state - add job to list
const job: Job = {
id: crypto.randomUUID(),
name: name,
status: 'In-Progress'
};
setJobs([...jobs, job]); // THIS CAUSES navigate() TO FAIL IN CHILD COMPONENT
// API call that persists form submission to database
const result = await appService.editFeed(userFeed);
if (result.success) {
// reload items from service
}
return result;
}
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout jobs={jobs}/>,
children: [
{ path: '/home', element: <HomePage getAccessToken={getAccessToken} userFeeds={userFeeds} activeUserFeed={activeUserFeed}/> },
{
path: '/feeds', element: <Outlet />, children: [
{ path: '/feeds/manage', element: <ManageFeeds userFeeds={userFeeds}/> },
{ path: '/feeds/edit', element: <EditFeed onSubmit={handleEditFeed} userFeeds={userFeeds}/> }
]
}
]
},
]);
useEffect(() => {
reloadUserFeeds();
}, []);
return (
<RouterProvider router={router} />
);
}
export default App;
RootLayout.tsx (parent)个
import { useState } from 'react';
import { Outlet, Link } from 'react-router-dom';
import { useMsal } from "@azure/msal-react";
import { createTheme } from '@mui/material/styles';
import { AppBar, Box, IconButton, Menu, MenuItem, ThemeProvider, Toolbar, Typography } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import SettingsIcon from '@mui/icons-material/Settings';
import './RootLayout.css';
import { Job } from '../models';
type props = {
jobs: Job[];
}
export default function RootLayout(props: props) {
const { instance, accounts } = useMsal();
const [profileMenuAnchorElem, setProfileMenuAnchorElem] = useState<null | HTMLElement>(null);
const globalTheme = createTheme({
palette: {
primary: {
main: '#0052cc',
},
secondary: {
main: '#edf2ff',
},
}
});
const handleProfileMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setProfileMenuAnchorElem(event.currentTarget);
}
const handleProfileMenuClose = () => {
setProfileMenuAnchorElem(null);
}
return (
<ThemeProvider theme={globalTheme}>
<Box sx={{ height: 1, flexGrow: 1 }}>
<AppBar position="static" sx={{}}>
<Toolbar>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" component="div" sx={{ pr: 4 }}>
<span id="header__title">DASHBOARD</span>
</Typography>
<Box sx={{ flexGrow: 1, display: 'flex', alignItems: 'center' }}>
<Link to={"/home"} className='router-nav-link'>Home</Link>
<Link to={"/feeds/manage"} className='router-nav-link'>Manage Feeds</Link>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box>
<Typography>{accounts && accounts.length > 0 && accounts[0].username}</Typography>
</Box>
<IconButton
size="large"
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleProfileMenuOpen}
color="inherit"
>
<SettingsIcon />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={profileMenuAnchorElem}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(profileMenuAnchorElem)}
onClose={handleProfileMenuClose}
>
<MenuItem onClick={handleProfileMenuClose}>My account</MenuItem>
<MenuItem onClick={() => instance.logout()}>Logout</MenuItem>
</Menu>
</Box>
</Toolbar>
</AppBar>
<Box sx={{ height: 'calc(100% - 64px)', overflowY: 'auto' }}>
<Outlet />
</Box>
</Box>
</ThemeProvider>
)
}
EditFeed.tsx (child)个
import React, { useEffect, useState } from "react";
import { useLocation, useNavigate } from 'react-router-dom';
import { Box, Button, Stack, TextField, Typography } from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers-pro";
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LoadingButton } from '@mui/lab';
import { UserFeed } from "../models";
import dayjs, { Dayjs } from "dayjs";
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
import { ServiceResult } from "../services/AppService";
type props = {
userFeeds: UserFeed[] | undefined;
onSubmit: (userFeed: UserFeed) => Promise<ServiceResult>;
}
export default function EditFeed(props: props) {
const location = useLocation();
const navigate = useNavigate();
const [userFeed, setUserFeed] = useState<UserFeed>({ startDateTime: null, endDateTime: null });
const [originalUserFeed, setOriginalUserFeed] = useState<UserFeed>();
const [submitIsLoading, SetSubmitIsLoading] = useState<boolean>(false);
const handleNameChange = async (inUserFeed: UserFeed) => {
setUserFeed(inUserFeed);
}
const handleSubmit = async () => {
SetSubmitIsLoading(true);
const result = await props.onSubmit(userFeed);
if (result.success) {
// redirect home
navigate("/home");
} else {
// handle failure
}
SetSubmitIsLoading(false);
}
useEffect(() => {
if (location.state && location.state.userFeed && location.state.userFeed.userFeedId) {
setUserFeed(location.state.userFeed);
const userFeedCopy = JSON.parse(JSON.stringify(location.state.userFeed));
setOriginalUserFeed(userFeedCopy);
} else {
setUserFeed({ ...userFeed });
setOriginalUserFeed(undefined);
}
}, []);
return (
<Box sx={{ height: 1, display: 'flex', flexDirection: 'column', boxSizing: 'border-box' }}>
<Box sx={{ height: 0.5, display: 'flex', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', width: 0.49, p: 2 }}>
<Typography variant="h5" sx={{ mb: 2 }}>{!originalUserFeed ? "Create" : "Edit"} Feed</Typography>
<TextField
label="Name"
id="filled-size-small"
value={userFeed?.feedName || ''}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
handleNameChange({ ...userFeed, feedName: event.target.value });
}}
/>
<TextField
label='Keywords'
id="filled-size-small"
value={userFeed?.requiredKeywords || ''}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setUserFeed({ ...userFeed, requiredKeywords: event.target.value });
}}
/>
<Box sx={{ display: 'flex', width: 1, justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', width: 0.49 }}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label="Start Date"
timezone="UTC"
value={dayjs.utc(userFeed?.startDateTime)}
onChange={(newValue: Dayjs | null) => {
setUserFeed({ ...userFeed, startDateTime: newValue ? newValue!.format('YYYY-MM-DD') : undefined });
}}
/>
</LocalizationProvider>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', width: 0.49 }}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label="End Date"
timezone="UTC"
value={dayjs.utc(userFeed?.endDateTime)}
onChange={(newValue: Dayjs | null) => {
setUserFeed({ ...userFeed, endDateTime: newValue ? newValue!.format('YYYY-MM-DD') : undefined });
}}
/>
</LocalizationProvider>
</Box>
</Box>
<Stack direction="row" spacing={2}>
<LoadingButton
loading={submitIsLoading}
onClick={handleSubmit}
variant="contained">
Submit
</LoadingButton>
<Button href="/feeds/manage" variant="outlined">Cancel</Button>
</Stack>
</Box>
</Box>
</Box>
)
}
package.json
{
"name": "overwatch.react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"test": "jest"
},
"dependencies": {
"@arcgis/core": "^4.28.10",
"@azure/msal-browser": "^3.5.0",
"@azure/msal-react": "^2.0.7",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.18",
"@mui/lab": "^5.0.0-alpha.162",
"@mui/material": "^5.14.18",
"@mui/x-data-grid": "^6.19.1",
"@mui/x-data-grid-pro": "^6.19.3",
"@mui/x-date-pickers": "^6.19.0",
"@mui/x-date-pickers-pro": "^6.19.3",
"axios": "^1.6.5",
"dayjs": "^1.11.10",
"lodash.debounce": "^4.0.8",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.20.0",
"react-router-dom": "^6.20.0"
},
"devDependencies": {
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^20.10.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.2.0",
"eslint": "^8.53.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"jest": "^29.7.0",
"typescript": "^5.2.2",
"vite": "^5.0.0"
}
}
我错过了什么?