How to trigger useEffect() with multiple dependencies only on button click and on nothing else

半腔热情 提交于 2021-02-11 12:28:51

问题


I have the following code:

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

also see this Codesandbox.

The code works fine. I can change the search query in the text field and click the option checkbox. Once, I am ready, I can click the "Search" button and only then the side effect occurs by fetching the data.

Now, the problem is that the compiler complains about:

React Hook useEffect has missing dependencies: 'props.user.loginName', 'searchFilter', and 'searchQuery'. Either include them or remove the dependency array. [react-hooks/exhaustive-deps]

However, if I add props.user.loginName, searchFilter and searchQuery to the dependency list, then the side effect is triggered whenever I click the checkbox or type a single character in the text field.

I do understand the concept of hook dependencies, but I don't know how to first enter some data and only with a button click trigger the side effect.

What is the best practice for this? I have read both https://reactjs.org/docs/hooks-effect.html and https://www.robinwieruch.de/react-hooks-fetch-data but couldn't find any example concerning my question.

Update 1:

I have also come up with this solution which looks like:

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

Now the actual function is the only dependency which works fine and the compiler is happy, too. However, what I do not like, is that fact that I need that wrapper object with the call property. If I want to pass an arrow function directly to the state, this does not work as expected, e.g.:

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

The doSearch is not set to the arrow function, but callSearchApi is executed straight away. Does anybody know why?


回答1:


You could remove setIsSearching(true) from your effect, and set it apart when you click your button.

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

Then, you can modify your useEffect statement like this:

    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

This will accomplish what you are looking for. Another way would be just disabling the react-hooks/exhaustive-deps rule.

If you just need to trigger the fetch only when the button is clicked, I'd just use a function.

useEffect is useful for instance, when you have a list of filters (toggles), and you want to make a fetch every time you toggle one filter (imagine an e-commerce). This is a naive example, but it makes the point:

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



回答2:


That is how useEffect supposed to be.

  • props.userName make sense to be in the dependency list, cuz we definitely want to fetch new data when userName is changed.
  • searchOptions and searchQuery when you have this case, it's better to use reducer, so you just need to dispatch action ==> searchOptions and searchQuery won't be inside userEffect. This article from Dan Abramov provides deep explanation and they simple example implementing it

I quickly convert your example using useReducer, please have a look

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




回答3:


Side effects are not meant for that. But if you want to execute the useEffect when there is change in variable, you can put that in dependency array.

Example

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

Hope this helps



来源:https://stackoverflow.com/questions/62595701/how-to-trigger-useeffect-with-multiple-dependencies-only-on-button-click-and-o

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!