Search code examples
firebasereact-nativefirebase-authenticationwarningsrerender

React Native - Warning: Cannot update a component from inside the function body of a different component


I know there are other answers on here for this but I can't find one that works the way my app is structured. I set up my app with Firebase Authentication using their documentation but once it showed how to create a persistent log in, it didn't show how to logout and re-render the Home Screen.

This is the warning I can't resolve:

Warning: Cannot update a component from inside the function body of a different component. at [native code]:null in dispatchAction at node_modules/@react-navigation/core/src/useNavigationCache.tsx:100:22 in acc.route.key.setOptions at node_modules/@firebase/webchannel-wrapper/dist/index.js:42:151 in Rb$argument_0 at [native code]:null in performSyncWorkOnRoot at [native code]:null in forEach at node_modules/react-native/Libraries/Core/setUpReactRefresh.js:43:6 in Refresh.performReactRefresh at [native code]:null in callFunctionReturnFlushedQueue

Here is my App.js file:

import React, { useState, useEffect } from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { NavigationContainer } from '@react-navigation/native';
import { Login, Home, Events, CreateEvent, EventDetails } from './src/screens/';
import { Registration } from './src/navigation';
import { firebase } from './src/firebase/config';
import { TouchableOpacity, Image, View } from 'react-native';
import Logout from './src/screens/Logout/Logout';

const Stack = createStackNavigator();

export default function App() {
  const [loading, setLoading] = useState(true)
  const [user, setUser] = useState(null);

  useEffect(() => {
    const usersRef = firebase.firestore().collection('users');
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        usersRef
          .doc(user.uid)
          .get()
          .then((document) => {
            const userData = document.data()
            setLoading(false)
            setUser(userData);
          })
          .catch((error) => {
            setLoading(false)
          });
      } else {
        setLoading(false)
      }
    });
  }, [user]);

  if (loading) {    
    return (    
      <></> 
    )   
  }

  return (
    <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen 
            name="Home"  
            options={{ 
              headerTitle: 'Showday',
              headerTitleAlign: 'center',
              headerRight: () => (
                <TouchableOpacity 
                  onPress={() => alert('This is the menu button')}>
                    <Image source={require('./assets/menuicon.png')} />
                </TouchableOpacity>
              ),
              headerLeftContainerStyle: {marginLeft: 20},
              headerRightContainerStyle: {marginRight: 20}}}>
                {props => <Home {...props} extraData={user} />}
              </Stack.Screen>
          <Stack.Screen 
            name="Events" 
            options={{ 
              headerTitle: 'Events',
              headerTitleAlign: 'center',
              headerRight: () => (
                <TouchableOpacity 
                  onPress={() => alert('This is the menu button')} 
                  style={{ marginLeft: 20}} >
                    <Image source={require('./assets/menuicon.png')} />
                </TouchableOpacity>
              ),
            headerRightContainerStyle: {marginRight: 20}}}>
              {props => <Events {...props} extraData={user} />}
            </Stack.Screen>
          <Stack.Screen 
            name='CreateEvent'
            options={{ 
              headerTitle: 'Create New Event',
              headerTitleAlign: 'center',
              headerRight: () => (
                <TouchableOpacity 
                  onPress={() => alert('This is the menu button')} 
                  style={{ marginLeft: 20}} >
                    <Image source={require('./assets/menuicon.png')} />
                </TouchableOpacity>
              ),
            headerRightContainerStyle: {marginRight: 20}}}>
              {props => <CreateEvent {...props} extraData={user} />}
          </Stack.Screen>
          <Stack.Screen name='EventDetails' options={{headerTitle: 'Event Details', headerTitleAlign: 'center'}}>
            {props => <EventDetails {...props} />}
          </Stack.Screen>
          <Stack.Screen name='Login' component={Login}/>
          <Stack.Screen name='Logout' component={Logout} />
          <Stack.Screen name='Register' component={Registration} />
        </Stack.Navigator>
    </NavigationContainer>
  );
}

Here is my Home.js file which displays Login or Logout depending if the user is signed in or not:

import React, { useState } from 'react';
import { useNavigation } from '@react-navigation/native';
import { Text, View, TouchableOpacity, Image } from 'react-native';
import { firebase } from '../../firebase/config';
import styles from './HomeStyles';


function Home(props) {
    const [loginButtonDisplay, setLoginButtonDisplay] = useState(true)
    const user = props.extraData;
    const nav = useNavigation();

    const login = () => {
        nav.navigate('Login');
      }
    
    const logout = () => {
        nav.navigate('Logout');
    }
    const renderLoginLogout = () => {
        if (user) {
            setLoginButtonDisplay(false);
        }
        if (loginButtonDisplay) {
            return (
                <TouchableOpacity
                    onPress={login}>
                    <Text>Login</Text>
                    {/* <Image source={require('../../../assets/login.png')} /> */}
                </TouchableOpacity>)
        } else {
            return (
                <TouchableOpacity 
                  onPress={logout}>
                  <Text>Logout</Text>
                    {/* <Image source={require('../../../assets/logout.png')} /> */}
                </TouchableOpacity>
            );
        }
    }

    nav.setOptions({
        headerLeft: () => (
            <View>
              {renderLoginLogout()}
            </View>
          ),
    })

    const onEventsPressed = () => {
        nav.navigate('Events');
    }

    return (
        <View style={styles.container}>
            <View style={styles.button__row}>
                <TouchableOpacity 
                    style={styles.button}
                    onPress={onEventsPressed}>
                        <Text style={styles.button__text}>{'Upcoming\nEvents'}</Text>
                </TouchableOpacity>
                <TouchableOpacity 
                    style={styles.button}
                    onPress={() => null}>
                        <Text style={styles.button__text}>Shows Today</Text>
                </TouchableOpacity>
            </View>
            <View style={styles.button__row}>
                <TouchableOpacity 
                    style={styles.button}
                    onPress={() => null}>
                        <Text style={styles.button__text}>Stats</Text>
                </TouchableOpacity>
                <TouchableOpacity 
                    style={styles.button}
                    onPress={() => null}>
                        <Text style={styles.button__text}>Locations</Text>
                </TouchableOpacity>
            </View>
            <View style={styles.button__row}>
                <TouchableOpacity 
                    style={styles.button}
                    onPress={() => null}>
                        <Text style={styles.button__text}>Workouts</Text>
                </TouchableOpacity>
                <TouchableOpacity 
                    style={styles.button}
                    onPress={() => null}>
                        <Text style={styles.button__text}>More</Text>
                </TouchableOpacity>
            </View>
        </View>
    );
}

export default Home;

If a user is logged in I take them to a logout screen asking to confirm and when I go back to Home Screen it still displays 'Logout' instead of 'Login'

import React, { useState } from 'react';
import { Text, View, TouchableOpacity, TextInput, Keyboard } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import Spinner from 'react-native-loading-spinner-overlay';
import { firebase } from '../../firebase/config';
import styles from './LogoutStyles';

function Logout () {
    const [visible, setVisible] = useState(false);
    const nav = useNavigation();

    const yesPressed = () => {
        setVisible(true);
        firebase.auth().signOut().catch(error => alert(error.message));
        nav.goBack();
    }

    const noPressed = () => nav.goBack();

    return (
        <View style={styles.container}>
            <Spinner visible={visible} />
            <Text style={styles.text}>Are you sure you want to log out?</Text>
            <View style={styles.button__container}>
                <TouchableOpacity 
                    onPress={yesPressed}
                    style={styles.button}>
                    <Text style={styles.button__text}>Yes</Text>
                </TouchableOpacity>
                <TouchableOpacity 
                    onPress={noPressed}
                    style={styles.button}>
                    <Text style={styles.button__text}>No</Text>
                </TouchableOpacity>
            </View>
        </View>
    );   
}

export default Logout;

Any help with this warning is greatly appreciated.


Solution

  • Your code is ok in the most common use cases, but navigation options are built on first render, so every new render with props changes does not update them.

    You need to manage the navigation.setOption on a useEffect hook, so on each change of useEffect dependencies, you update the nav options:

        React.useEffect(()=>{
            nav.setOptions({
                headerLeft: () => (
                    <View>
                      {renderLoginLogout()}
                    </View>
                  ),
            })
        },[user])