如何在点击 React Hooks 方式上发送请求?

How to send request on click React Hooks way?

如何使用 React Hooks 在按钮点击时发送 http 请求?或者,就此而言,如何对按钮点击产生副作用?

到目前为止我看到的是 "indirect" 像:

export default = () => {
  const [sendRequest, setSendRequest] = useState(false);

  useEffect(() => {
    if(sendRequest){
       //send the request
       setSendRequest(false);
    }
  },
  [sendRequest]);

  return (
    <input type="button" disabled={sendRequest} onClick={() => setSendRequest(true)}
  );
}

这是正确的方法还是有其他模式?

export default () => {
  const [isSending, setIsSending] = useState(false)
  const sendRequest = useCallback(async () => {
    // don't send again while we are sending
    if (isSending) return
    // update state
    setIsSending(true)
    // send the actual request
    await API.sendRequest()
    // once the request is sent, update state again
    setIsSending(false)
  }, [isSending]) // update the callback if the state changes

  return (
    <input type="button" disabled={isSending} onClick={sendRequest} />
  )
}

这就是当您想在点击时发送请求并在发送时禁用按钮时的结果

更新:

@tkd_aj指出这可能会给出警告:"Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."

实际上,请求仍在处理中,同时您的组件已卸载。然后它会尝试 setIsSending(一个 setState)在一个未安装的组件上。

export default () => {
  const [isSending, setIsSending] = useState(false)
  const isMounted = useRef(true)

  // set isMounted to false when we unmount the component
  useEffect(() => {
    return () => {
      isMounted.current = false
    }
  }, [])

  const sendRequest = useCallback(async () => {
    // don't send again while we are sending
    if (isSending) return
    // update state
    setIsSending(true)
    // send the actual request
    await API.sendRequest()
    // once the request is sent, update state again
    if (isMounted.current) // only update if we are still mounted
      setIsSending(false)
  }, [isSending]) // update the callback if the state changes

  return (
    <input type="button" disabled={isSending} onClick={sendRequest} />
  )
}

您可以像以前一样在状态中定义布尔值,一旦触发请求,将其设置为 true,当您收到响应时,将其设置回 false:

const [requestSent, setRequestSent] = useState(false);

const sendRequest = () => {
  setRequestSent(true);
  fetch().then(() => setRequestSent(false));
};

Working example

您可以像您在问题中所做的那样,作为某些状态变化的结果来获取数据,但您也可以像在 class 组件中习惯的那样,直接在点击处理程序中获取数据。

例子

const { useState } = React;

function getData() {
  return new Promise(resolve => setTimeout(() => resolve(Math.random()), 1000))
}

function App() {
  const [data, setData] = useState(0)

  function onClick() {
    getData().then(setData)
  }

  return (
    <div>
      <button onClick={onClick}>Get data</button>
      <div>{data}</div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>

<div id="root"></div>

您不需要在按钮点击时发送请求的效果,您需要的只是一个处理程序方法,您可以使用 useCallback 方法

对其进行优化
const App = (props) => {
   //define you app state here
   const fetchRequest = useCallback(() => {
       // Api request here
   }, [add dependent variables here]);

  return (
    <input type="button" disabled={sendRequest} onClick={fetchRequest}
  );
}

使用带有 useEffect 的变量的跟踪请求不是正确的模式,因为您可以使用 useEffect 将状态设置为调用 api,但是由于其他一些更改而导致的额外渲染将导致请求继续在循环中

在函数式编程中,任何异步函数都应被视为副作用。

在处理副作用时,需要将启动副作用的逻辑和该副作用的结果逻辑分开(类似于 redux saga)。

基本上,按钮职责只是触发副作用,副作用职责是更新dom。

此外,由于 React 正在处理组件,因此您需要确保您的组件在任何 setState 之前或每个 await 之后仍然安装,这取决于您自己的偏好。

为了解决这个问题,我们可以创建一个自定义钩子useIsMounted这个钩子可以让我们很容易地检查组件是否仍然挂载

/**
 * check if the component still mounted
 */
export const useIsMounted = () => {
  const mountedRef = useRef(false);
  const isMounted = useCallback(() => mountedRef.current, []);

  useEffect(() => {
    mountedRef.current = true;
    return () => {
      mountedRef.current = false;
    };
  });

  return isMounted;
};

那么您的代码应该如下所示

export const MyComponent = ()=> {
  const isMounted = useIsMounted();
  const [isDoMyAsyncThing, setIsDoMyAsyncThing] = useState(false);

  // do my async thing
  const doMyAsyncThing = useCallback(async () => {
     // do my stuff
  },[])

  /**
   * do my async thing effect
  */
  useEffect(() => {
    if (isDoMyAsyncThing) {
      const effect = async () => {
        await doMyAsyncThing();
        if (!isMounted()) return;
        setIsDoMyAsyncThing(false);
      };
      effect();
    }
  }, [isDoMyAsyncThing, isMounted, doMyAsyncThing]);

  return (
     <div> 
        <button disabled={isDoMyAsyncThing} onClick={()=> setIsDoMyAsyncThing(true)}>
          Do My Thing {isDoMyAsyncThing && "Loading..."}
        </button>;
     </div>
  )
}

注意:最好将副作用的逻辑与触发效果的逻辑分开(useEffect

更新:

使用 useAsync and useAsyncFn from the react-use 库而不是上面所有的复杂性,它更简洁明了。

示例:

import {useAsyncFn} from 'react-use';

const Demo = ({url}) => {

  const [state, doFetch] = useAsyncFn(async () => {
    const response = await fetch(url);
    const result = await response.text();
    return result
  }, [url]);

  return (
    <div>
      {state.loading
        ? <div>Loading...</div>
        : state.error
          ? <div>Error: {state.error.message}</div>
          : <div>Value: {state.value}</div>
      }
      <button onClick={() => doFetch()}>Start loading</button>
    </div>
  );
};

您可以创建一个自定义挂钩 useApi 和 return 一个函数 execute,该函数在被调用时将调用 api(通常通过一些 onClick) .

useApi挂钩:

export type ApiMethod = "GET" | "POST";

export type ApiState = "idle" | "loading" | "done";

const fetcher = async (
    url: string,
    method: ApiMethod,
    payload?: string
  ): Promise<any> => {
    const requestHeaders = new Headers();
    requestHeaders.set("Content-Type", "application/json");
  
    console.log("fetching data...");
    const res = await fetch(url, {
      body: payload ? JSON.stringify(payload) : undefined,
      headers: requestHeaders,
      method,
    });
  
    const resobj = await res.json();
    return resobj;
  };

export function useApi(
  url: string,
  method: ApiMethod,
  payload?: any
): {
  apiState: ApiState;
  data: unknown;
  execute: () => void;
} {
  const [apiState, setApiState] = useState<ApiState>("idle");

  const [data, setData] = useState<unknown>(null);
  const [toCallApi, setApiExecution] = useState(false);

  const execute = () => {
    console.log("executing now");
    setApiExecution(true);
  };


  const fetchApi = useCallback(() => {
    console.log("fetchApi called");
    fetcher(url, method, payload)
      .then((res) => {
        const data = res.data;
        setData({ ...data });
        return;
      })
      .catch((e: Error) => {
        setData(null);
        console.log(e.message);
      })
      .finally(() => {
        setApiState("done");
      });
  }, [method, payload, url]);

  // call api
  useEffect(() => {
    if (toCallApi &&  apiState === "idle") {
      console.log("calling api");
      setApiState("loading");
      fetchApi();
    }
  }, [apiState, fetchApi, toCallApi]);

  return {
    apiState,
    data,
    execute,
  };
}

在某些组件中使用 useApi

const SomeComponent = () =>{

const { apiState, data, execute } = useApi(
      "api/url",
      "POST",
      {
        foo: "bar",
      }
    );

}

if (apiState == "done") {
      console.log("execution complete",data);
}

return (
 <button
   onClick={() => {
            execute();
          }}>
Click me
</button>
);


为此,您可以在 ReactJS 中使用回调挂钩,这是实现此目的的最佳选择,因为 useEffect 不是正确的模式,因为您可能将状态设置为使用 useEffect 进行 api 调用,但是由于某些其他更改而导致的额外呈现将导致请求进入循环。

 <const Component= (props) => {
       //define you app state here
       const getRequest = useCallback(() => {
           // Api request here
       }, [dependency]);
    
      return (
        <input type="button" disabled={sendRequest} onClick={getRequest}
      );
    }