React Router 6:仅当状态数组非空时渲染组件
问题
我目前在React项目中处理搜索参数和URL跳转的方式不够优雅,且由于Header组件里handleSubmit下方的第一个useEffect,Search组件出现了大量不必要的渲染。比如搜索页面刷新时,Search组件会渲染7次(其中5次allImages为空,2次是已获取到图片的状态)。我想加个条件,只在allImages状态数组非空(也就是已获取到图片)时才渲染Search组件,想问这个方案可行吗?
相关代码
Header.js
import React from 'react' import Navbar from './Navbar' import create from 'zustand' import ErrorMsg, { useError } from './ErrorMsg' import { useEffect } from 'react' import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' // Zustand let store = (set) => ({ input: '', setInput: (value) => set({ input: value }), allImages: [], setAllImages: (images) => set({ allImages: images}), totalResults: null, setTotalResults: (num) => set({ totalResults: num}), }) export const useHeader = create(store) function Header() { // global state and search params let navigate = useNavigate() const location = useLocation() const [searchParams] = useSearchParams() const query = searchParams.get('query') const page = Number(searchParams.get('page') || 1) const input = useHeader(state => state.input) const setInput = useHeader(state => state.setInput) const allImages = useHeader(state => state.allImages) const setAllImages = useHeader(state => state.setAllImages) const setTotalResults = useHeader(state => state.setTotalResults) const error = useError(state => state.error) const setError = useError(state => state.setError) const showError = useError(state => state.showError) const setShowError = useError(state => state.setShowError) const setFadeOut = useError(state => state.setFadeOut) function handleChange(event) { setInput(event.target.value) } async function fetchImages() { try { const res = await fetch(`https://api.unsplash.com/search/photos?&page=${page}&per_page=30&query=${input}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`) const data = await res.json() if (data.total !== 0) { setAllImages(data.results) setTotalResults(data.total) } else { setAllImages([]) setTotalResults(0) } } catch(error) { setError(error) } } const handleSubmit = async (event) => { event.preventDefault() navigate(`/search?query=${input}&page=1`) } // this useEffect causes Search.js to render too many times // especially the second conditional need improvement useEffect(() => { if (location.pathname === '/search' && allImages.length === 0) { if (query) { setInput(query) } navigate(`/search?query=${input}&page=${page}`) fetchImages() } // need this to deal with page not refreshing when submitting or changing pages if (location.pathname === '/search' && allImages.length !== 0) { fetchImages() } // eslint-disable-next-line }, [searchParams]) // error useEffect(() => { if (error) { setShowError(true) setTimeout(() => { setFadeOut(true) setTimeout(() => { setShowError(false) }, 1000) }, 5000) } }, [error, setFadeOut, setShowError]) return ( <div className='header'> <Navbar /> <h2 className='header--heading text-center text-light'>Find Images</h2> <div className='header--form'> <form onSubmit={handleSubmit}> <input className='header--form--input' autoComplete='off' type='text' placeholder='Search' onChange={handleChange} name='input' value={input} /> </form> </div> {showError && <ErrorMsg />} </div> ) } export default Header
Search.js
import React from 'react' import Header from '../Header' import Image from '../Image' import { useHeader } from '../Header'; import { useSearchParams } from 'react-router-dom'; function Search() { const [searchParams, setSearchParams] = useSearchParams() const page = Number(searchParams.get('page') || 1) const allImages = useHeader(state => state.allImages) const totalResults = useHeader(state => state.totalResults) console.log(allImages) console.log('Search.js rendered') // pages function handlePrev() { setSearchParams(params => { params.set("page", Math.max(1, page - 1)) return params }) } function handleNext() { setSearchParams(params => { params.set("page", page + 1) return params }) } return ( <div> <Header /> {/* {totalResults === 0 && <p>Nothing Found</p>} */} <div className='image-list mt-5 pb-5'> {allImages.map(el => ( <Image key={el.id} // do need spread operator below for img's src to work in Image.js {...el} el={el} /> ))} </div> {allImages.length !== 0 && <div className='pagination'> <button disabled={page === 1} onClick={handlePrev}> Prev </button> <h5 className='pagination--h5'>{page}</h5> <button disabled={totalResults < 31} onClick={handleNext}> Next </button> </div>} </div> ) } export default Search
解决方案
方案可行性判断
直接只在allImages非空时渲染Search组件不可行,理由如下:
- 搜索无结果时(
totalResults为0,allImages为空),用户看不到任何提示,体验缺失 - 首次加载/刷新页面时,
allImages为空的阶段会导致页面完全空白,直到接口返回数据,交互体验极差 - 切换分页时,
allImages会被重新赋值,短暂为空的阶段会导致页面闪烁
优化渲染次数的正确思路
核心问题出在Header里的useEffect逻辑混乱,以及状态依赖处理不当,以下是针对性优化:
1. 重构Header中的useEffect逻辑
原useEffect依赖searchParams,但内部又调用navigate修改searchParams,导致无限循环触发渲染;同时重复调用fetchImages,加剧渲染次数。修改如下:
useEffect(() => { if (location.pathname !== '/search') return; // 同步URL参数到input状态,仅当不一致时更新 if (query && input !== query) { setInput(query); } // 仅当query存在时发起请求,避免无意义调用 if (query) { const fetchWithUrlParams = async () => { try { // 直接用URL中的query和page,避免依赖input状态同步延迟 const res = await fetch(`https://api.unsplash.com/search/photos?page=${page}&per_page=30&query=${query}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`); const data = await res.json(); setAllImages(data.results || []); setTotalResults(data.total || 0); } catch (error) { setError(error); } }; fetchWithUrlParams(); } }, [location.pathname, query, page, input, setInput, setAllImages, setTotalResults, setError]);
- 移除内部
navigate调用,避免修改searchParams触发重复渲染 - 直接使用URL中的
query和page发起请求,无需依赖input状态同步 - 增加条件判断,仅在
query存在时才请求,避免空请求
2. 优化Search组件的渲染
- 用
React.memo包裹Search组件,减少不必要的重渲染:
export default React.memo(Search);
- 恢复空状态提示,在
allImages为空但totalResults为0时显示:
{totalResults === 0 && <p>未找到相关图片</p>}
- 添加加载状态:在
zustand中新增isLoading状态,请求发起时设为true,结束后设为false,Search组件中显示加载动画:
// 在zustand store中添加 isLoading: false, setIsLoading: (bool) => set({ isLoading: bool }), // fetchImages中修改 async function fetchImages() { setIsLoading(true); try { // ...原有请求逻辑 } catch(error) { setError(error); } finally { setIsLoading(false); } } // Search组件中使用 const isLoading = useHeader(state => state.isLoading); {isLoading && <div>加载中...</div>}
3. 其他优化点
- 移除
Header组件中allImages的依赖,因为useEffect不需要监听它,避免额外渲染 - 优化分页逻辑,避免闭包问题:
function handlePrev() { setSearchParams(prev => { const currentPage = Number(prev.get('page') || 1); prev.set('page', Math.max(1, currentPage - 1)); return prev; }); } function handleNext() { setSearchParams(prev => { const currentPage = Number(prev.get('page') || 1); prev.set('page', currentPage + 1); return prev; }); }
总结
不要通过隐藏Search组件来减少渲染,而是通过优化状态依赖、避免循环触发、使用React.memo、添加加载/空状态来解决问题,既保证渲染效率,又不破坏用户体验。
内容的提问来源于stack exchange,提问作者haharimoto
相关产品推荐
相关产品推荐

