0

I'm trying to update user credentials in a React/Redux project with TypeScript. I'm using Firebase so the user credentials are being returned in the .then() block of the signInWithPopup method.

Here is the component. It is broken because it is breaking the rules of hooks. I can't find a workaround. I thought that using useEffect with a state object named currentUser would let me run the hook in the root of the functional component after the local (currentUser) state updates inside of the .then block.

Functional Component

import React, { useState, useEffect } from 'react';
import {
  getAuth,
  signInWithPopup,
  GithubAuthProvider,
  Auth,
} from 'firebase/auth';
import 'bulmaswatch/superhero/bulmaswatch.min.css';
import { storeState } from '../state';
import { UserData } from '../state/action-creators';
import { useTypedSelector } from '../hooks/use-typed-selector';

const UserAuth: React.FC = () => {
  const [currentUser, setCurrentUser] = useState<UserData | null>(null);
  const [loginButtonText, setLoginButtonText] = useState<string>('Log In');
  const provider: GithubAuthProvider = new GithubAuthProvider();
  const auth: Auth = getAuth();

  useEffect(() => {
  if (currentUser !== null) {
    useTypedSelector(({ user }) => currentUser);
  }
  }, [currentUser]);

  const signinHandler = () => {
    signInWithPopup(auth, provider)
      .then(({ user: { uid, email, displayName, photoURL } }) => {
        console.log({ uid, email, displayName, photoURL });

        const userPayload: UserData = {
          uid: uid,
          email: email as string,
          displayName: displayName as string,
          photoURL: photoURL as string,
        };

        setCurrentUser(userPayload);

        console.log({ userPayload });

        console.log({ storeState });
      })
      .catch((err) => {
        const errorCode = err.code;
        const errorMessage = err.message;
        const email = err.email;
        const credential = GithubAuthProvider.credentialFromError(err);
        // todo - display errors
        console.log(err);
        console.log(errorCode, errorMessage, email, credential);
      });
  };

  return (
    <button className="gh-button button is-primary" onClick={signinHandler}>
      <i className="fab fa-github" />
      <strong style={{ marginLeft: '10px' }}>{loginButtonText}</strong>
    </button>
  );
};

export default UserAuth;

This is the useTypedSelector hook

import { useSelector, TypedUseSelectorHook } from 'react-redux';
import { RootState } from '../state';

export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;

This is my userReducer for context


import produce from 'immer';
import { ActionType } from '../action-types';
import { Action } from '../actions';

interface UserState {
  uid: string;
  email: string;
  displayName: string;
  photoURL: string;
}

const initialState: UserState = {
  uid: '',
  email: '',
  displayName: '',
  photoURL: '',
};

const reducer = produce((state: UserState = initialState, action: Action) => {
  switch (action.type) {
    case ActionType.SET_USER:
      const { uid, email, displayName, photoURL } = action.payload;
      state.uid = uid;
      state.email = email;
      state.displayName = displayName;
      state.photoURL = photoURL;
      return state;

    default:
      return state;
  }
});

export default reducer;

Notes

I'm using Immer so I can easily modify the immutable state

If I could just connect my component to the Redux store so that I could dispatch the action directly from my component - that'd be great too.

I'm about ready to switch to the context API because Redux is nowhere as intuitive as Vuex is.

Bob Bass
  • 127
  • 2
  • 16

1 Answers1

3

This isn't even a Redux issue. As you mentioned, it's a React "rules of hooks" issue. All hook calls must be at the top level of a function component or another custom hook, and can never be nested inside conditional statements.

It's actually not clear from the current code what you're trying to do here. If we ignore the hooks rules aspect, the selector usage itself doesn't seem correct:

  useEffect(() => {
  if (currentUser !== null) {
    useTypedSelector(({ user }) => currentUser);
  }
  }, [currentUser]);

Even if this was legal usage, it doesn't do anything useful:

  • The selector is ignoring the user field being destructured from the state, and returning the currentUser variable already in scope
  • useSelector always returns the value that your selector extracts from the Redux state object, but here the return value is being ignored completely

I think what you're saying is that "when I get a new user object from Firebase, I want to update the Redux store to hold that user data". In that case, what you actually need is to dispatch an action, which is the opposite of selecting data from the state.

Assuming that's the case, then a correct approach here would probably involve dropping the useState<UserData> completely, and using the React-Redux useDispatch hook to let you dispatch actions from within the component:

// The reducer file should export an action creator for this case
import { userLoaded } from './userSlice';

const UserAuth = () => {
  const [loginButtonText, setLoginButtonText] = useState<string>('Log In');
  // get access to the Redux store `dispatch` method
  const dispatch = useAppDispatch();

  const signinHandler = () => {    
    const provider: GithubAuthProvider = new GithubAuthProvider();
    const auth: Auth = getAuth();

    signInWithPopup(auth, provider)
      .then(({ user: { uid, email, displayName, photoURL } }) => {
        console.log({ uid, email, displayName, photoURL });

        const userPayload: UserData = {
          uid: uid,
          email: email as string,
          displayName: displayName as string,
          photoURL: photoURL as string,
        };

        // Dispatch a Redux action containing this data
        dispatch(userLoaded(userPayload))
      })
      .catch((err) => {
        const errorCode = err.code;
        const errorMessage = err.message;
        const email = err.email;
        const credential = GithubAuthProvider.credentialFromError(err);
        // todo - display errors
        console.log(err);
        console.log(errorCode, errorMessage, email, credential);
      });
  };

  return (
    <button className="gh-button button is-primary" onClick={signinHandler}>
      <i className="fab fa-github" />
      <strong style={{ marginLeft: '10px' }}>{loginButtonText}</strong>
    </button>
  );
};

I think you're missing some key bits of understanding for how Redux works and how to use it correctly. I'd strongly recommend reading through the "Redux Essentials" tutorial in our Redux core docs, which teaches "how to use Redux, the right way".

A couple other notes on the code:

  • You shouldn't be instantiating "instances" like a Github auth provider inside render logic. Do that in an effect, or just directly in the click handler
  • You should also be using our official Redux Toolkit package to write your Redux logic (reducers, etc). RTK already has Immer built in, and is designed for a good TS usage experience. In particular, the createSlice API will auto-generate action creators and action types for you based on the reducers you define.
  • It's generally recommended to not use the React.FC type. Instead, just declare the type of props in the component, if there are any, and let TS infer the return type.
markerikson
  • 63,178
  • 10
  • 141
  • 157
  • Thank you for the response. This is well thought out. I'm disappointed with the Redux docs. I've been banging my head against the wall. I love Vuex, I'm comfortable with the Context API, I'm starting to hate Redux. I've been hacking at this mess for days. The state is working except for this auth. I can mutate the state by dispatching it within the state actions, or state files, but then dispatch doesn't work outside of the store. The `useEffect` was supposed to be an update workaround. I only added {user} so the typed selector would know which state I was trying to update. – Bob Bass Sep 04 '21 at 19:36
  • Can you clarify what specifically you're "disappointed" in with our docs? I also wrote our Redux docs tutorials, and they _should_ be very comprehensive in explaining how to use Redux and Redux Toolkit. FWIW, if you have questions, I and the other Redux maintainers hang out in the `#redux` channel in [the Reactiflux Discord](https://www.reactiflux.com). Looking at the code you showed, it does look like you're missing some fundamental understanding of how dispatching works, but I'm not sure where the disconnect is. What do you mean by "dispatch doesn't work outside the store"? – markerikson Sep 05 '21 at 16:05
  • Yes, absolutely. And of course, I don't mean any offense by it but it assumes (in my opinion) that the person reading is familiar with React/Redux including terminology that is specific to redux as well as plugins. Even when I was specifically trying to search for plugins, assuming that I was missing something, I only got more confused as I read. I come from a .NET and then Vue background. I saw this same thing all over MS docs where they assume that the end-user knows the WIN32 API inside and out. In terms of flux, my intro to this concept was Vue so maybe I was spoiled that Vue... – Bob Bass Sep 05 '21 at 20:43
  • ...was less complicated, for anybody familiar with JS (opposed to Vue), they even use analogies to familiar concepts where it made sense. I had tricked myself into thinking that flux was simpler than it is because of Vuex, maybe because I've been using Vue for a long time and I only using React in the past year - I find the Redux docs really hard to follow without starting from the first page of the docs. As it's an older app, I'm not interested in learning all the finer points now. I really like React hooks and I can accomplish the same thing easier. I'm sure that all play a role. – Bob Bass Sep 05 '21 at 20:51
  • For the record, I moved the state to the context API and everything is working perfectly now. The area I struggle with is "Why learn Redux when I can use the Context API?". I wonder if I'd feel differently if I got into React back in the Class-based component days. And the dispatching outside of the store, I meant the store folder. I know it was either an error of not using the right connector method or using the custom useTypedSelector hook I made, but having the rest of my store work perfectly and to struggle so hard on 1 single action was driving me nuts. – Bob Bass Sep 05 '21 at 20:53
  • 1
    I'll be honest... I am very confused what Redux resources you have been looking at. We don't mention anything about "plugins" in our docs. And yes, learning both React and Redux _does_ involve learning specific new terms and concepts. Ultimately, the real problem in your original code is that you were not _dispatching an action_. That's how _all_ Redux state updates occur: `store.dispatch({type: 'something/happened"})`. You were trying to write some "read state" code in a place where you needed to write "trigger an update" code. – markerikson Sep 06 '21 at 00:45
  • 1
    If you read our tutorials, they teach you everything you need to know to use Redux, either writing it "by hand" without any abstractions, or with our (recommended) Redux Toolkit package for writing the logic. I would love to have you drop by the `#redux` channel in Reactiflux so we can talk further, even if you have switched over to something else - I suspect that you were just missing one or two key pieces of understanding here, and I'd like to try to identify what the specific issue is and help clarify how this is supposed to work. – markerikson Sep 06 '21 at 00:47
  • 2
    For the "Redux vs Context" thing specifically, please see my explanations at [Why React Context is Not a "State Management" Tool (and Why It Doesn't Replace Redux)](https://blog.isquaredsoftware.com/2021/01/context-redux-differences/), [React, Redux, and Context Behavior](https://blog.isquaredsoftware.com/2020/01/blogged-answers-react-redux-and-context-behavior/), and [When (and when not) to Reach for Redux](https://changelog.com/posts/when-and-when-not-to-reach-for-redux), which cover how these are different tools that solve different problems. – markerikson Sep 06 '21 at 00:48
  • I appreciate that. I'll learn Redux deeper at some point, I just don't have the time to work on getting deep in the weeds now. I have the added complexity of using Redux almost exclusively to manage some WASM code that I'm afraid to tinker with too much. The plugins - I think it was the Ecosystem/Library section where it then links to GitHub. I realize that's not your documentation but as a whole, it felt like reading the docs from the beginning to the end was more effort than I was willing to put in to this particular project. – Bob Bass Sep 06 '21 at 20:15
  • I don't mean to make the docs sound bad, I'm just so overwhelmed with Redux right now. It's been one of the hardest things for me to digest in a long time. This is the question I have for you. Granted, Vue is a framework and React is a library, I get that. But why is Redux so much more complex than Vuex? Is it because it's less opinionated and requires more boilerplate? – Bob Bass Sep 06 '21 at 20:17
  • The SO comments section isn't the right place to have this discussion :) But I'll repeat my invitation: _please_ come by the [Reactiflux chat channels on Discord](https://www.reactiflux.com) and ping me, `@acemarke`, in the `#redux` channel. I'm not trying to _force_ you to use Redux, but I'd really like to chat more, figure out where the disconnects in understanding are, and try to offer some suggestions that could help you. – markerikson Sep 07 '21 at 02:17