You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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组件不可行,理由如下:

  1. 搜索无结果时(totalResults为0,allImages为空),用户看不到任何提示,体验缺失
  2. 首次加载/刷新页面时,allImages为空的阶段会导致页面完全空白,直到接口返回数据,交互体验极差
  3. 切换分页时,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 21:15:35