React 18 + Suspense下点击按钮触发搜索的状态管理问题
我是React 18和Suspense的新手,之前一直用ASP.NET MVC开发。现在想实现一个功能:点击表单按钮,把表单输入值传给带[FromQuery]特性的Web API的HttpGet方法,再把返回结果渲染到div里。
在ASP.NET MVC里我是这么写按钮点击事件的:
const btnSearch = document.getElementById('btnSearch'); btnSearch.addEventListener("click", function() { executeMySearch(); return false; });
executeMySearch方法里获取表单值、调用接口并渲染HTML:
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;
但React的实现逻辑完全不一样,上面的代码只是用来说明需求。我现在的问题是:希望只有点击搜索按钮时才执行搜索,但现在点一次按钮后,用户修改表单输入会重复触发搜索。我知道原因是isSearch状态一直是true,表单输入变化触发组件重新渲染时会再次执行搜索。
我试过把setIsSearch传给MySearchResults组件,在渲染后设为false,但会导致搜索结果闪一下就消失;也试过在选择框变更时把isSearch设为false,但搜索结果会直接消失,都不符合预期。
后端Web API代码(可正常运行)
[HttpGet("MySearchMethod")] public async Task<List<MySearchResult>> MySearchMethod([FromQuery]MySearchForm mySearchForm) { return await _myRepository.GetMySearchResults(mySearchForm); }
现有React组件代码
搜索组件结构与状态
const MySearchComponent = () => { const [myEnum, setMyEnum] = useState(0); function onChangeMyEnum(myEnumId : number){ setMyEnum(myEnumId); } // 其他三个下拉框的状态与 onChange 方法... const [isSearch, setIsSearch] = useState(false); return ( <> <form> <div> <ErrorBoundary FallbackComponent={MyErrorHandler}> <h2>搜索条件选择</h2> <Suspense fallback={<Spinner/>}> <MyEnums onChange={onChangeMyEnum} /> {/* 其他三个懒加载的下拉选择组件 */} </Suspense> </ErrorBoundary> </div> <button className='btn btn-blue' onClick={(e) => { e.preventDefault(); setIsSearch(true); }}>搜索</button> </form> <div> { isSearch === true ? <ErrorBoundary FallbackComponent={MyErrorHandler}> <Suspense fallback={<Spinner/>}> <MySearchResults myEnum={myEnum} {/* 其他搜索条件 */} /> </Suspense> </ErrorBoundary> : <span> </span> } </div> </> ); };
MySearchResults组件(使用react-query)
import React from 'react'; import { useQuery } from 'react-query'; import MySearchResult from './MySearchResult'; const fetchMySearchResults = async (myEnumId : number /* 其他条件参数 */) => { let url = `${process.env.REACT_APP_MY_API}/GetMySearchResults/?myEnumId=${myEnumId}&/* 其他查询参数 */`; const response = await fetch(url); return response.json(); } const MySearchResults = (props : any) => { const {data} = useQuery( ['myquery', props.myEnum, /* 其他搜索条件 */], () => fetchMySearchResults(props.myEnum /* 其他搜索条件 */), { suspense:true } ); return ( <> <table> <thead> <tr> <th>列标题1</th> <th>列标题2</th> {/* 其他表头 */} </tr> </thead> <tbody> {data.map((mySearchResult: any) => ( {/* 渲染每条结果的逻辑 */} ))} </tbody> </table> </> ); }; export default MySearchResults;
解决方案
核心思路是把搜索条件的"提交版本"和"实时输入版本"分开,只有点击搜索按钮时,才将当前的实时输入值同步到提交版本,用提交版本作为查询的依赖,这样实时输入变化不会触发查询,只有点击按钮才会更新提交版本,进而触发搜索。
步骤1:修改搜索组件的状态管理
在MySearchComponent中新增一组状态,用来存储点击搜索按钮时的查询条件快照:
const MySearchComponent = () => { // 实时输入的状态(用户修改下拉框时更新) const [myEnum, setMyEnum] = useState(0); // 其他实时输入状态... // 提交版本的查询条件(只有点击搜索时才更新) const [searchParams, setSearchParams] = useState({ myEnum: 0, // 其他初始条件... }); const [isSearch, setIsSearch] = useState(false); // 搜索按钮点击事件:同步实时输入到提交版本,并触发搜索 const handleSearch = (e) => { e.preventDefault(); setSearchParams({ myEnum, // 同步其他实时输入状态... }); setIsSearch(true); }; // 其他下拉框的onChange方法不变... return ( <> <form> {/* 下拉选择组件部分不变,依然绑定实时状态 */} <button className='btn btn-blue' onClick={handleSearch}>搜索</button> </form> <div> {isSearch ? ( <ErrorBoundary FallbackComponent={MyErrorHandler}> <Suspense fallback={<Spinner/>}> {/* 把提交版本的searchParams传给结果组件,而不是实时状态 */} <MySearchResults {...searchParams} /> </Suspense> </ErrorBoundary> ) : <span> </span>} </div> </> ); };
步骤2:调整MySearchResults组件的查询依赖
将useQuery的查询键和参数改为从searchParams(提交版本)获取,这样只有当searchParams变化时(即点击搜索按钮时),才会重新执行查询:
const MySearchResults = (props : any) => { const {data} = useQuery( // 用提交版本的参数作为查询键 ['myquery', props.myEnum, /* 其他props中的提交参数 */], () => fetchMySearchResults(props.myEnum /* 其他props中的提交参数 */), { suspense:true, // 可选:设置staleTime,避免缓存自动失效重复请求 staleTime: 5 * 60 * 1000 // 5分钟 } ); // 渲染逻辑不变... };
可选优化:处理搜索结果的保留与清空
如果希望用户修改实时输入时,搜索结果依然保留,直到再次点击搜索,那么不需要在下拉框变更时修改isSearch。如果需要用户修改条件时清空结果,可以在每个下拉框的onChange方法中添加setIsSearch(false),但这样会清空结果,根据你的需求选择即可。
内容的提问来源于stack exchange,提问作者Tom Regan

