【问题标题】:React: what is the right pattern to prevent double submission caused by multiple onClick()?React:防止多次 onClick() 导致重复提交的正确模式是什么?
【发布时间】:2020-04-22 23:19:21
【问题描述】:

尽管 setState() 的异步操作使任务复杂化,但在第一次单击后防止任何进一步的函数调用的最佳方法是什么?

该机制不一定必须基于 React 状态,但必须是易于可逆的(例如,在凭据错误的情况下通过 login())。

在下面的示例中,refs 仅用于模拟非常快速的点击。

在进入处理程序后立即检查状态的解决方案似乎不起作用:https://stackoverflow.com/a/49642037 (https://codesandbox.io/s/stupefied-moon-uouz2)

通过 ref 直接禁用按钮的解决方案对于可以通过回车从任何字段提交的表单来说根本不方便:https://stackoverflow.com/a/35316216

一个似乎适用于 Class 组件的解决方案,但看起来有点难看,并且奇怪地导致 codeandbox.io 下的双重 setState(为什么?)无论点击次数如何,但不是本地(https://codesandbox.io/s/ecstatic-river-wuhov):

import React, { Component } from "react";

export default class Login extends Component {
  constructor() {
    super();
    this.state = { disabled: false };
    this.ref = React.createRef();
  }

  componentDidMount() {
    console.log("click");
    this.ref.current.click();
    console.log("click");
    this.ref.current.click();
    console.log("click");
    this.ref.current.click();
  }

  onClick = () => {
    this.setState(({ disabled }) => {
      disabled || this.login();
      return { disabled: true };
    });
  };

  login = () => console.log("login");

  render() {
    return (
      <button
        ref={this.ref}
        onClick={this.onClick}
        disabled={this.state.disabled}
      >
        Login
      </button>
    );
  }
}

一个稍重的变体,但没有那种奇怪的副作用 (https://codesandbox.io/s/compassionate-bouman-hjtv6):

import React, { Component } from "react";

export default class Login extends Component {
  constructor() {
    super();
    this.state = { disabled: false };
    this.ref = React.createRef();
  }

  componentDidMount() {
    console.log("click");
    this.ref.current.click();
    console.log("click");
    this.ref.current.click();
    console.log("click");
    this.ref.current.click();
  }

  componentDidUpdate(prevProps, prevState) {
    const { disabled } = this.state;
    if (prevState.disabled !== disabled && disabled) {
      this.login();
    }
  }

  onClick = () => this.setState({ disabled: true });

  login = () => console.log("login");
  render() {
    return (
      <button
        ref={this.ref}
        onClick={this.onClick}
        disabled={this.state.disabled}
      >
        Login
      </button>
    );
  }
}

对于功能组件,此解决方案有效,但在开发模式下会导致 eslint 警告(React Hook useEffect 缺少依赖项)(这很糟糕吗?)(https://codesandbox.io/s/frosty-allen-tgs42):

import React, { useRef, useEffect, useState } from "react";

export default () => {
  const [disabled, setDisabled] = useState(false);

  const ref = useRef();

  useEffect(() => {
    console.log("click");
    ref.current.click();
    console.log("click");
    ref.current.click();
    console.log("click");
    ref.current.click();
  }, []);

  useEffect(() => {
    disabled && login();
  }, [disabled]);

  const onClick = () => setDisabled(true);

  const login = () => console.log("login");

  return (
    <button ref={ref} onClick={onClick} disabled={disabled}>
      Login
    </button>
  );
};

在codeandbox.io下导致双重setSate的类组件的解决方案似乎对功能组件(我目前在生产中使用的模式)没有问题(https://codesandbox.io/s/kind-hoover-ls3t3):

import React, { useRef, useEffect, useState } from "react";

export default () => {
  const [disabled, setDisabled] = useState(false);

  const ref = useRef();

  useEffect(() => {
    console.log("click");
    ref.current.click();
    console.log("click");
    ref.current.click();
    console.log("click");
    ref.current.click();
  }, []);

  const onClick = () =>
    setDisabled(prevDisabled => {
      prevDisabled || login();
      return true;
    });

  const login = () => console.log("login");

  return (
    <button ref={ref} onClick={onClick} disabled={disabled}>
      Login
    </button>
  );
};

由于这些解决方案似乎都不令人满意,有什么建议吗?

【问题讨论】:

  • 那个 eslint 警告意味着你的依赖数组可能不应该是空的。看起来ref.current 可能需要在里面。
  • 您是否考虑过使用去抖动器?
  • @ellitt:完全警告:Line 23:8: React Hook useEffect has a missing dependency: 'login'. Either include it or remove the dependency array react-hooks/exhaustive-deps。似乎是表格中缺少的“登录”,但是当您将其添加到其中时,情况会更糟:The 'login' function makes the dependencies of useEffect Hook (at line 43) change on every render...
  • 哦,我明白了,你有两个 useEffects。我只是在看第一个。
  • @JayBee:确实,去抖动器肯定会允许解决 pb。另一方面,这个解决方案可能不是最干净的,因为我们必须估计去抖动器延迟与我们期望观察到的 setState 相比(停用按钮然后接管去抖动器)。因此它们不会以编程方式链接。

标签: reactjs


【解决方案1】:

创建一个带有闭包的函数式函数,该闭包只会运行一次传递给它的函数,如下所示:

function runOnce(functionToRun){
  var run = false;
  return function(){
    if(!run){
      run = true;
      functionToRun();
    }
  }
}

这是我在浏览器控制台中运行的测试:

const login = runOnce(() => console.log("login"));


var x = runOnce(function(){console.log("Hi");});
undefined
x()
VM396:1 Hi
undefined
x()
undefined

根据您的问题进行编辑:

如果您只需要运行一次并允许在您的函数失败时重试,请让您的函数返回一个表示成功的布尔值(或解析为布尔值的承诺)并将 run 值设置为该值。

如果您的函数返回布尔值(同步):

if(!run){
      run = true; // Block from running twice while function is executing
      var success = functionToRun();
      if(!success) { // We were not successful
        run = false; // Allow us to run again
      }
}

如果你的函数返回一个承诺:

if(!run){
      run = true; // Block from running twice while function is executing
      // However you process your function's result, this is just
      // an example
      functionToRun().then().catch(() => {run = false});          
}

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2020-12-04
  • 2013-02-21
  • 1970-01-01
  • 2018-02-01
  • 1970-01-01
相关资源
最近更新 更多