javascriptreactjsreact-nativeexpoasyncstorage

React native "rendered more hooks than during the previous render" when not first load


In my React Native (expo) app, I have a firstLoad variable in AsyncStorage to determine whether the welcome screen or home screen is rendered. The app loads the welcome screen followed by the home screen perfectly well when firstLoad isn't set (meaning it is the first load). However, I get a “Rendered more hooks than during the previous render” error when firstLoad is false and the home screen needs to be rendered directly.

Here’s my App class in 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>
      );
    }

  }
}

And this is what my code for the Home Screen looks like:

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>
    );
}

Any help is appreciated. Thank you!


Solution

  • The Problem

    You have a useEffect that is being rendered conditionally. According to the Rules of Hooks, you should not call hooks inside loops, conditions or nested functions. You can think of an Early Return as putting everything that comes after the if as inside an else block.

    What do I do?

    Try moving the early return so that it is AFTER your useEffect, so that:

    const fetchTopStory = async () => {
        // Your effect
    }
    
    useEffect(() => {
        fetchTopStory();
    }, []);
    
    if (!fontsLoaded || !imagesLoaded || fontError) {
        return (
            <SafeAreaView style={styles.container}>
                <ActivityIndicator size="large" color="#0000ff" />
            </SafeAreaView>
        );
    }