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

Redux结合Firebase Auth登录状态判断Hook异常问题求助

问题:useAuth Hook无法正确判断用户登录状态

我正在开发一个加密货币资产应用,用于练习Redux与数据状态管理。未注册用户可添加加密货币资产、查看统计数据;注册用户的资产需在重新登录时保留。采用Firebase Authentication做身份认证,Redux管理状态。目前Firebase交互正常,用户创建与登录流程能正确执行,但自定义的useAuth Hook无法正确判断用户是否已登录。

应用外观

application appearance

Firebase后台状态

inside firebase

控制台输出

here is what the console shows me

Store代码

export const store = configureStore({
    reducer: {
        user: userReducer,
    },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

User Slice代码

const initialState = {
    email: null,
    token: null, 
    id: null,
}

const userSlice = createSlice({
    name: 'user',
    initialState,
    reducers: {
        setUser(state, action) {
            state.email = action.payload.email;
            state.token = action.payload.token;
            state.id = action.payload.id;
        },
        removeUser(state) {
            state.email = null;
            state.token = null;
            state.id = null;
        }
    }
})

export const {setUser, removeUser} = userSlice.actions;

export default userSlice.reducer

useAuth Hook代码

export function useAuth() {
    const {email, token, id} = useAppSelector(state => state.user);
    console.log('is auth work', email, token, id)
    return {
        isAuth: !!email, 
        email,
        token,
        id,
    };
}

登录表单组件代码

interface LoginFormProps {
    onSignUp: () => void;
    onClose: () => void;
}

type FieldType = {
    email?: string;
    password?: string;
    remember?: string;
};

export function LoginForm({ onSignUp, onClose }: LoginFormProps) {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [rememberMe, setRememberMe] = useState(false);
    const dispatch = useAppDispatch();

    const onFinish = (email: string, password: string) => {
        const auth = getAuth();
        signInWithEmailAndPassword(auth, email, password)
            .then(({user}) => {
                dispatch(setUser({
                    isAuth: true,
                    email: user.email,
                    id: user.uid,
                    token: user.refreshToken,
                }))
                console.log('user', user);
            })
            .catch();
    };

    const onFinishFailed = (errorInfo: any) => {
        console.log('Failed:', errorInfo);
    };

    return (
        <Form
            name="basic"
            labelCol={{ span: 4 }}
            wrapperCol={{ span: 18 }}
            layout="horizontal"
            initialValues={{ remember: true }}
            onFinish={() => {onFinish(email, password); onClose()}}
            onFinishFailed={onFinishFailed}
            autoComplete="off"
        >
            <Form.Item<FieldType>
                label="Email"
                name="email"
                rules={[{ required: true, message: 'Please input your email!' }]}
            >
                <Input
                    value={email || ''}
                    onChange={(e) => { setEmail(e.target.value) }}
                />
            </Form.Item>

            <Form.Item<FieldType>
                label="Password"
                name="password"
                rules={[{ required: true, message: 'Please input your password!' }]}
            >
                <Input.Password
                    value={password || ''}
                    onChange={(e) => { setPassword(e.target.value) }}
                />
            </Form.Item>

            <Form.Item<FieldType>
                name="remember"
                valuePropName="checked"
                wrapperCol={{ span: 18 }}
            >
                <Checkbox
                    value={rememberMe}
                    onChange={(e) => { setRememberMe(e.target.checked) }}
                >Remember me</Checkbox>
            </Form.Item>

            <Flex gap={12}>
                <Form.Item wrapperCol={{ span: 18 }}>
                    <Button type="primary" htmlType="submit">
                        Log In
                    </Button>
                </Form.Item>
                <Button type="primary" onClick={onSignUp}>Sign Up</Button>
            </Flex>
        </Form>
    )
}

Header组件代码

export default function Header() {
    const { crypto } = useContext(CryptoContext);
    const [select, setSelect] = useState(false);
    const [showModal, setshowModal] = useState(false);
    const [coin, setCoin] = useState<ICryptoCurrency | null>(null);
    const [showDrawer, setShowDrawer] = useState(false);
    const [showCoinForm, setShowCoinForm] = useState(false);
    const [showLoginForm, setShowLoginForm] = useState(false);
    const [showRegistrationForm, setShowRegistrationForm] = useState(false);

    const {isAuth, email} = useAuth();

    const handleSelect = (value: string) => {
        const selectedCoin = crypto.find((c) => c.id === value)
        if (selectedCoin) {
            setshowModal(true);
            setCoin(selectedCoin);
        }
    }

    const handleAddAsset = () => {
        setShowDrawer(true);
        setShowCoinForm(true);
    }

    const handleRegistration = () => {
        setShowDrawer(true);
        setShowRegistrationForm(true);
        setShowCoinForm(false);
        setShowLoginForm(false);
    }

    const handleLogin = () => {
        setShowDrawer(true);
        setShowLoginForm(true);
        setShowRegistrationForm(false);
        setShowCoinForm(false);
    }

    useEffect(() => {
        const keypress = (event: any) => {
            if (event.key === '/') {
                setSelect((prev) => !prev);
            }
        }
        document.addEventListener('keypress', keypress);
        return () => document.removeEventListener('keypress', keypress);
    }, [])

    const formNames = () => {
        if (showCoinForm) return 'Add Asset'
        if (showRegistrationForm) return 'Registration'
        if (showLoginForm) return 'Log In'
    }


    return (
        <Layout.Header style={headerStyle}>
            <Select
                open={select}
                onClick={() => setSelect((prev) => !prev)}
                style={{ width: '250px' }}
                placeholder="press / to open"
                onSelect={handleSelect}
                options={crypto.map((coin: ICryptoCurrency) => (
                    {
                        label: coin.name,
                        value: coin.id,
                        icon: coin.icon
                    }
                ))}
                optionRender={(option) => (
                    <Space>
                        <img src={option.data.icon} style={{ width: '20px', paddingTop: '6px' }} alt={option.data.id} />
                        <span> {option.data.label}</span>
                    </Space>
                )}
            />

            <Flex gap={10}>
                <Button onClick={handleAddAsset} style={addButtonStyle}>Add Asset</Button>
                {isAuth ? (
                    <Avatar icon={<UserOutlined />} />
                ) : (
                    <Button onClick={handleLogin} style={addButtonStyle}>Log In</Button>
                )}
            </Flex>

            <Modal open={showModal} style={{ maxWidth: '420px' }} onCancel={() => setshowModal(false)} footer={null}>
                <CryptoModal coin={coin} />
            </Modal>

            <Drawer
                width={600}
                destroyOnClose
                title={formNames()}
                onClose={() => {
                    setShowDrawer(false);
                    setShowCoinForm(false);
                    setShowLoginForm(false);
                    setShowRegistrationForm(false)
                }}
                open={showDrawer}
            >
                {showCoinForm && (
                    <AddAssetForm onClose={() => { setShowDrawer(false); setShowCoinForm(false) }} />
                )}

                {showLoginForm && (
                    <LoginForm onSignUp={handleRegistration} onClose={() => setShowDrawer(false)} />
                )}

                {showRegistrationForm && (
                    <RegistrationForm onLogin={handleLogin} onClose={() => setShowDrawer(false)} />
                )}
            </Drawer>
        </Layout.Header>
    )
}
解决方案

1. 移除登录dispatch的冗余参数

LoginForm的onFinish中,dispatchsetUser时传入的isAuth: true是冗余字段(user slice的state中无该属性),直接移除:

dispatch(setUser({
    email: user.email,
    id: user.uid,
    token: user.refreshToken,
}))

2. 监听Firebase Auth状态变化,同步Redux状态

仅在登录时手动dispatch无法覆盖页面刷新、自动登录等场景,需在应用初始化时添加监听:

import { getAuth, onAuthStateChanged } from "firebase/auth";
import { useAppDispatch } from './store';
import { setUser, removeUser } from './userSlice';

function App() {
  const dispatch = useAppDispatch();

  useEffect(() => {
    const auth = getAuth();
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      if (user) {
        dispatch(setUser({
          email: user.email,
          id: user.uid,
          token: user.refreshToken,
        }));
      } else {
        dispatch(removeUser());
      }
    });

    return unsubscribe;
  }, [dispatch]);

  // ...其他组件代码
}

3. 优化登录状态判断逻辑

Firebase的user.email可能为null(如第三方登录未绑定邮箱),改用uid判断更可靠:

export function useAuth() {
    const {email, token, id} = useAppSelector(state => state.user);
    console.log('is auth work', email, token, id)
    return {
        isAuth: !!id, // uid是用户唯一标识,不会为null
        email,
        token,
        id,
    };
}

4. 验证Redux Provider包裹

确保整个应用被Provider包裹,否则组件无法访问Redux状态:

import { Provider } from 'react-redux';
import { store } from './store';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <Provider store={store}>
    <App />
  </Provider>,
);

5. 调整Drawer关闭时机

Redux状态更新是异步的,登录后延迟关闭Drawer,确保Header组件拿到最新状态:

const onFinish = async (email: string, password: string) => {
    const auth = getAuth();
    try {
        const {user} = await signInWithEmailAndPassword(auth, email, password);
        dispatch(setUser({
            email: user.email,
            id: user.uid,
            token: user.refreshToken,
        }));
        setTimeout(() => {
            onClose();
        }, 100);
    } catch (error) {
        console.error('Login error:', error);
    }
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 19:44:57