Redux 工具包状态在分派后不会更改,直到下一次渲染

问题描述 投票:0回答:1

我正在 React 和我的登录页面中使用 RTK 和 Firebase 构建基本身份验证流程:


export default function LoginPage() {
    //redux states and dispatch
    const isLoading = useAppSelector(state => state.auth.loading)
    const user = useAppSelector(state => state.auth.user)
    // const error = useAppSelector(state => state.auth.error)
    const dispatch = useAppDispatch()
    const navigate = useNavigate()
    const [errorMessage, setErrorMessage] = useState("")
    const [isPassVisible, setIsPassVisible] = useState<boolean>(false)
    const [formData, setFormData] = useState<LoginRequestType>({
        email: "",
        password: ""
    })

    const handleFormInput = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
        const { name, value } = event.target;
        setFormData((prevState) => {
            return {
                ...prevState,
                [name]: value
            }
        });
    }

    const onSubmitAction = async (e: React.ChangeEvent<HTMLFormElement>) => {
        e.preventDefault()
        setErrorMessage("")
        try {
            await dispatch(loginUser(formData))
            if (!user.accessToken) {
                setErrorMessage("Login error: check your email and password and try again")
            }else{
                toast.success('Login Successful', {
                    position: "top-center",
                    autoClose: 5000,
                    hideProgressBar: false,
                    closeOnClick: true,
                    pauseOnHover: true,
                    draggable: true,
                    progress: undefined,
                    theme: "dark",
                    transition: Bounce,
                });
                navigate("/user")
            }
        } catch (error) {
            toast.error('An error occured, please try again later!', {
                position: "top-center",
                autoClose: 5000,
                hideProgressBar: false,
                closeOnClick: true,
                pauseOnHover: true,
                draggable: true,
                progress: undefined,
                theme: "dark",
                transition: Bounce,
            });
        }
    }
    return (
        <div className="p-3 flex flex-col gap-12 place-content-center place-items-center">
            <div>
                <h1 className="font-merriweather font-medium text-2xl lg:text-4xl text-center">Login</h1>
                <h1 className="font-merriweather text-sm xl:text-xl text-center mt-5">Login to get started with your course or get enrolled</h1>
            </div>

            <form onSubmit={onSubmitAction} className="border-2  border-primary-100 bg-primary-50 p-8 lg:p-10 mb-16 rounded-3xl">
                {/* EMAIL AND PHONE CONTAINER */}
                <div className="flex flex-col lg:gap-6">
                    {/* Email */}
                    <div>
                        <div className="text-xs text-gray font-bold">Email</div>
                        <input required name="email" onChange={handleFormInput} type="email" className="border-2 bg-transparent border-gray rounded-xl py-2 px-6 mt-2 w-full" />
                    </div>
                    {/* Password */}
                    <div>
                        <div className="text-xs text-gray font-bold mt-2">Password</div>
                        <div className="relative">
                            <input
                                required
                                autoComplete="off"
                                name="password"
                                onChange={handleFormInput}
                                type={`${isPassVisible ? "text" : "password"}`}
                                maxLength={15}
                                className="border-2 bg-transparent border-gray rounded-xl py-2 px-6 w-full mt-2"
                            />
                            <button type="button" onClick={() => setIsPassVisible(p => !p)} className="text-primary-400 text-2xl absolute inset-y-0 right-3 top-[0.55rem]">
                                {isPassVisible ? <FaRegEye /> : <FaRegEyeSlash />}
                            </button>
                        </div>
                    </div>
                </div>
                {
                    errorMessage && <div className="max-w-[270px] text-red-500 mt-3 text-wrap">{errorMessage}</div> 
                }
                <div className="flex place-content-center mt-6">
                    {
                        !!isLoading ?
                            <div className="flex place-content-center gap-10 place-items-center">
                                <PacmanLoader size={15} speedMultiplier={2} color="#2b966f" />
                                <div className="text-primary-600 font-semibold">Logging you in!</div>
                            </div>
                            :
                            <Button type="submit" variant="default" text="Login" />
                    }
                </div>

            </form>
        </div>
    )
}

分派异步登录操作后,我检查用户访问令牌,并根据该令牌将用户导航到适当的路线。但是,直到下一次渲染时状态才会更新。这是我的授权切片:


const cookies = new Cookies()

type AuthStateType = {
    loading: boolean,
    user: User,
    error: any,
}


type RegisterPayloadType = {
    email: string, 
    password: string, 
    name: string, 
    type: ChipType, 
    orgName: string,
    date: string
}


let initialState: AuthStateType = {
    loading: false,
    user: {
        name: "",
        email: "",
        //Default state of type 
        type: "normal",
        accessToken: "",
        orgName: "",
        isEmailVerified: false,
    },
    error: null
}
const storedToken = cookies.get('token') 
const storedData = cookies.get('userData')
if(storedToken && storedData){
    initialState.user = {
        ...initialState.user,
        accessToken: storedToken,
        ...storedData
    }
}


// Registering Users
export const registerUser = createAsyncThunk("auth/register", 
    async (payload: RegisterPayloadType) => {
    const userCredentials = await createUserWithEmailAndPassword(auth, payload.email, payload.password)
    const user = userCredentials.user
    const token = await user.getIdToken()
    const isEmailVerified = user.emailVerified
    const userData: UserFBDocType = {
        uid: user.uid,
        orgName:payload.orgName,
        email: payload.email,
        name: payload.name,
        type: payload.type,
        date: payload.date,
    }
    await setDoc(doc(db, "users", user.uid), userData)
    await sendEmailVerification(user)    
    return { userData, token, isEmailVerified }
})

//Logging In User
export const loginUser = createAsyncThunk("auth/login",

    async (payload: LoginRequestType ) =>{
        try {
            await setPersistence(auth, browserLocalPersistence);
            const userCredentials = await signInWithEmailAndPassword(auth, payload.email, payload.password)
            const user = userCredentials.user
            const token = await user.getIdToken()
            //Getting the user data from the db: 
            const docRef = doc(db, "users", user.uid);
            const userDocSnap = await getDoc(docRef)

            let userData
            let error = null
            if (userDocSnap.exists()) {
                userData = userDocSnap.data()
            } else {
                error = "User does not exist on the db"
            }

            cookies.set("token", token)
            if(userData){
                cookies.set("userData", JSON.stringify(userData))
            }

            return { user, token, userData, error }
        } catch (error) {
            throw error
       }
    }

)

const authSlice = createSlice({
    name: 'auth',
    initialState: initialState,
    reducers:{
        signOut:(state) =>{

            state.user = {
                ...state.user,
                accessToken:"",
                email: "",
                name:"",
                orgName: "",
                type:"normal"
            }
            cookies.remove('token');
            cookies.remove('userData');
        }
    },

    extraReducers: (builder) =>{
        //CASES FOR SIGN UP
        builder.addCase(registerUser.pending, (state) =>{
            state.loading = true
        })
        builder.addCase(registerUser.fulfilled, (state, { payload }) =>{
            state.loading = false
            state.user = {
                ...state.user,
                accessToken: payload.token,
                email: payload.userData.email,
                name: payload.userData.name,
                orgName: payload.userData.orgName,
                isEmailVerified: payload.isEmailVerified 
            }
        })
        builder.addCase(registerUser.rejected, (state, action) =>{
            state.loading = false
            state.error = action.error.message
        })
        //CASES FOR LOGIN
        builder.addCase(loginUser.pending, (state) => {
            state.loading = true
        })
        builder.addCase(loginUser.fulfilled, (state, { payload }) => {
            state.loading = false
            state.error = null
            state.user = {
                ...state.user,
                accessToken: payload.token,
                email: payload.userData?.email,
                name: payload.userData?.name,
                orgName: payload.userData?.orgName,
                isEmailVerified: payload.userData?.isEmailVerified
            }
        })
        builder.addCase(loginUser.rejected, (state, action) => {
            state.loading = false
            state.error = action.error.message
        })
    }
})

export const authActions = authSlice.actions
export default authSlice.reducer

不是应该在loginUser函数完成后立即更新状态吗?

reactjs firebase react-redux redux-toolkit
1个回答
0
投票

状态在大多数情况下实际上是“实时”更新的,并将触发组件重新渲染(使用更新后的值),但这里的问题是您的

onSubmitAction
正在引用过时的闭包从调用回调时起,从外部范围选择
user
状态,
user
值在回调内永远不会是不同的值。

您可以在回调中等待并解开 resolved 值。有关更多详细信息,请参阅处理 Thunk 结果

const onSubmitAction = async (e: React.ChangeEvent<HTMLFormElement>) => {
  e.preventDefault();
  setErrorMessage("");

  try {
    const { user } = await dispatch(loginUser(formData)).unwrap();

    if (!user.accessToken) {
      setErrorMessage("Login error: check your email and password and try again")
    } else {
      toast.success('Login Successful', {
        ....
      });
      navigate("/user");
    }
  } catch (error) {
    toast.error('An error occurred, please try again later!', {
      ....
    });
  }
}

thunk 也应该拒绝错误值而不是重新抛出它。

示例:

export const loginUser = createAsyncThunk("auth/login",
  async (payload: LoginRequestType, thunkApi) => {
    try {
      await setPersistence(auth, browserLocalPersistence);
      const userCredentials = await signInWithEmailAndPassword(
        auth,
        payload.email,
        payload.password
      );

      const user = userCredentials.user;
      const token = await user.getIdToken();

      // Getting the user data from the db: 
      const docRef = doc(db, "users", user.uid);
      const userDocSnap = await getDoc(docRef);

      if (!userDocSnap.exists()) {
        throw new Error("User does not exist on the db");
      }

      const userData = userDocSnap.data();

      cookies.set("token", token);

      if (userData) {
        cookies.set("userData", JSON.stringify(userData));
      }

      return { user, token, userData };
    } catch (error) {
      thunkApi.rejectWithValue(error);
   }
});
builder.addCase(loginUser.rejected, (state, action) => {
  state.loading = false
  state.error = action.payload.message
})
© www.soinside.com 2019 - 2024. All rights reserved.