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

Expo Router:如何避免router.push/navigate完成前当前页面重渲染?

Expo Router导航时避免组件重渲染闪烁的解决方案

问题重现

调用router.push()或router.navigate()时,当前页面组件会在导航完成前以初始状态重渲染,导致UI闪烁。比如搜索场景中,用户输入的文本会短暂变回占位符,再跳转到搜索结果页,体验糟糕。已尝试useRouter()、useFocusEffect及调整状态初始化策略,但问题仍存在。

核心原因分析

结合你的代码,问题主要源于两个点:

  1. 点击搜索按钮时,TextInput失去焦点触发onBlur,将isFocused设为false,同时路由更新触发组件重渲染,导致placeholder意外显示。
  2. Expo Router在导航触发时会更新全局路由状态,所有依赖usePathname、useRouter的组件都会重渲染,若组件状态未正确隔离,可能出现初始状态回滚。

可行解决方案

1. 用useRef隔离查询状态,避免意外重置

将query从useState改为useRef,确保状态不会因组件重渲染或临时挂载变化而重置,同时减少不必要的重渲染:

import { useState, useRef } from 'react';
import { Alert, Image, TextInput, View, TouchableOpacity } from 'react-native';
import { router, usePathname } from 'expo-router';

import { type Video } from '../types/video';
import { colors } from '../constants';
import { icons } from '../constants';

import { useFirestoreContext } from '../services/db/firestore.context';

interface ISearchInput {
  placeholder?: string;
  initialQuery?: string;
  [key: string]: any;
}

const SearchInput = ({ placeholder, initialQuery, ...props }: ISearchInput) => {
  const pathname = usePathname();
  const queryRef = useRef<string>(initialQuery || ''); // 用useRef保存查询值
  const [isFocused, setIsFocused] = useState<boolean>(false);

  const { filterBySubstring } = useFirestoreContext<Video>();

  const handleSearch = () => {
    const searchQuery = queryRef.current.trim();
    if (!searchQuery) {
      return Alert.alert('Missing query', 'Please input something to search results across database');
    }

    filterBySubstring('title', searchQuery);

    if (pathname.startsWith('/search')) {
      router.setParams({ query: searchQuery });
    } else {
      router.push(`/search/${searchQuery}`);
    }
  };

  return (
    <View className={`w-full h-16 ${isFocused ? 'border-secondary' : 'border-black-200'} flex flex-row items-center`}>
      <TextInput
        style={{ includeFontPadding: false }}
        value={queryRef.current}
        placeholder={isFocused ? undefined : placeholder}
        placeholderTextColor={colors.gray[100]}
        onChangeText={(e) => queryRef.current = e} // 更新ref值
        onFocus={() => setIsFocused(true)}
        onBlur={() => setIsFocused(false)}
        {...props}
      />

      <TouchableOpacity onPress={handleSearch}>
        <Image source={icons.search} />
      </TouchableOpacity>
    </View>
  );
};

export default SearchInput;

2. 阻止导航过程中的状态回滚

添加一个isNavigating状态锁,在导航开始时阻止onBlur触发的状态更新,避免placeholder意外显示:

import { useState, useCallback } from 'react';
import { Alert, Image, TextInput, View, TouchableOpacity } from 'react-native';
import { router, usePathname } from 'expo-router';

import { type Video } from '../types/video';
import { colors } from '../constants';
import { icons } from '../constants';

import { useFirestoreContext } from '../services/db/firestore.context';

interface ISearchInput {
  placeholder?: string;
  initialQuery?: string;
  [key: string]: any;
}

const SearchInput = ({ placeholder, initialQuery, ...props }: ISearchInput) => {
  const pathname = usePathname();
  const [query, setQuery] = useState<string>(initialQuery || '');
  const [isFocused, setIsFocused] = useState<boolean>(false);
  const [isNavigating, setIsNavigating] = useState<boolean>(false); // 导航锁

  const { filterBySubstring } = useFirestoreContext<Video>();

  const handleSearch = useCallback(async () => {
    const searchQuery = query.trim();
    if (!searchQuery) {
      return Alert.alert('Missing query', 'Please input something to search results across database');
    }

    setIsNavigating(true); // 开启导航锁
    filterBySubstring('title', searchQuery);

    try {
      if (pathname.startsWith('/search')) {
        await router.setParams({ query: searchQuery });
      } else {
        await router.push(`/search/${searchQuery}`);
      }
    } finally {
      setIsNavigating(false); // 导航完成后解锁
    }
  }, [query, pathname, filterBySubstring]);

  return (
    <View className={`w-full h-16 ${isFocused ? 'border-secondary' : 'border-black-200'} flex flex-row items-center`}>
      <TextInput
        style={{ includeFontPadding: false }}
        value={query}
        placeholder={isFocused || isNavigating ? undefined : placeholder} // 导航时不显示placeholder
        placeholderTextColor={colors.gray[100]}
        onChangeText={setQuery}
        onFocus={() => !isNavigating && setIsFocused(true)}
        onBlur={() => !isNavigating && setIsFocused(false)} // 导航时不更新焦点状态
        {...props}
      />

      <TouchableOpacity onPress={handleSearch}>
        <Image source={icons.search} />
      </TouchableOpacity>
    </View>
  );
};

export default SearchInput;

3. 用useCallback优化事件处理函数

包裹handleSearch避免因组件重渲染导致函数重新创建,减少不必要的重渲染触发:

const handleSearch = useCallback(() => {
  // 原逻辑
}, [query, pathname, filterBySubstring]);

验证建议

  • 测试导航时的UI变化,确认搜索框内容不再闪烁
  • 检查组件重渲染次数(可使用React DevTools),确认不必要的重渲染已减少

内容的提问来源于stack exchange,提问作者Nir

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 11:15:19