代码之家  ›  专栏  ›  技术社区  ›  Jose A

状态正在重新初始化为其原始值-useState或useReduce

  •  0
  • Jose A  · 技术社区  · 4 年前

    我创建了一个自定义钩子( useAuth )扩展第三方身份验证服务Auth0的useAuth0挂钩,并设置一些保存基本用户信息(如userId)的局部变量。

    我有一个可以冒充其他帐户的主帐户。这意味着它覆盖了我的自定义钩子中的userId,并在整个系统中传播。

    我面临的问题是,每当我打电话给 impersonate 改变这个钩子内部状态的函数,它会改变它,但随后会重新初始化自己。我不知道是什么导致了这种重新初始化。代码在下面。

    import { useAuth0 } from '@auth0/auth0-react';
    import produce from 'immer';
    import { useState, useEffect, useCallback, useReducer, Reducer } from 'react';
    import { AccountType, Auth0HookUser, TenantInfo, TenantType } from '../@dts';
    
    type AuthVariants =
      | 'INDIVIDUAL_TEACHER'
      | 'INSTITUTION_TEACHER'
      | 'STUDENT'
      | 'SECRETARY'
      | 'COORDINATOR'
      | 'ADMINISTRATOR';
    
    type AuthTenant = {
      accountType: AccountType;
      tenantType: TenantType;
      employeeId: string;
      tenantId: string;
      selectedTenant: TenantInfo;
      variant: AuthVariants;
      mode: 'IMPERSONATION' | 'NORMAL';
      user: Auth0HookUser;
    };
    
    const defaultAuthTenant: () => AuthTenant = () => ({
      accountType: 'teacher',
      employeeId: '',
      mode: 'NORMAL',
      selectedTenant: {
        accountType: 'teacher',
        tenantType: 'INSTITUTION',
        tenantId: '',
      },
      tenantId: '',
      tenantType: 'INSTITUTION',
      variant: 'INDIVIDUAL_TEACHER',
      user: {
        name: '',
        nickname: '',
      } as any,
    });
    
    type Action =
      | {
          type: 'UPDATE_AUTH';
          auth: AuthTenant;
        }
      | {
          type: 'IMPERSONATE';
          impersonatedEmployeeId: string;
          impersonatedName: string;
          accountType: AccountType;
        }
      | {
          type: 'EXIT_IMPERSONATION';
        };
    
    type State = {
      current: AuthTenant;
      original: AuthTenant;
    };
    
    const reducer = produce((state: State, action: Action) => {
      switch (action.type) {
        case 'IMPERSONATE':
          console.log('Impersonating');
          const selectedTenant =
            state.current.user['https://app.schon.io/user_data'].tenants[0];
          state.current = {
            ...state.current,
            user: {
              ...state.current.user,
              name: action.impersonatedName,
              nickname: action.impersonatedName,
              'https://app.schon.io/user_data': {
                ...state.current.user['https://app.schon.io/user_data'],
                userId: action.impersonatedEmployeeId,
              },
            },
            mode: 'IMPERSONATION',
            accountType: action.accountType,
            employeeId: action.impersonatedEmployeeId,
            variant: getVariant(action.accountType, selectedTenant.tenantType),
            selectedTenant: {
              ...state.current.selectedTenant,
              accountType: action.accountType,
            },
          };
          return state;
        case 'UPDATE_AUTH':
          state.current = action.auth;
          state.original = action.auth;
          return state;
        default:
          return state;
      }
    });
    
    export function useAuth() {
      const { user: _user, isAuthenticated, isLoading, ...auth } = useAuth0();
      const user = _user as Auth0HookUser;
    
      const [selectedTenantIndex, setSelectedTenantIndex] = useState(0);
      const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, {
        current: defaultAuthTenant(),
        original: defaultAuthTenant(),
      });
    
      const impersonate = (
        impersonatedEmployeeId: string,
        accountType: AccountType,
        impersonatedName: string,
      ) => {
        if (!user) {
          return;
        }
    
        dispatch({
          type: 'IMPERSONATE',
          accountType,
          impersonatedEmployeeId,
          impersonatedName,
        });
      };
    
      const exitImpersonation = useCallback(() => {
        dispatch({ type: 'EXIT_IMPERSONATION' });
      }, []);
    
      useEffect(() => {
        if (isLoading || (!isLoading && !isAuthenticated)) {
          return;
        }
        if (!user || state.current.mode === 'IMPERSONATION') {
          return;
        }
        console.log('Use Effect Running');
        const { tenants, userId } = user['https://app.schon.io/user_data'];
        const selectedTenant = tenants[selectedTenantIndex];
        const { accountType, tenantType } = selectedTenant;
    
        dispatch({
          type: 'UPDATE_AUTH',
          auth: {
            tenantId: selectedTenant.tenantId,
            employeeId: userId,
            mode: 'NORMAL',
            variant: getVariant(accountType, tenantType),
            user,
            selectedTenant,
            accountType,
            tenantType,
          },
        });
      }, [
        user,
        isAuthenticated,
        isLoading,
        selectedTenantIndex,
        state.current.mode,
      ]);
    
      console.log('State Current', state.current);
      return {
        isAuthenticated,
        isLoading,
        impersonate,
        exitImpersonation,
        setSelectedTenantIndex,
        ...auth,
        ...state.current,
      };
    }
    
    function getVariant(
      accountType: AccountType,
      tenantType: TenantType,
    ): AuthVariants {
      if (accountType === 'teacher') {
        return tenantType === 'INSTITUTION'
          ? 'INSTITUTION_TEACHER'
          : 'INDIVIDUAL_TEACHER';
      }
      return accountType.toUpperCase() as AuthVariants;
    }
    

    看图片。在我调用模拟函数后,它将其设置为模拟模式,但会重新初始化自身并设置为默认模式。

    enter image description here

    这是我尝试过的:

    • 仔细检查是否向useEffect传递了正确的依赖关系(它不是导致重新初始化的依赖关系)。
    • 在reducer之前,我使用的是useStae,我通过它的函数调用它,而不是直接设置状态。
    • 我试着在整个周期中介入(调试),但什么也没找到。
    • 我浏览了几个SO帖子和React dosc,看看是否能找到任何问题,但我的盲眼看不见。

    这是我调用它的视图(参见 const {impersonate} = useAuth() ) :

    import React, { memo, useCallback, useMemo, useState } from 'react';
    import { RouteComponentProps } from '@reach/router';
    import { Button, Typography } from 'components';
    import Skeleton from 'react-loading-skeleton';
    import { useAuth } from '../../../../../auth';
    import { Tabs, Dialog } from '../../../../../components/';
    import { useAllClassesAndTeacherForInstitution } from '../../../../../graphql';
    import { useThemeSpacing } from '../../../../../shared-styles/material-ui';
    import { AddClassTeacher, ListClassTeacher } from './components';
    
    type TeacherViewRouteProps = {
      teacherId: string;
    };
    
    export const TeacherView: React.FC<RouteComponentProps<
      TeacherViewRouteProps
    >> = memo((props) => {
      const { impersonate } = useAuth();
      const { teacherId } = props;
      const { data, loading } = useAllClassesAndTeacherForInstitution(teacherId!);
      const [open, setOpen] = useState(false);
      const openDialog = useCallback(() => setOpen(true), []);
      const closeDialog = useCallback(() => setOpen(false), []);
    
      const spacing = useThemeSpacing(4)();
      const teacherName = `${data?.teacher.name.fullName}`;
      const impersonateTeacher = useCallback(() => {
        if (!teacherName || !teacherId) {
          return;
        }
        impersonate(teacherId!, 'teacher', teacherName);
        closeDialog();
        // props?.navigate?.('/');
      }, [impersonate, closeDialog, teacherId, teacherName]);
    
      const tabOptions = useMemo(
        () => [
          {
            label: `Clases de ${teacherName}`,
          },
          {
            label: 'Agregar Clases',
          },
        ],
        [teacherName],
      );
    
      return (
        <>
          <Typography variant="h1" className={spacing.marginTopBottom}>
            {(loading && <Skeleton />) || teacherName}
          </Typography>
          <Dialog
            title={`Entrar en la cuenta de ${teacherName}`}
            open={open}
            onAgree={impersonateTeacher}
            onClose={closeDialog}
          >
            ¿Desea visualizar la cuenta de {teacherName}?
            <br />
            Si desea salir de la misma por favor refresque la página.
          </Dialog>
          <Button className={spacing.marginTopBottom} onClick={openDialog}>
            Entrar en cuenta de {teacherName || 'maestro'}
          </Button>
    
          {process.env.NODE_ENV === 'development' && (
            <>
              <Tabs options={tabOptions}>
                <>
                  {data?.teacher.klasses && (
                    <ListClassTeacher
                      klasses={data.teacher.klasses}
                      teacherName={teacherName || 'maestro'}
                    />
                  )}
                </>
                <>
                  {data?.grades && (
                    <AddClassTeacher
                      existingClasses={data?.teacher.klasses || []}
                      grades={data.grades}
                      teacherId={teacherId!}
                    />
                  )}
                </>
              </Tabs>
            </>
          )}
        </>
      );
    });
    
    export default TeacherView;
    

    以下是初始提供者:

    import React, { Suspense, memo } from 'react';
    import { Location } from '@reach/router';
    import { ThemeProvider } from '@material-ui/core';
    import { ApolloProvider } from '@apollo/client';
    import { theme } from 'components';
    
    import { Auth0Provider } from '@auth0/auth0-react';
    import CircularLoader from './components/CircularProgress';
    import { useGlobalClient } from './utilities/client';
    import { Layout } from './views/Layout';
    import { Root } from './views/Root';
    import { enableIfNotPreRendering } from './utilities/isPrerendering';
    import { AUTH_CONFIG } from './auth/auth0.variables';
    console.log('AUTH CONFIG', AUTH_CONFIG);
    function App() {
      // This will be a method to enable faster loading times.
      /**\
       * Main AppMethod which hosts the site. To improve FCP it was split into
       * 2 files: The main file which will load the <Home component without any
       * dependencies (making it extremely fast to load at the beginning as it won't)
       * download all the code on its entirety.
       *
       * All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes.
       * The propagation from Provider to its descendant consumers is not subject to the
       * shouldComponentUpdate method, so the consumer is updated even when an ancestor component
       * bails out of the update.
       *
       * Check this out whenever you're planning on implementing offline capabilities:
       * https://dev.to/willsamu/how-to-get-aws-appsync-running-with-offline-support-and-react-hooks-678
       */
      return (
        <Suspense fallback={<CircularLoader scrollsToTop={true} />}>
          <Location>
            {({ location }) => (
              <Auth0Provider
                {...AUTH_CONFIG}
                location={{ pathname: location.pathname, hash: location.hash }}
              >
                <ProviderForClient />
              </Auth0Provider>
            )}
          </Location>
        </Suspense>
      );
    }
    
    /**
     * This is done like this because we are using the useAuth0 Hook
     * and we need it to be after the Auth0Provider!!
     * @param props
     */
    export const ProviderForClient: React.FC = (props) => {
      const globalClient = useGlobalClient();
      return (
        <ThemeProvider theme={theme}>
          <ApolloProvider client={globalClient.current as any}>
            <Layout>
              <>{enableIfNotPreRendering() && <Root />}</>
            </Layout>
          </ApolloProvider>
        </ThemeProvider>
      );
    };
    
    export default memo(App);
    
    
    0 回复  |  直到 4 年前
        1
  •  0
  •   Jose A    4 年前

    我觉得自己很迟钝。钩子功能正常。上述方法并无不妥(其他一些问题有待商榷)。问题是,我没有通过上下文传递钩子,而是在每个组件上调用钩子。这意味着钩子正在为每个组件重新创建状态(应该如此),因此每当我更新状态时,它只会在一个组件中声明。

    就是这样。我只需要将钩子重写为一个组件,并与上下文共享。

    这是最后的代码(为了简洁起见省略了一些打字)

    const AuthContext = createContext<Context>({
      isAuthenticated: false,
      isLoading: false,
      getAccessTokenSilently() {
        return '' as any;
      },
      getAccessTokenWithPopup() {
        return '' as any;
      },
      getIdTokenClaims() {
        return '' as any;
      },
      loginWithPopup() {
        return '' as any;
      },
      loginWithRedirect() {
        return '' as any;
      },
      logout() {
        return '' as any;
      },
      impersonate(a: string, b: AccountType, c: string) {},
      exitImpersonation() {},
      setSelectedTenantIndex(i: number) {},
      accountType: 'teacher',
      employeeId: '',
      mode: 'NORMAL',
      selectedTenant: {
        accountType: 'teacher',
        tenantType: 'INSTITUTION',
        tenantId: '',
      },
      tenantId: '',
      tenantType: 'INSTITUTION',
      user: {
        name: '',
        nickname: '',
      } as any,
      variant: 'INSTITUTION_TEACHER',
    });
    
    /**
     * A custom wrapper for Auth. This allows us to set impersonation
     */
    export const Auth: React.FC = memo((props) => {
      const { user: _user, isAuthenticated, isLoading, ...auth } = useAuth0();
      const user = _user as Auth0HookUser;
    
      const defaultUser = useCallback(
        (user?: Auth0HookUser) => getDefaultAuthTenant(user),
        [],
      );
    
      const [selectedTenantIndex, setSelectedTenantIndex] = useState(0);
      const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, {
        current: defaultUser(_user),
        original: defaultUser(_user),
      });
    
      const calledDispatch = useCallback(dispatch, [dispatch]);
    
      const impersonate = useCallback(
        (
          impersonatedEmployeeId: string,
          accountType: AccountType,
          impersonatedName: string,
        ) => {
          if (!user) {
            return;
          }
          calledDispatch({
            type: 'IMPERSONATE',
            accountType,
            impersonatedEmployeeId,
            impersonatedName,
          });
        },
        [calledDispatch, user],
      );
    
      const exitImpersonation = useCallback(() => {
        dispatch({ type: 'EXIT_IMPERSONATION' });
      }, []);
    
      useEffect(() => {
        if (isLoading || (!isLoading && !isAuthenticated)) {
          return;
        }
        if (!user || state.current.mode === 'IMPERSONATION') {
          return;
        }
        const { tenants, userId } = user['https://app.schon.io/user_data'];
        const selectedTenant = tenants[selectedTenantIndex];
        const { accountType, tenantType } = selectedTenant;
    
        dispatch({
          type: 'UPDATE_AUTH',
          auth: {
            tenantId: selectedTenant.tenantId,
            employeeId: userId,
            mode: 'NORMAL',
            variant: getVariant(accountType, tenantType),
            user,
            selectedTenant,
            accountType,
            tenantType,
          },
        });
        // eslint-disable-next-line
      }, [
        user,
        isAuthenticated,
        isLoading,
        selectedTenantIndex,
        state.current.mode,
      ]);
    
      return (
        <AuthContext.Provider
          value={{
            isAuthenticated,
            isLoading,
            impersonate,
            exitImpersonation,
            setSelectedTenantIndex,
            ...auth,
            ...state.current,
          }}
        >
          {props.children}
        </AuthContext.Provider>
      );
    });
    
    export function useAuth() {
      const context = useContext(AuthContext);
      if (!context) {
        throw new Error(
          'You need to place the AuthContext below the Auth0Context and on top of the app',
        );
      }
      return context;
    }
    
    

    现在,我刚刚放置了 <Auth/> 我的应用程序顶部的组件。