I'm new to React 18 and Suspense. Nearly all of my previous web development was done in asp.net mvc. I want to click a button on a form, pass the form input values to a web api HttpGet method with the [FromQuery] attribute, and render the return into a div.
If I were doing this in asp.net mvc, I would wire up a button click event like so in javascript:
const btnSearch = document.getElementById('btnSearch');
btnSearch.addEventListener("click", function() {
executeMySearch();
return false;
});
And in the executeMySearch() method I'd grab the form input values, send them to server, fetch some html from the server and plunk it into a div like so:
const searchresults = document.getElementById('searchresults');
let formData = new FormData(document.forms[0]);
fetch('/Index?handler=MySearchMethod', {
method: 'post',
body: new URLSearchParams(formData),
}).then(function (response) {
return response.text();
}).then(function (html) {
searchresults.innerHTML = html;
Of course in React the approach is completely different, I showed the code above only to demonstrate what I want to happen. I want the search to execute only when the search button is clicked. My problem is, I cannot figure out how to manage React state to make that happen. Currently, after the search button is clicked once, my search is executing every time the user changes the value of a form input. I understand why that is happening, but I can't figure out how to structure my components so that the search executes only when the search button is clicked.
Server-side, my web api receives a form and returns a generic list, like so. This works fine:
[HttpGet("MySearchMethod")]
public async Task<List<MySearchResult>> MySearchMethod([FromQuery]MySearchForm mySearchForm)
{
return await _myRepository.GetMySearchResults(mySearchForm);
}
In my React app I have a search component. The component renders a form with the following elements:
- four selects, which contain the search criteria. These selects are wrapped in React components.
- a search button
- a component that renders the search results
Each select input is a React component that contains a list of enums fetched from the web api. Each select is defined in the search component like so:
const MyEnums = lazy(() => import('./MyEnums'));
Each of these React components is tied to the React state when the search component is defined, like so:
const MySearchComponent = () => {
const [myEnum, setMyEnum] = useState(0);
function onChangeMyEnum(myEnumId : number){
setMyEnum(myEnumId);
}...
and I tie my search button to React state like so:
const [isSearch, setIsSearch] = useState(false);
My search component returns a form with the search criteria and search button, and a div to contain the search results:
return (
<>
<form>
<div>
<ErrorBoundary FallbackComponent={MyErrorHandler}>
<h2>My Search Criteria Select</h2>
<Suspense fallback={<Spinner/>}>
<MyEnums onChange={onChangeMyEnum} />
</Suspense>
</ErrorBoundary>
</div>
<button className='btn btn-blue' onClick={(e) => {
e.preventDefault();
setIsSearch(true);
}
}>Search</button>
</form>
<div>
{
isSearch === true ?
<ErrorBoundary FallbackComponent={MyErrorHandler}>
<Suspense fallback={<Spinner/>}>
<MySearchResults myEnum={myEnum} [..other search criteria] />
</Suspense>
</ErrorBoundary>
: <span> </span>
}
</div>
Everything works fine. The problem is, after the first time the search button is clicked (which executes "setIsSearch(true)"), every time a user alters a selection in one of the form inputs, the search executes. I understand why. My "isSearch" variable remains true, so when the state is altered by the form input changing, and the component is re-rendered, the search happens again.
I tried passing the "setIsSearch" method into the MySearchResults component, and calling setIsSearch(false) after the component rendered, but that of course does exactly what it is supposed to to. The React state changes, the component re-renders, it sees that "isSearch" is false, and it makes the search results disappear. When I click my search button I see the search results flicker briefly and then disappear, which is exactly what should happen.
I also tried calling setIsSearch(false) every time a select changes, but of course this causes my search results to disappear, which is not desired.
What am I missing? How do I structure this so that the search only occurs when I click the Search button?
P.S. the web api call is made inside of the MySearchResults component when it renders. The MySearchResults component looks like this:
import React from 'react';
import { useQuery } from 'react-query';
import MySearchResult from './MySearchResult';
const fetchMySearchResults = async (myEnumId : number [...other criteria]) => {
let url = `${process.env.REACT_APP_MY_API}/GetMySearchResults/?myEnumId=${myEnumId}&[...other criterial]`;
const response = await fetch(url);
return response.json();
}
const MySearchResults = (props : any) => {
const {data} = useQuery(['myquery', props.myEnum,...other search criteria...]
,() => fetchMySearchResults(props.myEnun [...other search criteria]),
{
suspense:true
});
return (
<>
<table>
<thead>
<tr>
<th>My Column Header</th>
<th>...and so on</th>
</tr>
</thead>
<tbody>
{data.map((mySearchResult: {
...and so on
</tbody>
</table>
</>
);
};
export default MySearchResults;
CodePudding user response:
Move the useQuery
hook up from MySearchResults
to MySearchComponent
. Use the enabled
flag in your useQuery
function, which is the flag which prevents the query from automatically running: -
const {data} = useQuery(['myquery', props.myEnum,...other search criteria...]
,() => fetchMySearchResults(props.myEnun [...other search criteria]),
{
enabled: isSearch,
suspense:true
}
);
When isSearch
is set to true (which you do when you click on Search button), the query will execute. And when you get data
then render your MySearchResults
component providing data
as props.
Afterwards, if you want to trigger search function again, you can use refetch
function provided by useQuery
hook in your Search button's event handler like so: -
const {data, refetch} = useQuery();
const onSearchClick = (e) => {
e.preventDefault();
if(isSearch === true){
refetch();
}else{
setIsSearch(true);
}
}
For full reference, visit here: -
https://react-query-v3.tanstack.com/reference/useQuery
CodePudding user response:
Use 2 useState:
const [myEnum, setMyEnum] = useState(0);
const [searchEnum, setSearchEnum] = useState();
setIsSearch(true);
function onChangeMyEnum(myEnumId : number){
setMyEnum(myEnumId);
}
<Suspense fallback={<Spinner/>}>
<MyEnums onChange={onChangeMyEnum} />
</Suspense>
<button className='btn btn-blue' onClick={(e) => {
e.preventDefault();
setSearchEnum(myEnum)
setIsSearch(true);
}
}>Search</button>
{
isSearch === true ?
<ErrorBoundary FallbackComponent={MyErrorHandler}>
<Suspense fallback={<Spinner/>}>
<MySearchResults myEnum={searchEnum} [..other search criteria] />
</Suspense>
</ErrorBoundary>
: <span> </span>
}
if you use only myEnum every time someone types a new value it triggers a rendering which triggers your search. If you separate the input and the search by setting the value that trigger the search when you click on the button your user can type in the input but only the click will trigger the search. Because only the click will change the value that the search is listening too.
CodePudding user response:
Most of the answer is this--if you don't want the component to re-render, don't set a variable with useState(). Employ useRef() instead.
So, each select component's change handler is defined like this now:
const myEnum = useRef(0);
function onChangeMyEnum(myEnumId : number){
myEnum.current = myEnumId;
}
This is what it looked like before, when I (wrongly) called useState:
const [myEnum, setMyEnum] = useState(0);
function onChangeMyEnum(myEnumId : number){
setMyEnum(myEnumId);
}
And I pass these variables to my component the same way I did before:
<div>
<ErrorBoundary FallbackComponent={MyErrorHandler}>
<h2>My Search Criteria Select</h2>
<Suspense fallback={<Spinner/>}>
<MyEnums onChange={onChangeMyEnum} />
</Suspense>
</ErrorBoundary>
</div>
The prevents the problem of every change in the select causing the component to re-render.
This caused a different problem--the "MySearchResults" component did not re-render after the first button click. The button click event calls "setIsSearch(true);". This caused a rendering of the search results exactly once. To fix that, I pass the setIsSearch method into the props of the MySearchResults component. Inside MySearchResults I call setIsSearch(false). This causes the desired change of state.
The final piece of the puzzle is to pass the isSearch variable into the MySearchResults component. Per the @ZaeemKhaliq answer above, I added that to my useQuery call, so that I'm not going through the expense of running the query unless the user has clicked the search button. This is needed because useQuery cannot be run conditionally; that is to say, in the TemplateSearchResults component I can't simply put an if block above the useQuery statement and return if props.isSearch is false.
Here is what the MySearchResults component looks like now:
import React from 'react';
import { useQuery } from 'react-query';
import TemplateSearchResult from './TemplateSearchResult';
const fetchTemplates = async (carrierGroupId : number, stateId : number, policyTypeId : number, activityStatusCode: string) => {
let url = `${process.env.REACT_APP_PMSYS_API}/GetTemplates/?carrierGroupId=${carrierGroupId}&stateId=${stateId}&policyTypeId=${policyTypeId}&activityStatusCode=${activityStatusCode}`;
console.log('fetchTemplates: ' url)
const response = await fetch(url);
return response.json();
}
const TemplateSearchResults = (props : any) => {
//fyi, you cannot return here if props.IsSearch===false, compiler barks at you. useQuery cannot be called conditionally.
const {data} = useQuery(['templates', props.carrierGroup,props.state,props.policyType,props.activityStatusCode]
,() => fetchTemplates(props.carrierGroup.current,props.state.current,props.policyType.current,props.activityStatusCode.current),
{
suspense:true,
enabled:props.isSearch
});
props.setIsSearch(false);
return (data === undefined ? <span>No search was done</span> :
<>
<table className='table-auto'>
<thead>
<tr>
<th></th>
<th>File Name</th>
<th>Groups</th>
<th>Types</th>
<th>Title</th>
<th>Form</th>
<th>State</th>
<th>Active</th>
<th>Required</th>
<th>Client Specific</th>
<th>Checked Out</th>
</tr>
</thead>
<tbody>
{data.map((templateSearchResult: {
active : boolean ,
carrierGroupNumbers: string,
checkedOut : string,
fileName: string,
formType: string,
isClientSpecific: boolean,
policyTypes: string,
required: boolean,
stateName: string,
templateId:number,
title:string}) => <TemplateSearchResult key={templateSearchResult.templateId}
active={templateSearchResult.active}
carrierGroupNumbers={templateSearchResult.carrierGroupNumbers}
checkedOut={templateSearchResult.checkedOut}
fileName={templateSearchResult.fileName}
formType={templateSearchResult.formType}
isClientSpecific={templateSearchResult.isClientSpecific}
policyTypes={templateSearchResult.policyTypes}
required={templateSearchResult.required}
stateName={templateSearchResult.stateName}
templateId={templateSearchResult.templateId}
title={templateSearchResult.title}/>)}
</tbody>
</table>
</>
);
};
export default TemplateSearchResults;
On my parent search page I removed the "if" block around the rendering of the search results component. Instead it just renders, passing the "isSearch" and "setIsSearch" variables so that the component can decide what to render:
<div className='w-auto'>
<ErrorBoundary FallbackComponent={TemplatesErrorHandler}>
<Suspense fallback={<Spinner/>}>
<TemplateSearchResults isSearch={isSearch} setIsSearch={setIsSearch} carrierGroup={carrierGroup} state={state} policyType={policyType} activityStatusCode={activityStatusCode} />
</Suspense>
</ErrorBoundary>
</div>
Wow. All of this reminds me of the old criticism of nuclear power: "helluva complex way to boil water." This is one helluva complex way to generate html.