这个问题发生在使用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"
  }
}

我错过了什么?

推荐答案

我怀疑问题在于,在您的App组件中,每次重新渲染App次时,您都会创建一个新路由,例如,当jobsuserFeeds状态更新时.这显然会中断任何活动的导航操作.

由于您没有使用数据API,因此只需使用普通路由即可.

import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';

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 = useMemo(() => {
    return new AppService(instance, accounts[0], [props.msalConfig.scopes]);
  }, [instance, accounts, props.msalConfig.scopes]);

  const getAccessToken = async () => {
    const msalResponse = await instance.acquireTokenSilent({
      scopes: [props.msalConfig.scopes],
      account: accounts[0]
    });
    return msalResponse.accessToken;
  };

  const reloadUserFeeds = useCallback(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
    }
  }, [appService, setUserFeeds, setActiveUserFeed]);

  const handleEditFeed = async (userFeed: UserFeed): Promise<ServiceResult> => {
    // set state - add job to list
    const job: Job = {
      id: crypto.randomUUID(),
      name,
      status: 'In-Progress'
    };

    setJobs([...jobs, job]);
    
    // API call that persists form submission to database
    const result = await appService.editFeed(userFeed);

    if (result.success) {
      // reload items from service
    }

    return result;
  };

  useEffect(() => {
    reloadUserFeeds();
  }, []);

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<RootLayout jobs={jobs} />}>
          <Route
            path="home"
            element={(
              <HomePage
                getAccessToken={getAccessToken}
                userFeeds={userFeeds}
                activeUserFeed={activeUserFeed}
              />
            )}
          />
          <Route path="feeds">
            <Route path="manage" element={<ManageFeeds userFeeds={userFeeds} />} />
            <Route
              path="edit"
              element={(
                <EditFeed onSubmit={handleEditFeed} userFeeds={userFeeds} />
              )}
            />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

如果您更喜欢使用数据API,例如数据路由,那么我建议通过Reaction上下文将状态和回调向下传递到被路由的组件.

示例:

import { createContext, useContext } from 'react';

interface IAppContext {
  jobs: Job[];
  userFeeds: UserFeed[];
  activeUserFeed?: UserFeed;
  getAccessToken: () => Promise<string>;
  reloadUserFeeds: () => Promise<void>;
  handleEditFeed: (userFeed: UserFeed) => Promise<ServiceResult>;
}

export const AppContext = createContext<IAppContext>({
  jobs: [],
  userFeeds: [],
  activeUserFeed: undefined,
  getAccessToken: () => "",
  reloadUserFeeds: () => {},
  handleEditFeed: () => {},
});

export const useAppContext = () => useContext(AppContext);

const AppProvider = ({ children }) => {
  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,
      status: 'In-Progress'
    };

    setJobs(jobs => [...jobs, job]);
        
    // API call that persists form submission to database
    const result = await appService.editFeed(userFeed);

    if (result.success) {
      // reload items from service
    }

    return result;
  };

  useEffect(() => {
    reloadUserFeeds();
  }, []);

  return (
    <AppContext.Provider
      value={{
        jobs,
        userFeeds,
        activeUserFeed,
        getAccessToken,
        reloadUserFeeds,
        handleEditFeed,
      }}
    >
      {children}
    </AppContext.Provider>
  );
};

export default AppProvider;
const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      { path: 'home', element: <HomePage /> },
      {
        path: 'feeds',
        children: [
          { path: 'manage', element: <ManageFeeds /> },
          { path: 'edit', element: <EditFeed /> }
        ]
      }
    ]
  },
]);

function App(props: any) {
  return (
    <AppProvider>
      <RouterProvider router={router} />
    </AppProvider>
  );
}

组件HomePageManageFeedsEditFeed使用useAppContext挂钩来访问先前传递给它们的状态和回调函数.

示例:

const { jobs } = useAppContext();

Reactjs相关问答推荐

为什么我的标签在Redux API中不能正常工作?

Formik验证不适用于物料UI自动完成

后端关闭时如何在REACTION AXIOS拦截器中模拟数据?

导致useState中断的中断模式

无法在主组件的返回语句中呈现预期组件

捕获表单数据时的Reactjs问题

无法通过 fetch 获取数据到上下文中

React useEffect 依赖项

在 React 中将 MUI 样式外包到单独的文件中?

在遵循 react-navigation 的官方文档时收到警告消息

计数器递增 2 而不是 1

如何让 useEffect 在 React.JS 中只运行一次?

Recharts 笛卡尔网格 - 垂直线和水平线的不同样式

React - 添加了输入数据但不显示

React Final Form - 单选按钮在 FieldArray 中不起作用

如何通过onpress on view in react native flatlist来降低和增加特定视图的高度?

调用 Statsig.updateUser 时 UI 不呈现

ReactJs 行和列

如何实现两个 SVG 形状相互叠加的 XOR 操作?

ClearInterval 在 React 中的 useRef 没有按预期工作