如何仅在按钮单击时触发具有多个依赖项的 useEffect() 而没有其他任何内容

IT技术 reactjs typescript react-hooks
2021-05-10 11:54:49

我有以下代码:

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

另请参阅此 Codesandbox

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

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

React Hook useEffect 缺少依赖项:“props.user.loginName”、“searchFilter”和“searchQuery”。包括它们或删除依赖项数组。[react-hooks/详尽的deps]

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

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

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

更新 1:

我还提出了这个解决方案,它看起来像:

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被立刻执行。有人知道为什么吗?

3个回答

您可以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])

这就是 useEffect 应该的样子。

  • props.userName 在依赖项列表中是有意义的,因为我们肯定希望在用户名更改时获取新数据。
  • searchOptionssearchQuery当你有这样的情况下,最好是使用减速机,所以你只需要派遣行动==>searchOptionssearchQuery不会在里面userEffectDan 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;

代码沙盒

副作用不是为了那个。但是如果你想在变量发生变化时执行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)
          }
  }

希望这可以帮助