ReactJS - 如何使用 React Router 创建具有单路径的多步组件/表单



我想在用户输入请求的信息后在组件之间切换。
将按此顺序向用户显示的组件:

{手机号码
  1. } 输入手机号码
  2. {身份证号码
  3. } 身份证号码
  4. {创建密码
  5. } 创建密码

完成所有这些步骤后,浏览器将切换到主页。 用户在填写每个组件中的每个请求之前,不得页面之间移动。

现在我想要一种更好的路由器方式,就好像我在Login里面有 3-4 个组件一样,并且必须在安全的乳清中,用户也不能通过 URL 手动切换组件。

import React, { Component } from 'react';
import {
BrowserRouter as Router,
Redirect,
Route,
Switch,
} from 'react-router-dom';
import MobileNum from './MobileNum.jsx';
import IdNumber from './IdNum.jsx';
import CreatePassword from './createPassword .jsx';
class SignUp extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<Router>
<Switch>
//// Here needs to know how to navigate to each component on its turn
<Route path='/'  component={MobileNum} />                                                
<Route path='/'  component={IdNumber} />
<Route path='/'  component={CreatePassword } />
</Switch>
</Router>
</div>
);
}
}
export default SignUp ;

我在reactrouter.com和许多其他人的网上搜索了一个干净的解决方案,但没有找到答案。
知道最好的方法是什么吗?

谢谢

由于像位置这样的路由器变量是不可变的,条件渲染本身将是更好的选择,如果你不想使用,你可以尝试切换。 我在下面举了一个例子,当每个组件中提交值时,您必须在 afterSubmit 之后触发该 .如果你使用 redux,你可以更好地实现它,因为你可以将值存储在 redux 状态,并使用 dipatch 直接从每个组件设置它。

//App.js
import React, { useState } from 'react';
import MobileNum from './MobileNum.jsx';
import IdNumber from './IdNum.jsx';
import CreatePassword from './createPassword .jsx';
function App (){
const [stage,setStage]= useState(1);
switch(stage){
case 2:
return <IdNumber  afterSubmit={setStage.bind(null,3)}/>
break;
case 3:
return <CreatePassword afterSubmit={setStage.bind(null,4)} />
case 4:
return <Home  />
break;
default:
return <MobileNum  afterSubmit={setStage.bind(null,2)}/>
}
}
export default App;
//Root
import React, { Component } from 'react';
import App from './App.jsx';
import {
BrowserRouter as Router,
Redirect,
Route,
Switch,
} from 'react-router-dom';
class Login extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<Router>
<Switch>
<Route path='/'  component={App} />    
</Switch>
</Router>
</div>
);
}
}
//Add on - Sign up form class based 
class SignUp extends React.Component {
constructor(props) {
super(props);
this.state = { stage: 1 };
}
render() {
switch (this.state.stage) {
case 2:
return <IdNumber afterSubmit={() => this.setState({ stage: 3 })} />;
break;
case 3:
return <CreatePassword afterSubmit={() => this.setState({ stage: 4 })} />;
case 4:
return <Home />;
break;
default:
return <MobileNum afterSubmit={() => this.setState({ stage: 2 })} />;
}
}
}

在React Router 中需要特殊处理才能满足您的安全要求。 我个人会在一个 URL 上加载多步骤向导,而不是更改每个步骤的 URL,因为这简化了事情并避免了许多潜在问题。您可以获得所需的设置,但这比需要的要困难得多

基于路径的路由

我正在使用新的 React Router v6 alpha 来回答这个答案,因为它使嵌套路由变得更加容易。 我使用/signup作为我们表单和 URL 的路径,例如各个步骤的/signup/password

主应用路由可能如下所示:

import { Suspense, lazy } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Header from "./Header";
import Footer from "./Footer";
const Home = lazy(() => import("./Home"));
const MultiStepForm = lazy(() => import("./MultiStepForm/index"));
export default function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<BrowserRouter>
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/signup/*" element={<MultiStepForm/>} />
</Routes>
<Footer />
</BrowserRouter>
</Suspense>
);
}

您将处理MultiStepForm组件内的各个步骤路径。您可以在所有步骤中共享表单的某些部分。 作为您Routes的部分应该只是不同的部分,即。表单域。

MultiStepForm中的嵌套Routes对象本质上是这样的:

<Routes>
<Route path="/" element={<Mobile />} />
<Route path="username" element={<Username />} />
<Route path="password" element={<Password />} />
</Routes>

但是我们需要知道路由路径的顺序,以便处理"上一个"和"下一个"链接。 因此,在我看来,基于配置数组生成路由更有意义。 在 React Router v5 中,您可以将配置作为 props 传递给<Route/>。在 v6 中,您可以跳过该步骤并使用基于对象的路由。

import React, { lazy } from "react";
const Mobile = lazy(() => import("./Mobile"));
const Username = lazy(() => import("./Username"));
const Password = lazy(() => import("./Password"));

/**
* The steps in the correct order.
* Contains the slug for the URL and the render component.
*/
export const stepOrder = [
{
path: "",
element: <Mobile />
},
{
path: "username",
element: <Username />
},
{
path: "password",
element: <Password />
}
];
// derived path order is just a helper
const pathOrder = stepOrder.map((o) => o.path);

请注意,这些组件是在没有 props 的情况下调用的。 我假设他们通过上下文获得所需的所有信息。 如果你想传递道具,那么你需要重构它。

import { useRoutes } from "react-router-dom";
import { stepOrder } from "./order";
import Progress from "./Progress";
export default function MultiStepForm() {
const stepElement = useRoutes(stepOrder);
return (
<div>
<Progress />
{stepElement}
</div>
);
}

当前位置

这是事情开始变得复杂的部分。 似乎useRouteMatch已在 v6 中删除(至少目前如此)。

我们可以使用useParams钩子上的"*"属性访问 URL 上匹配的通配符部分。 但这感觉它可能是一个错误而不是故意的行为,所以我担心它会在未来的版本中发生变化。 请记住这一点。 但它目前确实有效。

我们可以在自定义钩子中执行此操作,以便我们可以获取其他有用的信息。

export const useCurrentPosition = () => {
// access slug from the URL and find its step number
const urlSlug = useParams()["*"]?.toLowerCase();
// note: will be -1 if slug is invalid, so replace with 0
const index = urlSlug ? pathOrder.indexOf(urlSlug) || 0 : 0;
const slug = pathOrder[index];
// prev and next might be undefined, depending on the index
const previousSlug = pathOrder[index - 1];
const nextSlug = pathOrder[index + 1];
return {
slug,
index,
isFirst: previousSlug === undefined,
isLast: nextSlug === undefined,
previousSlug,
nextSlug
};
};

下一步

用户在填写每个组件中的每个请求之前,不得在页面之间移动。

您将需要某种表单验证。 您可以等到用户单击"下一步"按钮后再进行验证,但大多数现代网站都会选择在每次表单更改时验证数据。 像Formik和Yup这样的软件包对此有很大帮助。 查看 Formik 验证文档中的示例。

您将有一个isValid布尔值,它告诉您何时允许用户继续前进。 您可以使用它在"下一步"按钮上设置disabled道具。 该按钮应具有type="submit",以便其单击可以通过窗体的onSubmit操作进行处理。

我们可以将该逻辑制作成一个PrevNextLinks组件,我们可以在每种形式中使用。 此组件使用 formik 上下文,因此必须在<Formik/>窗体中呈现。

我们可以使用useCurrentPosition挂钩中的信息来呈现对上一步的Link

import { useFormikContext } from "formik";
import { Link } from "react-router-dom";
import { useCurrentPosition } from "./order";
/**
* Needs to be rendered inside of a Formik component.
*/
export default function PrevNextLinks() {
const { isValid } = useFormikContext();
const { isFirst, isLast, previousSlug } = useCurrentPosition();
return (
<div>
{/* button links to the previous step, if exists */}
{isFirst || (
<Link to={`/form/${previousSlug}`}>
<button>Previous</button>
</Link>
)}
{/* button to next step -- submit action on the form handles the action */}
<button type="submit" disabled={!isValid}>
{isLast ? "Submit" : "Next"}
</button>
</div>
);
}

下面是一个步骤的示例:

import { Formik, Form, Field, ErrorMessage } from "formik";
import React from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import Yup from "yup";
import "yup-phone";
import PrevNextLinks from "./PrevNextLinks";
import { useCurrentPosition } from "./order";
import { saveStep } from "../../store/slice";
const MobileSchema = Yup.object().shape({
number: Yup.string()
.min(10)
.phone("US", true)
.required("A valid US phone number is required")
});
export default function MobileForm() {
const { index, nextSlug, isLast } = useCurrentPosition();
const dispatch = useDispatch();
const navigate = useNavigate();
return (
<div>
<h1>Signup</h1>
<Formik
initialValues={{
number: ""
}}
validationSchema={MobileSchema}
validateOnMount={true}
onSubmit={(values) => {
// I'm iffy on this part. The dispatch and the navigate will occur simoultaneously,
// so you should not assume that the dispatch is finished before the target page is loaded.
dispatch(saveStep({ values, index }));
navigate(isLast ? "/" : `/signup/${nextSlug}`);
}}
>
{({ errors, touched }) => (
<Form>
<label>
Mobile Number
<Field name="number" type="tel" />
</label>
<ErrorMessage name="number" />
<PrevNextLinks />
</Form>
)}
</Formik>
</div>
);
}

阻止通过 URL 进行访问

用户不得能够通过 URL 手动切换组件。

如果用户尝试访问不允许他们查看的页面,我们需要重定向用户。 文档中的重定向 (身份验证) 示例应该会为您提供有关如何实现此操作的一些想法。 这PrivateRoute组件尤其:

// A wrapper for <Route> that redirects to the login
// screen if you're not yet authenticated.
function PrivateRoute({ children, ...rest }) {
let auth = useAuth();
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}

但是您的useAuth的等效版本是什么?

理念:看当前进展

我们可以允许访问者查看他们当前的步骤和任何以前输入的步骤。 我们查看是否允许用户查看他们尝试访问的步骤。 如果是,我们会加载该内容。 如果没有,您可以将他们重定向到正确的步骤或第一步。

您需要知道已完成的进度。 该信息需要存在于链中更高位置的位置,例如localStorage,父组件,Redux,上下文提供程序等。 你选择哪个取决于你,会有一些差异。 例如,使用localStorage将保留部分完成的表单,而其他表单则不会。

存储位置不如存储内容重要。 我们希望允许向后导航到前面的步骤,如果转到以前访问的步骤,则允许向前导航。 因此,我们需要知道哪些步骤可以访问,哪些步骤不能。 顺序很重要,所以我们想要某种数组。 我们将找出允许我们访问的最大步骤,并将其与请求的步骤进行比较。

您的组件可能如下所示:

import { useRoutes, Navigate } from "react-router-dom";
import { useSelector } from "../../store";
import { stepOrder, useCurrentPosition } from "./order";
import Progress from "./Progress";
export default function MultiStepForm() {
const stepElement = useRoutes(stepOrder);
// attempting to access step
const { index } = useCurrentPosition();
// check that we have data for all previous steps
const submittedStepData = useSelector((state) => state.multiStepForm);
const canAccess = submittedStepData.length >= index;
// redirect to first step
if (!canAccess) {
return <Navigate to="" replace />;
}
// or load the requested step
return (
<div>
<Progress />
{stepElement}
</div>
);
}

代码沙盒链接。 (注意:三步形式的大多数代码可以并且应该组合在一起)。

这一切都变得相当复杂,所以让我们尝试一些更简单的东西。

想法:要求从上一个/下一个链接访问 URL

我们可以使用位置的state属性来传递某种信息,让我们知道我们来自正确的位置。 喜欢{fromForm: true}. 您的MultiStepForm可以将缺少此属性的所有流量重定向到第一步。

const {state} = useLocation();
if ( ! state?.fromForm ) {
return <Navigate to="" replace state={{fromForm: true}}/>
}

您将确保表单内的所有Linknavigate操作都传递此状态。

<Link to={`/signup/${previousSlug}`} state={{fromForm: true}}>
<button>Previous</button>
</Link>
navigate(`/signup/${nextSlug}`, {state: { fromForm: true} });

无需更改路径

在编写了大量有关验证路径的代码和解释之后,我意识到您没有明确表示路径需要更改。

我只需要使用react-router-dom属性进行导航。

因此,您可以使用位置对象上的state属性来控制当前步骤。 您将state传递到Linknavigate与上述相同,但使用像{step: 1}这样的对象而不是{fromForm: true}.

<Link to="" replace state={{step: 2}}>Next</Link>

通过这样做,您可以大大简化代码。虽然我们回到一个基本问题,为什么。 如果重要信息是一种状态,为什么要使用 React Router? 为什么不直接使用本地组件状态(或 Redux 状态)并在单击"下一步"按钮时调用setState

这是一篇很好的文章,其中包含使用本地状态和材质 UI 步进器组件的完全实现的代码示例:

  • 使用 React Hooks、Formik、Yup 和 MaterialUI 构建多步骤表单,作者:Vu Nguyen
  • 代码沙盒

完成所有这些步骤后,浏览器将切换到主页。

有两种方法可以处理最终重定向到主页。 您可以有条件地呈现<Navigate/>组件(在 v5 中<Redirect/>),也可以调用navigate()(在 v5 中history.push())以响应用户操作。 这两个版本在 React Router v6 迁移指南中有详细说明。

我不认为添加 React Router 或任何库会改变我们在 React 中解决问题的方式。

你之前的方法很好。您可以将所有这些包装在一个新组件中,例如,

class MultiStepForm extends React.Component {
constructor(props) {
super(props);
this.state = {
askMobile: true,
}; 
};
askIdentification = (passed) => {
if (passed) {
this.setState({ askMobile: false });
}
};
render() {
return (
<div className='App-div'>
<Header />
{this.state.askMobile ? (
<MobileNum legit={this.askIdentification} />
) : (
<IdNumber />
)}
</div>
);
}
}

然后在您的路由上使用此组件。

...
<Switch>
<Route path='/' component={MultiStepForm} />
// <Route path='/'  component={MobileNum} />                                                    
// <Route path='/'  component={IdNumber} />
// <Route path='/'  component={CreatePassword } />
</Switch>
...

现在你想如何继续这是一个全新的问题。 另外,我还更正了askIdentification的拼写。

最新更新