问题
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
andsearchQuery
when you have this case, it's better to use reducer, so you just need to dispatch action ==>searchOptions
andsearchQuery
won't be insideuserEffect
. 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