0

I have the following problem. I need to set the state of the app component after the onSnapshot event. That's why I have put that in the componentDidMount() and it works fine it logs the set user, however when I put the same code in a useEffect with an empty dependency array it logs null. Any ideas?

Here are the two components:

Class

class App extends Component<{}, AppState> {
    constructor(props: any) {
        super(props);

        this.state = {
            currentUser: undefined,
        };
    }

    unsubscribeFromAuth = () => {};

    componentDidMount() {
        this.unsubscribeFromAuth = auth.onAuthStateChanged(async (userAuth) => {
            if (userAuth) {
                const userRef = await createUserProfileDoc(userAuth);

                userRef?.onSnapshot((snapShot) => {
                    this.setState({
                        currentUser: {
                            id: snapShot.id,
                            ...snapShot.data(),
                        },
                    });

                    console.log(this.state);
                });
            }
        });
    }

    componentWillUnmount() {
        this.unsubscribeFromAuth();
    }

    render() {
        return (
            <div>
                <Header currentUser={this.state.currentUser} />
                <Switch>
                    <Route exact path="/" component={HomePage} />
                    <Route path="/shop" component={ShopPage} />
                    <Route path="/signin" component={SignInUpPage} />
                </Switch>
            </div>
        );
    }
}

Functional

function App() {
    const [currentUser, setCurrentUser] = useState<User | null>(null);
    useEffect(() => {
        let unsubscribeFromAuth = () => {};
        const afterMount = () => {
            unsubscribeFromAuth = auth.onAuthStateChanged(async (userAuth) => {
                if (userAuth) {
                    const userRef = await createUserProfileDoc(userAuth);

                    userRef?.onSnapshot((snapShot) => {
                        setCurrentUser({
                            id: snapShot.id,
                            ...snapShot.data(),
                        });

                        console.log(currentUser);
                    });
                }
            });
        };
        afterMount();
        return () => {
            unsubscribeFromAuth();
        };
    }, []);
    return (
        <div>
            <Header currentUser={currentUser} />
            <Switch>
                <Route exact path="/" component={HomePage} />
                <Route path="/shop" component={ShopPage} />
                <Route path="/signin" component={SignInUpPage} />
            </Switch>
        </div>
    );
}

export default App;
skyboyer
  • 22,209
  • 7
  • 57
  • 64
Dimitar Nizamov
  • 145
  • 1
  • 8

3 Answers3

2

Setting state and console logging the current value of that state in the same function will always return the old value.

Try this:

useEffect(()=>{
//...
const newCurrentUser = { id: snapShot.id,
 ...snapShot.data(),
}
setCurrentUser(newCurrentUser);
console.log(newCurrentUser);
//outside the useEffect()

console.log(currentUser) //<<---You will first see null then the actual current user after useEffect is done doing its work
SlothOverlord
  • 1,655
  • 1
  • 6
  • 16
1

They act the same indeed, it's the difference between what you are logging and setState caveat that cause the issue.

The setState mechanism in both cases does not update the state immediately; an update happens after componentDidMount/effect were executed. At the time console.log is called, both states have null/undefined instead of user object. However, when you log this.state, due to asynchronous nature of console.log, you see in the console the updated state object. If you do console.log(this.state.currentUser) instead, you will see undefined.

Nikita Ivanov
  • 767
  • 5
  • 17
0

The state setter is asynchronous, so you cannot get the updated value immediately after calling it. You should use another useEffect for this:

function App() {
    const [currentUser, setCurrentUser] = useState(null);

    useEffect(()=>{
        console.log(currentUser);
    }, [currentUser])


    useEffect(() => {
        let unsubscribeFromAuth = () => {};
        const afterMount = () => {
            unsubscribeFromAuth = auth.onAuthStateChanged(async (userAuth) => {
                if (userAuth) {
                    const userRef = await createUserProfileDoc(userAuth);

                    userRef?.onSnapshot((snapShot) => {
                        setCurrentUser({
                            id: snapShot.id,
                            ...snapShot.data(),
                        });
                    });
                }
            });
        };
        afterMount();
        return () => {
            unsubscribeFromAuth();
        };
    }, []);
    return (...)
}

Bad example for your case, as you can keep using the local variable there, so just for reference - you can use a custom hook from my library if you really need to get the state value inside an async function (Some demo to play):

import { useAsyncWatcher } from "use-async-effect2";

function App() {
    const [currentUser, setCurrentUser] = useState(null);  
    const currentUserWatcher = useAsyncWatcher(currentUser);

    useEffect(() => {
        let unsubscribeFromAuth = () => {};
        const afterMount = () => {
            unsubscribeFromAuth = auth.onAuthStateChanged(async (userAuth) => {
                if (userAuth) {
                    const userRef = await createUserProfileDoc(userAuth);

                    userRef?.onSnapshot((snapShot) => {
                        setCurrentUser({
                            id: snapShot.id,
                            ...snapShot.data(),
                        });
                        console.log(await currentUserWatcher()); // wait for updated state value
                    });
                }
            });
        };
        afterMount();
        return () => {
            unsubscribeFromAuth();
        };
    }, []);
    return (...)
}

Another approach with deep state (Generic Demo):

import { useAsyncDeepState } from "use-async-effect2";

function App() {
    const [state, setState] = useAsyncDeepState({currentUser: null});  

    useEffect(() => {
        let unsubscribeFromAuth = () => {};
        const afterMount = () => {
            unsubscribeFromAuth = auth.onAuthStateChanged(async (userAuth) => {
                if (userAuth) {
                    const userRef = await createUserProfileDoc(userAuth);

                    userRef?.onSnapshot((snapShot) => {
                        await setState({
                          currentUser:{
                            id: snapShot.id,
                            ...snapShot.data(),
                          }
                        });
                        console.log(state.currentUser); 
                    });
                }
            });
        };
        afterMount();
        return () => {
            unsubscribeFromAuth();
        };
    }, []);
    return (...)
}
Dmitriy Mozgovoy
  • 1,419
  • 2
  • 8
  • 7