【问题标题】:How to send request on click React Hooks way?如何通过点击 React Hooks 方式发送请求?
【发布时间】:2019-09-02 23:06:12
【问题描述】:

如何使用反应钩子在按钮单击时发送 http 请求?或者,就此而言,如何对按钮单击产生任何副作用?

到目前为止我看到的是有一些“间接”的东西,比如:

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

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

【问题讨论】:

  • 我认为使用 react 钩子来做这件事没有意义,你可以在你的组件中拥有一个正常的函数并将它作为你的输入事件处理程序。
  • render(因此 sfc)不应该有副作用的观点怎么样?
  • 使用钩子会使代码更加灵活,但它是一种临时措施,React 将使用 Suspense 处理这个问题,请参阅robinwieruch.de/react-hooks-fetch-data 了解更多信息。
  • @dorriz 是的,是的,随着时间的推移,这个故事会增加悬念。但在那之前,让我们假装暂停不/不会存在。

标签: reactjs react-hooks


【解决方案1】:

您可以创建一个自定义挂钩useApi 并返回一个函数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>
);


【讨论】:

    【解决方案2】:

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

    在处理副作用时,需要将启动副作用的逻辑和产生副作用的逻辑分开(类似于 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

    更新:

    只需使用react-use 库中的useAsyncuseAsyncFn 即可替代上述所有复杂性,它更加简洁明了。

    示例:

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

    【讨论】:

    • 最佳答案,尤其是react-use上的推荐。
    【解决方案3】:
    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 指出这可能会给出警告:“无法在未安装的组件上执行 React 状态更新。这是一个空操作,但它表明您的应用程序中存在内存泄漏。要修复,请取消所有useEffect 清理函数中的订阅和异步任务。”

    实际上,发生的情况是请求仍在处理中,同时您的组件会卸载。然后它会尝试在一个未安装的组件上 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} />
      )
    }
    

    【讨论】:

    • 所以,1. 将标志设置为状态,然后被其他钩子(useEffectuseCallback)拾取是正确的方法吗? 2. 能否请您解释一下为什么您更喜欢使用useCallback 而不是useCallback? tnx!
    • useCallback 而不是 useEffect 你的意思是?好吧,因为它是回调而不是副作用
    • 好吧,取决于你如何对待它。如果你使用响应式/声明式方式,它可以是sideEffect。如果你使用命令式的方式,它可以是一个回调。两者都有效。但我当然明白你的意思。
    • 我不推荐使用 useEffect。为此,它将成为由状态更改触发的效果,这也会导致不必要的重新渲染。当回调被用户操作触发时,回调本质上是副作用。
    • @tkd_aj 这意味着实际请求在组件卸载后得到解决。我将更新我的代码以举例说明如何解决这个问题:-)
    【解决方案4】:

    您不需要在单击按钮时发送请求的效果,而是您需要的只是一个处理程序方法,您可以使用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,但是由于其他一些更改而导致的额外渲染会导致请求进入循环

    【讨论】:

    • @deezg 很高兴能帮上忙 :-)
    【解决方案5】:

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

    示例

    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>

    【讨论】:

    • 显然 OP 正在尝试禁用触发请求的按钮,因此您可能必须在 onClick 中添加 setData(0)
    • 首选方式是什么,为什么?另外,您对render/sfc 不应该引起副作用有什么看法?在这里,我们渲染的内容取决于请求的状态(已发送、待处理、响应)
    • @deezg 这是非常具体的上下文,主要是见仁见智。如果你的效果有很多依赖关系,使用效果可能会更好,但如果像我的示例中只有一个状态,则很容易将逻辑直接放在点击处理程序中。
    【解决方案6】:

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

    const [requestSent, setRequestSent] = useState(false);
    
    const sendRequest = () => {
      setRequestSent(true);
      fetch().then(() => setRequestSent(false));
    };
    

    Working example

    【讨论】:

    • 我很可能是错的,但 sfc 不意味着没有副作用吗?我知道这行得通,但它引入了副作用,而没有实际使用类似效果的钩子。这是推荐的方法吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-04-03
    • 2013-03-26
    • 1970-01-01
    • 1970-01-01
    • 2019-12-16
    • 2017-04-30
    • 2017-10-09
    相关资源
    最近更新 更多