ReactJS - 如何使用 React Router 通过单路径创建多步骤 component/form

ReactJS -How to create multistep component/form with single path using React Router

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

  1. {MobileNum } 输入手机号码
  2. {IdNumber }身份证号码
  3. {CreatePassword } 创建密码

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

现在我想要一个更好的方法 router 就像我在 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 和许多其他网站上搜索了干净的解决方案,但没有找到答案。
知道最好的方法是什么吗?

谢谢

我不认为添加 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.

的拼写

由于位置等路由器变量是不可变的,条件渲染本身会是更好的选择,如果您不想使用 if else,可以尝试切换。 我在下面给出了一个例子,当在每个组件中提交值时,你必须触发 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 中,您可以跳过该步骤并使用 object-based routing.

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);

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

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
  };
};

下一步

The user must not be able to move between pages until he filled each request in each component.

您将需要某种形式的验证。您可以等到用户单击“下一步”按钮后再进行验证,但大多数现代网站都选择在每次表单更改时验证数据。 Formik and Yup are a huge help with this. Check out the examples in the Formik Validation docs.

等软件包

您将有一个 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

访问

The user must not be able to switch components manually through the URL.

如果用户试图访问不允许他们查看的页面,我们需要重定向用户。文档中的 Redirects (Auth) 示例应该可以让您了解如何实现它。这个 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>
  );
}

CodeSandbox Link。 (注意:三步形式的大部分代码可以而且应该组合)。

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

想法:要求从 Previous/Next Link

访问 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} });

没有路径改变

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

I just need to use react-router-dom properties to navigate.

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

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

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

这是一篇很好的文章,其中包含使用本地状态和 Material UI Stepper component:

的完全实现的代码示例

When all these steps are completed the browser will switch to the home page.

有两种方法可以处理最终重定向到主页。您可以有条件地呈现 <Navigate/> 组件(v5 中的 <Redirect/>),或者您可以调用 navigate()(v5 中的 history.push())来响应用户操作。 React Router v6 Migration guide.

中详细解释了这两个版本