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

React Native非首次加载时出现钩子渲染数量不一致错误求助

问题

在React Native(Expo)应用中,使用AsyncStorage的firstLoad变量控制渲染欢迎页或首页。首次加载(firstLoad未设置)时,先显示欢迎页再跳转首页正常;但当firstLoad为false直接渲染首页时,出现**"Rendered more hooks than during the previous render"**错误。

App.js 代码

export default class App extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      isLoading: true,
      showHome: false,
    }
  }

  componentDidMount() {
    AsyncStorage.getItem('firstLoad').then((value) => {
      if (value == null || value == 'true') {
        this.setState({ showHome: false, isLoading: false });
      } else {
        this.setState({ showHome: true, isLoading: false });
      }
    });
  }

  _onContinue = () => {
    AsyncStorage.setItem('firstLoad', 'false').then(() => {
      this.setState({ showHome: true, isLoading: false});
    });
  }

  render() {

    Appearance.addChangeListener(({ colorScheme }) => {
      this.forceUpdate();  
    });

    if (this.state.isLoading) {
      return (
        <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
          <ActivityIndicator size="large" color="#037bfc" />
        </View>
      );
    } else if (!this.state.showHome) {
      return (
        <Welcome onContinue={() => this._onContinue()} />
      );
    } else {
      return (
        <RootSiblingParent>
          <ActionSheetProvider>
            <NavigationContainer theme={
              Appearance.getColorScheme() === 'dark' ? DarkTheme : LightTheme
            }>
              <BottomNav />
            </NavigationContainer>
          </ActionSheetProvider>
        </RootSiblingParent>
      );
    }

  }
}

首页(Home.js)代码

export default function Home({route, navigation}) {

    const [imagesLoaded, setImagesLoaded] = useState(true);
    const [topStory, setTopStory] = useState({}); 
    const [topPost, setTopPost] = useState({});
    const [loading, setLoading] = useState(true);
    const [articles, setArticles] = useState([
        // article stuff here
    ]);

    const [fontsLoaded, fontError] = useFonts({
        'Open-Sans': require('../assets/fonts/Open_Sans/static/OpenSans-Regular.ttf'),
        'Madimi-One': require('../assets/fonts/Madimi_One/MadimiOne-Regular.ttf'),
    });

    const cardTheme = useColorScheme() === 'dark' ? ColorTheme.cardDark : ColorTheme.cardLight;
    const textTheme = useColorScheme() === 'dark' ? ColorTheme.textDark : ColorTheme.textLight;
    const blurhash = '|rF?hV%2WCj[ayj[a|j[az_NaeWBj@ayfRayfQfQM{M|azj[azf6fQfQfQIpWXofj[ayj[j[fQayWCoeoeaya}j[ayfQa{oLj?j[WVj[ayayj[fQoff7azayj[ayj[j[ayofayayayj[fQj[ayayj[ayfjj[j[ayjuayj[';

    if (!fontsLoaded || !imagesLoaded || fontError) {
        return (
            <SafeAreaView style={styles.container}>
                <ActivityIndicator size="large" color="#0000ff" />
            </SafeAreaView>
        );
    }

    const fetchTopStory = async () => {
        try {
            const postRef = ref(db, 'reports');
            const postSnapshot = await get(postRef).then((snapshot) => {
                if (snapshot.exists()) {
                    const posts = snapshot.val();
                    const postKeys = Object.keys(posts);
                    const postsWithImages = postKeys.filter(key => posts[key].images !== undefined);
                    const postsWithoutImages = postKeys.filter(key => posts[key].images === undefined);

                    const topStoryKey = postsWithImages[Math.floor(Math.random() * postsWithImages.length)];
                    const topStory = posts[topStoryKey];

                    const topPostKey = postsWithoutImages[Math.floor(Math.random() * postsWithoutImages.length)];
                    const topPost = posts[topPostKey];

                    setTopStory(topStory);
                    setTopPost(topPost);
                    setLoading(false);
                } else {
                    console.log("No data available");
                }
            });
        } catch (error) {
            console.error(error);
        }
    }


    useEffect(() => {
        fetchTopStory();
    }, []);

    const refresh = () => {
        setLoading(true);
        fetchTopStory();
    }

    return (
        <SafeAreaView style={styles.container}>
            {loading ? (
                <ActivityIndicator size="large" color="#0000ff" />
            ) : (
                <ScrollView 
                    contentContainerStyle={styles.content}
                    refreshControl={
                        <RefreshControl refreshing={loading} onRefresh={() => refresh()} />
                    }
                >
                    // content is shown here
                </ScrollView>
            )}
        </SafeAreaView>
    );
}

解决方法

1. 修复App.js中的Appearance监听问题

render方法中重复添加Appearance监听器会触发不必要的重渲染,间接干扰子组件Hook执行顺序。将监听器移到componentDidMount中,并在组件卸载时移除:

componentDidMount() {
    AsyncStorage.getItem('firstLoad').then((value) => {
      if (value == null || value == 'true') {
        this.setState({ showHome: false, isLoading: false });
      } else {
        this.setState({ showHome: true, isLoading: false });
      }
    });

    // 移动监听器到componentDidMount
    this.colorSchemeListener = Appearance.addChangeListener(({ colorScheme }) => {
      this.setState({}); // 用setState替代forceUpdate,符合React规范
    });
  }

componentWillUnmount() {
    // 移除监听器,避免内存泄漏
    this.colorSchemeListener?.remove();
}

2. 修复Home组件的Hook执行顺序问题

React要求Hook必须在组件顶层执行,不能在条件判断之后。当前Home组件在字体未加载时提前返回,导致后续Hook不执行,字体加载完成后重渲染时Hook数量变化,触发错误。调整代码结构,确保所有Hook在条件判断前执行:

export default function Home({route, navigation}) {
    // 所有Hook必须放在组件顶层
    const [imagesLoaded, setImagesLoaded] = useState(true);
    const [topStory, setTopStory] = useState({}); 
    const [topPost, setTopPost] = useState({});
    const [loading, setLoading] = useState(true);
    const [articles, setArticles] = useState([
        // article stuff here
    ]);

    const [fontsLoaded, fontError] = useFonts({
        'Open-Sans': require('../assets/fonts/Open_Sans/static/OpenSans-Regular.ttf'),
        'Madimi-One': require('../assets/fonts/Madimi_One/MadimiOne-Regular.ttf'),
    });

    const cardTheme = useColorScheme() === 'dark' ? ColorTheme.cardDark : ColorTheme.cardLight;
    const textTheme = useColorScheme() === 'dark' ? ColorTheme.textDark : ColorTheme.textLight;
    const blurhash = '|rF?hV%2WCj[ayj[a|j[az_NaeWBj@ayfRayfQfQM{M|azj[azf6fQfQfQIpWXofj[ayj[j[fQayWCoeoeaya}j[ayfQa{oLj?j[WVj[ayayj[fQoff7azayj[ayj[j[ayofayayayj[fQj[ayayj[ayfjj[j[ayjuayj[';

    useEffect(() => {
        const fetchTopStory = async () => {
            try {
                const postRef = ref(db, 'reports');
                const snapshot = await get(postRef);
                if (snapshot.exists()) {
                    const posts = snapshot.val();
                    const postKeys = Object.keys(posts);
                    const postsWithImages = postKeys.filter(key => posts[key].images !== undefined);
                    const postsWithoutImages = postKeys.filter(key => posts[key].images === undefined);

                    const topStoryKey = postsWithImages[Math.floor(Math.random() * postsWithImages.length)];
                    const topStory = posts[topStoryKey];

                    const topPostKey = postsWithoutImages[Math.floor(Math.random() * postsWithoutImages.length)];
                    const topPost = posts[topPostKey];

                    setTopStory(topStory);
                    setTopPost(topPost);
                    setLoading(false);
                } else {
                    console.log("No data available");
                }
            } catch (error) {
                console.error(error);
            }
        }

        // 仅在字体加载完成后执行数据请求
        if (fontsLoaded && !fontError) {
            fetchTopStory();
        }
    }, [fontsLoaded, fontError]);

    const refresh = () => {
        setLoading(true);
        const fetchTopStory = async () => {
            try {
                const postRef = ref(db, 'reports');
                const snapshot = await get(postRef);
                if (snapshot.exists()) {
                    const posts = snapshot.val();
                    const postKeys = Object.keys(posts);
                    const postsWithImages = postKeys.filter(key => posts[key].images !== undefined);
                    const postsWithoutImages = postKeys.filter(key => posts[key].images === undefined);

                    const topStoryKey = postsWithImages[Math.floor(Math.random() * postsWithImages.length)];
                    const topStory = posts[topStoryKey];

                    const topPostKey = postsWithoutImages[Math.floor(Math.random() * postsWithoutImages.length)];
                    const topPost = posts[topPostKey];

                    setTopStory(topStory);
                    setTopPost(topPost);
                    setLoading(false);
                } else {
                    console.log("No data available");
                }
            } catch (error) {
                console.error(error);
            }
        }
        fetchTopStory();
    }

    // 条件判断放在所有Hook之后
    if (!fontsLoaded || !imagesLoaded || fontError) {
        return (
            <SafeAreaView style={styles.container}>
                <ActivityIndicator size="large" color="#0000ff" />
            </SafeAreaView>
        );
    }

    return (
        <SafeAreaView style={styles.container}>
            {loading ? (
                <ActivityIndicator size="large" color="#0000ff" />
            ) : (
                <ScrollView 
                    contentContainerStyle={styles.content}
                    refreshControl={
                        <RefreshControl refreshing={loading} onRefresh={() => refresh()} />
                    }
                >
                    // content is shown here
                </ScrollView>
            )}
        </SafeAreaView>
    );
}

关键说明

  • Hook执行规则:组件每次渲染时,Hook的调用顺序和数量必须完全一致,禁止在条件语句、循环或嵌套函数中调用Hook。
  • 监听器管理:避免在render中重复绑定监听器,组件卸载时必须移除监听器,防止内存泄漏。
  • 逻辑拆分:数据请求逻辑移到useEffect中,通过依赖项控制执行时机,确保仅在必要时触发请求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 02:35:05