首页 > 解决方案 > 如何在 React 功能组件内的嵌套函数内使用类型化选择器?

问题描述

我正在尝试使用 TypeScript 更新 React/Redux 项目中的用户凭据。我正在使用 Firebase,因此在方法.then()块中返回了用户凭据signInWithPopup

这是组件。它被打破是因为它打破了钩子的规则。我找不到解决方法。我认为使用useEffect名为的状态对象可以让我在块内的本地 ( ) 状态更新currentUser后在功能组件的根目录中运行钩子。currentUser.then

功能组件

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;

这是useTypedSelector钩子

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

export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;

这是我userReducer的上下文


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;

笔记

我正在使用 Immer,所以我可以轻松修改不可变状态

如果我可以将我的组件连接到 Redux 存储,以便我可以直接从我的组件调度操作 - 那也很棒。

我正准备切换到上下文 API,因为 Redux 远不如 Vuex 直观。

标签: reactjsreduxfirebase-authenticationreact-hooksimmer.js

解决方案


这甚至不是 Redux 问题。正如您所提到的,这是一个 React “钩子规则”问题所有钩子调用都必须位于函数组件或另一个自定义钩子的顶层,并且永远不能嵌套在条件语句中。

从当前代码中实际上并不清楚您在这里尝试做什么。如果我们忽略钩子规则方面,选择器的使用本身似乎并不正确:

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

即使这是合法用法,它也没有做任何有用的事情:

  • 选择器忽略user从状态中解构的字段,并返回currentUser已经在范围内的变量
  • useSelector总是返回你的选择器从 Redux 状态对象中提取的值,但是这里的返回值被完全忽略了

认为您的意思是“当我从 Firebase 获得新的用户对象时,我想更新 Redux 存储以保存该用户数据”。在这种情况下,您真正​​需要的是调度一个动作,这与从状态中选择数据相反。

假设是这种情况,那么这里的正确方法可能涉及useState<UserData>完全删除,并使用 React-ReduxuseDispatch钩子让您从组件内调度操作:

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

我认为您对 Redux 的工作原理以及如何正确使用它缺少一些关键的理解。我强烈建议阅读我们的 Redux 核心文档中的“Redux Essentials”教程,该教程教授“如何以正确的方式使用 Redux”。

关于代码的其他几点说明:

  • 您不应该在渲染逻辑中实例化像 Github auth 提供者那样的“实例”。在效果中执行此操作,或者直接在单击处理程序中执行此操作
  • 您还应该使用我们的官方 Redux Toolkit 包来编写您的 Redux 逻辑(reducers 等)。RTK 已经内置了 Immer,旨在提供良好的 TS 使用体验。特别是,createSliceAPI 将根据您定义的 reducer 为您自动生成操作创建者和操作类型。
  • 通常建议不要使用该React.FC类型。相反,只需props在组件中声明类型,如果有的话,让 TS 推断返回类型。

推荐阅读