How to implement router leave guard like vue-router in react-router-dom?

Viewed 1187

I need to ask user if submit form or not in react project like in vue-router beforeRouteLeave guard:

<template>
  <div>
    <input
      type="text"
      name="name"
      v-model="name"
      placeholder="input some text"
    />
    <router-link to="/">go back or click goback menu in browser </router-link>
  </div>
</template>

<script>
import { MessageBox } from "element-ui";

export default {
  data() {
    return {
      name: "",
    };
  },
  methods: {
    http() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const isOk = Math.random() > 0.5;
          isOk && resolve({ ok: true });
          !isOk && reject(new Error("server 503"));
        }, 2000);
      });
    },
  },
  async beforeRouteLeave(to, from, next) {
    if (this.name) {
      // form has input
      try {
        // it's better to use UI modal than window.confirm
        await MessageBox.confirm("do u want to submit?", "tips", {
          confirmButtonText: "yes",
          cancelButtonText: "no",
          type: "warning",
        });
        const res = await this.http(); //submit form
        console.log(res);
        // http success,let it go
        res.ok && next();
      } catch (error) {
        next(false);
      }
    } else {
      next();
    }
  },
};
</script>

demo in codesandbox online by vue-router

This work well when user click goback and forward button in browser menu and Programmatic Navigation work as well. How can I implement the same requirement in react-router-dom?

I have tried this way based on deechris27's answer, but it does not work as vue-router.

my key code :

import React, { useState, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { Modal } from 'antd'
import { ExclamationCircleOutlined } from '@ant-design/icons'

const { confirm } = Modal
function Confirm() {
  return new Promise((resolve, reject) => {
    confirm({
      title: 'Do you want to save data?',
      icon: <ExclamationCircleOutlined />,
      onOk() {
        resolve({ ok: true })
      },
      onCancel() {
        reject(new Error('cancel'))
      },
    })
  })
}

function Users() {

  const http = () => {
    console.log("I'll show up on submit")
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({ ok: true })
      }, 2000)
    })
  }

  const history = useHistory()
  const [input, setInput] = useState('')
  const changeInput = (event) => {
    const { value } = event.currentTarget
    setInput(value)
  }

  useEffect(() => {
    return history.listen(async (location, action) => {
      console.log(location, action)
      if (action === 'POP') {
        // const ok = window.confirm('do u want to submit form?')
        // url has change when confirm modal show
        try {
        await Confirm()
        // url has change when confirm modal Component show

        // send http request
        const res = await http()
        console.log(res)
       }catch (error) {
        // http error or cancel modal
      }
      }
    })
  }, [history])

  const handleSubmit = async (e) => {
    console.log('**********')
    e.preventDefault()
    if (input) {
      // const ok = window.confirm('Do u want submit?')
      try {
        const { ok } = await Confirm()
        console.log(ok)
        if (ok) {
          console.log('send http')
          const res = await http() // answer is yet false
          console.log(res)
          res.ok && history.push('/')
        }
      } catch (error) {
        // http error or cancel modal
      }
    } else {
      history.push('/')
    }
  }
  return (
    <form onSubmit={handleSubmit}>
      <input name="name" value={input} onChange={changeInput} placeholder="input some text" />
      <br />
      <button type="submit">submit</button>
    </form>
  )
}

export default Users

When custom Confirm Component or window.confirm showed, the url has chenged, so this way can not stop navigate away. Prompt has the same issue.

4 Answers

You could do the same in React using window.confirm. Doing it the react way by using state value will have one problem; timing the 2 async actions: setState and your async function call based on the value of answer.

Example below:

App.js

import "./styles.css";
import { Switch, Route } from "react-router-dom";
import Test from "./Test";
import Leave from "./Leaving";

export default function App() {
  return (
    <div className="App">
      <Switch>
        <Route exact path="/prompt" component={Leave} />
      </Switch>
      <Test />
    </div>
  );
}

Test.js

import React, { useState } from "react";
import { Prompt, useHistory } from "react-router-dom";

function Test() {

  const [answer, setIsAnswer] = useState(false);

  const history = useHistory();

  const http = () => {
    console.log("I'll show up on submit");
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({ ok: true });
      }, 200);
    });
  };


  const handleSubmit = async (e) => {
    console.log("**********");
    e.preventDefault();
    e.target.reset();
    setIsAnswer(true);  // setting state is async
    history.push("/prompt");
    answer && (await http()); // answer is yet false
  };

  return (
    <div className="Test">
      <h1>Hello Jack</h1>
      <form onSubmit={(e) => handleSubmit(e)}>
          <Prompt when={true} message={(_) => "Do you want submit form?"} /> // **when**'s value should ideally depend on answer value, harcoded for example purpose.
        <button>Click</button>
      </form>
    </div>
  );
}

export default Test;

Only when you click the second time, the React way of prompt is in action since setIsAnswer is async.

You could use the same window.confirm onSubmit.

const handleSubmit = async (e) => {
    console.log("**********");
    e.preventDefault();
    e.target.reset();
    const answer = window.confirm("Do you want submit form?");
    answer && history.push("/prompt");
    answer && (await http());
  };
    
     

I created a working example for you here in codesandbox https://codesandbox.io/s/charming-dew-u32wx

In the above sandbox, handleSubmit is plain JS way. handleSubmit2 is React way(click twice). Remove the commented <Prompt .../> inside the form and change onSubmit function to handleSubmit then handleSubmit2 to see both in action.

Explanation:

Any react module or approach that will rely on the state value will not help with your scenario, where you make HTTP call after prompt confirmation. window.comfirm is blocking code, using state for answer is non-blocking. If it's just about prompting before navigating away then you could go with the above react or any other react approach suggested.

Update in response to comments:

You could prevent the browser back button navigation by adding the below code.

const history = useHistory();

  useEffect(() => {
    return () => {
      if (history.action === "POP") {
       return false;
      }
   };
 }, [history]);

or in your Prompt itself through callback

<Prompt
    when={(location, action) => {
      if (action === "POP") {
        //
      }
    }}
    message={(_) => "Are you sure, you want to go back?"}
/>

I've updated the Leaving.js in the codesandbox with this browser back button scenario.

Either you can create this using react-router APIs or use an already available package like React Navigation Prompt.

It uses react-router under the hood

It's the simplest example to prevent a user to change the route by using the react-router and react-router-dom packages.

import React from "react";
import { BrowserRouter, Route, NavLink } from "react-router-dom";
import { Prompt } from "react-router";
import "./App.css";

function App() {
  const [dirty, setDirty] = React.useState(false);
  return (
    <BrowserRouter>
      <div className="App">
       <input onChange={() => setDirty(true)} /> {dirty ? "dirty" : "clean"}
       <br />
       <NavLink to="/test">Leave</NavLink>
       <br />
       <Route path="/test" component={() => "Has left"} />
       <Prompt message="Are you sure you want to go to /test?" />
     </div>
    </BrowserRouter>
  );
}
export default App;

You just need to install these two packages react-router and react-router-dom. This is the simplest logic which is built like if your form is dirty then you should prompt the user and prevent them from the leaving the page.

Related