Expo Router:如何避免router.push/navigate完成前当前页面重渲染?
Expo Router导航时避免组件重渲染闪烁的解决方案
问题重现
调用router.push()或router.navigate()时,当前页面组件会在导航完成前以初始状态重渲染,导致UI闪烁。比如搜索场景中,用户输入的文本会短暂变回占位符,再跳转到搜索结果页,体验糟糕。已尝试useRouter()、useFocusEffect及调整状态初始化策略,但问题仍存在。
核心原因分析
结合你的代码,问题主要源于两个点:
- 点击搜索按钮时,TextInput失去焦点触发
onBlur,将isFocused设为false,同时路由更新触发组件重渲染,导致placeholder意外显示。 - 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
相关产品推荐
相关产品推荐

