【问题标题】:How to trigger useEffect() with multiple dependencies only on button click and on nothing else如何仅在按钮单击时触发具有多个依赖项的 useEffect()
【发布时间】:2026-01-09 18:00:02
【问题描述】:

我有以下代码:

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

function callSearchApi(userName: string, searchOptions: SearchOptions, searchQuery: string): Promise<SearchResult>{
    return new Promise((resolve, reject) => {
        const searchResult =
            searchOptions.fooOption
            ? ["Foo 1", "Foo 2", "Foo 3"]
            : ["Bar 1", "Bar 2"]
        setTimeout(()=>resolve(searchResult), 3000)
    })
}

type SearchOptions = {
    fooOption: boolean
}

type SearchResult = string[]

export type SearchPageProps = {
    userName: string
}

export function SearchPage(props: SearchPageProps) {
    const [isSearching, setIsSearching] = useState<boolean>(false)
    const [searchResult, setSearchResult] = useState<SearchResult>([])
    const [searchOptions, setSearchOptions] = useState<SearchOptions>({fooOption: false})
    const [searchQuery, setSearchQuery] = useState<string>("")
    const [lastSearchButtonClickTimestamp, setLastSearchButtonClickTimestamp] = useState<number>(Date.now())
    // ####################
    useEffect(() => {
        setIsSearching(true)
        setSearchResult([])
        const doSearch = () => callSearchApi(props.userName, searchOptions, searchQuery)
        doSearch().then(newSearchResult => {
            setSearchResult(newSearchResult)
            setIsSearching(false)
        }).catch(error => {
            console.log(error)
            setIsSearching(false)
        })
    }, [lastSearchButtonClickTimestamp])
    // ####################
    const handleSearchButtonClick = () => {
        setLastSearchButtonClickTimestamp(Date.now())
    }
    return (
        <div>
            <div>
                <label>
                    <input
                        type="checkbox"
                        checked={searchOptions.fooOption}
                        onChange={ev => setSearchOptions({fooOption: ev.target.checked})}
                    />
                    Foo Option
                </label>
            </div>
            <div>
                <input
                    type="text"
                    value={searchQuery}
                    placeholder="Search Query"
                    onChange={ev => setSearchQuery(ev.target.value)}
                />
            </div>
            <div>
                <button onClick={handleSearchButtonClick} disabled={isSearching}>
                    {isSearching ? "searching..." : "Search"}
                </button>
            </div>
            <hr/>
            <div>
                <label>Search Result: </label>
                <input
                    type="text"
                    readOnly={true}
                    value={searchResult}
                />
            </div>
        </div>
    )
}

export default SearchPage

另见this Codesandbox

代码运行良好。我可以更改文本字段中的搜索查询并单击选项复选框。一旦我准备好了,我可以点击“搜索”按钮,然后才会通过获取数据产生副作用。

现在,问题是编译器抱怨:

React Hook useEffect 缺少依赖项:“props.user.loginName”、“searchFilter”和“searchQuery”。要么包含它们,要么删除依赖数组。 [react-hooks/exhaustive-deps]

但是,如果我将props.user.loginNamesearchFiltersearchQuery 添加到依赖项列表中,那么每当我单击复选框或在文本字段中键入单个字符时都会触发副作用。

我确实了解钩子依赖的概念,但是我不知道如何先输入一些数据,只有单击按钮才能触发副作用。

这方面的最佳做法是什么?我已经阅读了https://reactjs.org/docs/hooks-effect.htmlhttps://www.robinwieruch.de/react-hooks-fetch-data,但找不到任何关于我的问题的例子。

更新 1:

我还想出了this solution,它看起来像:

type DoSearch = {
    call: ()=>Promise<SearchResult>
}

export function SearchPage(props: SearchPageProps) {
    const [isSearching, setIsSearching] = useState<boolean>(false)
    const [searchResult, setSearchResult] = useState<SearchResult>([])
    const [searchOptions, setSearchOptions] = useState<SearchOptions>({fooOption: false})
    const [searchQuery, setSearchQuery] = useState<string>("")
    const [doSearch, setDoSearch] = useState<DoSearch>()

    // ####################
    useEffect(() => {
        if(doSearch !==undefined){
            setIsSearching(true)
            setSearchResult([])
            doSearch.call().then(newSearchResult => {
                setSearchResult(newSearchResult)
                setIsSearching(false)
            }).catch(error => {
                console.log(error)
                setIsSearching(false)
            })
        }
    }, [doSearch])
    // ####################
    const handleSearchButtonClick = () => {
        setDoSearch({call: () => callSearchApi(props.userName, searchOptions, searchQuery)})
    }
    return (<div>...</div>)
}

现在实际函数是唯一可以正常工作的依赖项,编译器也很高兴。 但是,我不喜欢的是,我需要具有 call 属性的包装器对象。 如果我想将箭头函数直接传递给状态,这不会按预期工作,例如:

const [doSearch, setDoSearch] = useState<()=>Promise<SearchResult>>()
...
setDoSearch(() => callSearchApi(props.userName, searchOptions, searchQuery))

doSearch 没有设置为箭头函数,但callSearchApi 被立即执行。有人知道为什么吗?

【问题讨论】:

    标签: reactjs typescript react-hooks


    【解决方案1】:

    您可以从效果中删除setIsSearching(true),并在单击按钮时将其分开。

    const handleSearchButtonClick = () => {
            setLastSearchButtonClickTimestamp(Date.now())
            setIsSearching(true);
        }
    

    然后,您可以像这样修改您的useEffect 语句:

        useEffect(() => {
            if(!isSearching) {
               return false;
            }
            setSearchResult([])
            const doSearch = () => callSearchApi(props.userName, searchOptions, searchQuery)
            doSearch().then(newSearchResult => {
                setSearchResult(newSearchResult)
                setIsSearching(false)
            }).catch(error => {
                console.log(error)
                setIsSearching(false)
            })
        }, [allYourSuggestedDependencies]) // add all the suggested dependencies
    

    这将完成您正在寻找的东西。另一种方法是禁用react-hooks/exhaustive-deps 规则。

    如果您只需要在单击按钮时触发提取,我只需要使用一个函数。

    useEffect 很有用,例如,当您有一个过滤器列表(切换),并且您希望每次切换一个过滤器时都进行一次抓取(想象一下电子商务)。这是一个幼稚的例子,但它说明了这一点:

    useEffect(() => {
       fetchProducts(filters);
    }, [filters])
    

    【讨论】:

    • 啊,是的。谢谢你。我认为这会奏效。您会考虑这种最佳做法吗?我是唯一遇到这种问题的人吗?
    • 我认为这只是一个奇怪的场景。我过去也有过类似的情况。我有过滤器,你可以“保存”它们。每次您保存它们时,都会进行一次提取。显然,当您进入页面时,也会进行初始提取。我认为仅仅禁用规则不是一个好习惯。当您添加依赖项时,它可能会导致您将来遇到错误。此外,您必须发表评论,解释您禁用它的原因。试图理解这种影响的每个人都可能会感到困惑。
    • 所以,我的问题是:您是否有初始获取或其他触发器来实现此效果?如果不是,它可能只是一个函数。
    • 但这不是一个常见的日常场景,即一个表单具有多个输入元素,并且只有当用户单击某种确认/提交按钮时,才应该咨询后端?
    • 不,我没有初始提取。我只想在单击按钮后调用后端。当 promise 解决时,React 组件应该显示结果。
    【解决方案2】:

    useEffect 应该是这样的。

    • props.userName 在依赖列表中是有意义的,因为我们肯定想在用户名更改时获取新数据。
    • searchOptionssearchQuery 当你遇到这种情况时,最好使用 reducer,所以你只需要 dispatch action ==> searchOptionssearchQuery 不会在 userEffect 里面。 This article from Dan Abramov 提供了深入的解释和他们实现它的简单示例

    我使用useReducer 快速转换您的示例,请查看

    import React, { useState, useEffect, useReducer } from "react";
    
    function callSearchApi(
      userName: string,
      searchOptions: SearchOptions,
      searchQuery: string
    ): Promise<SearchResult> {
      return new Promise((resolve, reject) => {
        const searchResult = searchOptions.fooOption
          ? ["Foo 1", "Foo 2", "Foo 3"]
          : ["Bar 1", "Bar 2"];
        setTimeout(() => resolve(searchResult), 3000);
      });
    }
    
    const initialState = {
      searchOptions: { fooOption: false },
      searchQuery: "",
      startSearch: false, // can replace searching
      searchResult: []
    };
    
    const reducer = (state: any, action: any) => {
      switch (action.type) {
        case "SEARCH_START":
          return { ...state, startSearch: true, setSearchResult: [] }; //setSearchResult: [] base on your example
        case "SEARCH_SUCCESS":
          return { ...state, setSearchResult: action.data, startSearch: false };
        case "SEARCH_FAIL":
          return { ...state, startSearch: false };
        case "UPDATE_SEARCH_OPTION":
          return { ...state, searchOptions: { fooOption: action.data } };
        case "UPDATE_SEARCH_QUERY":
          return { ...state, searchQuery: action.data };
        default:
          return state;
      }
    };
    
    function SearchPage(props: SearchPageProps) {
      const [
        lastSearchButtonClickTimestamp,
        setLastSearchButtonClickTimestamp
      ] = useState<number>(Date.now());
      const [state, dispatch] = useReducer(reducer, initialState);
      const { searchOptions, startSearch, searchQuery, searchResult } = state;
      // ####################
      useEffect(() => {
        dispatch({ type: "SEARCH_START" });
      }, [lastSearchButtonClickTimestamp]);
      // ####################
      const handleSearchButtonClick = () => {
        setLastSearchButtonClickTimestamp(Date.now());
      };
    
      if (startSearch) {
        callSearchApi(props.userName, searchOptions, searchQuery)
          .then(newSearchResult => {
            dispatch({ type: "SEARCH_SUCCESS", data: newSearchResult });
          })
          .catch(error => {
            console.log(error);
            dispatch({ type: "SEARCH_FAIL" });
          });
      }
      return (
        <div>
          <div>
            <label>
              <input
                type="checkbox"
                checked={searchOptions.fooOption}
                onChange={ev =>
                  dispatch({
                    type: "UPDATE_SEARCH_OPTION",
                    data: ev.target.checked
                  })
                }
              />
              Foo Option
            </label>
          </div>
          <div>
            <input
              type="text"
              value={searchQuery}
              placeholder="Search Query"
              onChange={ev =>
                dispatch({ type: "UPDATE_SEARCH_QUERY", data: ev.target.value })
              }
            />
          </div>
          <div>
            <button onClick={handleSearchButtonClick} disabled={startSearch}>
              {startSearch ? "searching..." : "Search"}
            </button>
          </div>
          <hr />
          <div>
            <label>Search Result: </label>
            <input type="text" readOnly={true} value={searchResult} />
          </div>
        </div>
      );
    }
    
    export default SearchPage;
    

    Codesandbox for that

    【讨论】:

    • 感谢您的链接。为了简单起见,我希望避免使用整个 useReducer 主题。此外,如果您有一个复杂的状态机,其中状态依赖于其他状态等,AFAIK 减速器最有意义。在我的情况下,它只是一个“即发即弃”按钮单击。仅仅为此而引入减速器是不对的。但我会重新考虑。
    • 我用useReducer版本更新了我的答案,请看看
    • 这个场景很奇怪,如果你只需要在点击按钮时调用api,你可以在handleSearchButtonClick 中调用它,如果你想在lastSearchButtonClickTimestamp 的状态下调用它,useReducer是好方法。
    • 是的,Federico Alecci 也提到了这一点。也许我很困惑,因为我可以在网上找到的所有关于数据获取的示例都使用useEffect。甚至官方的 React 文档也链接到 robinwieruch.de/react-hooks-fetch-data,其中仅描述了 useEffect。我以某种方式假设长时间运行的数据获取必须通过useEffect,以免搞砸 React 生命周期和/或渲染。我猜那是一个误解。对吗?
    • 不是这个原因。您找到的原因示例是想在第一次(初始渲染)之后调用 api,并且每当依赖项发生变化时。n 你的情况。我认为您不想搜索初始渲染。但是是的,最好把它放在useEffect 中,以确保只有在大多数情况下安装组件时才调用api。
    【解决方案3】:

    副作用并非为此而生。但是如果你想在变量发生变化时执行useEffect,你可以把它放在依赖数组中。

    例子

     function effect() {
           let [n, setN] = useState('')
           useEffect(() => {
                //some api need to call when 'n' value updated everytime.
             }, [n])
    
            //update the N variable with ur requirement
             const updateN = (val) => {
                setN(val)
              }
      }
    

    希望对你有帮助

    【讨论】:

    • 为什么没有副作用? AFAIK,从后端获取数据意味着由 useEffect() 实现,如robinwieruch.de/react-hooks-fetch-data 中所述,这是关于此主题的参考文章。我错过了什么?另外,请您详细说明您的答案。这与我的问题有什么关系?我已经使用了您在回答中所使用的技巧。
    最近更新 更多