Перенаправление на главную страницу после входа или регистрации не работает. Firebase, частный маршрут, аутентификация по электронной почте

Я столкнулся с проблемой, которую не могу решить. В моей функции handleLogIn в компоненте ContextProvider я использую перехватчик useNavigate для перенаправления страницы на «/», если пользователь успешно входит в систему. Однако после успешного входа в систему он не перенаправляется на главную страницу; вместо этого они перенаправляются обратно на страницу входа. После второй отправки формы перенаправление страницы работает правильно.

Как я могу перенаправить пользователя со страницы «/sign-in» или «/sign-up» сразу после отправки формы?

вот мой contextProviderComponent:

export const ContextAPI = createContext<null | TContextAPI>(null);

const ContextProvider = ({ children }: { children: React.ReactNode }) => {
  const navigate = useNavigate();
  const [currentUser, setCurrentUser] = useState<firebase.User | undefined | null>(undefined);
  const [currentUserId, setCurrentUserId] = useState<string | undefined | null>(undefined);
  const [loading, setLoading] = useState<boolean>(true);
  const { signUpValues, SignUpInputConstructor, handleRegister, signUpError } = useAuth();
  const { logInValues, SignInInputConstructor } = useLogIn();
  const { handleLogout, logOutError } = useLogOut();
  const { newGroupName, isShowGroupCreator, setIsShowGroupCreator, setNewGroupName } = useGroupMenu();

  const handleUserGroups = () => {
    if (currentUserId) {
      const groupRef = doc(db, `/user_groups/${currentUserId}/${newGroupName}`, uuid());
      if (newGroupName === "" || newGroupName.length > 20) {
        console.info("group name is incorrect");
      } else {
        setDoc(groupRef, { merge: true });
        console.info("db updated sucessfully");
        navigate("/");
      }
    } else {
      console.info("currentUserId is not set");
    }
  };

  const [logInError, setLogInError] = useState<string>("");
  const logIn = (email: string, password: string) => {
    return auth.signInWithEmailAndPassword(email, password);
  };

  const handleLogIn = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (!/^[a-zA-Z\s].*@.*$/.test(logInValues.email)) {
      return setLogInError("email is not correct");
    }
    if (logInValues.password === "") {
      return setLogInError("password field can not be empty");
    } else {
      try {
        setLogInError("");
        setLoading(true);
        logIn(logInValues.email, logInValues.password).then(() => {
          navigate("/");
        });
      } catch (errors) {
        setLogInError("Failed to log-in in account");
      }
      setLoading(false);
    }
  };

  const vals: TContextAPI = {
    setNewGroupName,
    isShowGroupCreator,
    setIsShowGroupCreator,

    newGroupName,
    currentUserId,
    handleUserGroups,
    handleLogout,
    logOutError,
    setLoading,
    currentUser,
    signUpValues,
    SignUpInputConstructor,
    handleRegister,
    loading,
    signUpError,
    logInValues,
    SignInInputConstructor,
    handleLogIn,
    logInError,
  };

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      setCurrentUser(user);
      setLoading(false);
      setCurrentUserId(user?.uid || null);
    });

    return unsubscribe;
  }, []);

  return <ContextAPI.Provider value = {vals}> {!loading && children} </ContextAPI.Provider>;
};

и это мой компонент входа в систему:

const LogIn = () => {
  const SignInInputConstructor = useContextSelector(ContextAPI, (v) => v?.SignInInputConstructor);
  const loading = useContextSelector(ContextAPI, (v) => v?.loading);
  const currentUser = useContextSelector(ContextAPI, (v) => v?.currentUser);
  const handleLogIn = useContextSelector(ContextAPI, (v) => v?.handleLogIn);
  const logInError = useContextSelector(ContextAPI, (v) => v?.logInError);
  return (
    <>
      <h1>Log In page</h1>
      <form
        onSubmit = {(e) => {
          handleLogIn?.(e);
        }}
      >
        {currentUser?.email}
        {logInError && <Alert severity = "error">{logInError}</Alert>}
        {SignInInputConstructor?.map((item) => (
          <>
            <Typography>{item.typography}</Typography>
            <TextField
              id = {item.id}
              placeholder = {item.placeholder}
              variant = {item.variant}
              value = {item.value}
              onChange = {item.onChange}
            />
          </>
        ))}
        <Button variant = "contained" type = "submit" disabled = {loading}>
          Log In
        </Button>
        <Typography>
          Need an account? <Link to = {"/sign-up"}>Sign Up</Link>
        </Typography>
      </form>
    </>
  );
};

и это мой компонент PrivateRoute:

const PrivateRoute = () => {
  const currentUser = useContextSelector(ContextAPI, (v) => v?.currentUser);
  if (currentUser === undefined) return null;

  return currentUser ? <Outlet /> : <Navigate to = "/sign-in" />;
};

export default PrivateRoute;

и последний — мой основной компонент со всеми маршрутами:

const Main = () => {
  const router = createBrowserRouter(
    [
      {
        path: "/",
        element: <App />,
        children: [
          {
            path: "/sign-in",
            element: <LogIn />,
          },
          {
            path: "/sign-up",
            element: <Register />,
          },
          {
            path: "/",
            element: <PrivateRoute />,
            children: [
              {
                path: "/",
                element: <GroupMenu />,
                children: [
                  {
                    path: "/add-group",
                    element: <AddGroupModal />,
                  },
                ],
              },
              {
                path: "/group-content",
                element: <GroupContent />,
              },
              {
                path: "/dashboard",
                element: <Dashboard />,
              },
            ],
          },
        ],
      },
    ],
    { basename: "/thatswhy_items_counter/" }
  );

  return (
    <React.StrictMode>
      <ThemeProvider theme = {theme}>
        <RouterProvider router = {router} />
      </ThemeProvider>
    </React.StrictMode>
  );
};

Если вам нужна дополнительная информация, вот репозиторий проекта на GitHub: ссылка на репо Заранее спасибо всем за помощь!

Я попытался переместить свои функции входа из пользовательского перехватчика входа в компонент ContextProvider. Я пытался управлять состоянием условной загрузки в компоненте PrivateRoute. Я также попытался обновить свое текущее состояниеUser в функции входа в компонент ContextProvider. К сожалению, мне это не помогло. Я думаю, что у меня возникла эта проблема, потому что моей функции входа в систему требуется больше времени для входа в систему пользователя, а моя функция handleLogIn не ждет.

проблема со входом в систему gif

🤔 А знаете ли вы, что...
React Testing Library - это инструмент для тестирования компонентов React.


91
1

Ответ:

Решено

Вы объявили свой router в ReactTree, поэтому, когда компонент по какой-либо причине перерисовывается, router переобъявляется, отключает старое дерево маршрутизации и монтирует новое, и это прерывает любые активные действия навигации.

Переместите объявление router из ReactTree.

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
    children: [
      {
        path: "/sign-in",
        element: <LogIn />,
      },
      {
        path: "/sign-up",
        element: <Register />,
      },
      {
        element: <PrivateRoute />,
        children: [
          {
            path: "/",
            element: <GroupMenu />,
            children: [
              {
                path: "/add-group",
                element: <AddGroupModal />,
              },
            ],
          },
          {
            path: "/group-content",
            element: <GroupContent />,
          },
          {
            path: "/dashboard",
            element: <Dashboard />,
          },
        ],
      },
    ],
  },
]);

const Main = () => {
  return (
    <React.StrictMode>
      <ThemeProvider theme = {theme}>
        <RouterProvider router = {router} />
      </ThemeProvider>
    </React.StrictMode>
  );
};

Или запомните его, чтобы его можно было использовать в качестве стабильного справочного материала.

const Main = () => {
  const router = useMemo(createBrowserRouter([
    {
      path: "/",
      element: <App />,
      children: [
        {
          path: "/sign-in",
          element: <LogIn />,
        },
        {
          path: "/sign-up",
          element: <Register />,
        },
        {
          element: <PrivateRoute />,
          children: [
            {
              path: "/",
              element: <GroupMenu />,
              children: [
                {
                  path: "/add-group",
                  element: <AddGroupModal />,
                },
              ],
            },
            {
              path: "/group-content",
              element: <GroupContent />,
            },
            {
              path: "/dashboard",
              element: <Dashboard />,
            },
          ],
        },
      ],
    },
  ]), []);
  
  return (
    <React.StrictMode>
      <ThemeProvider theme = {theme}>
        <RouterProvider router = {router} />
      </ThemeProvider>
    </React.StrictMode>
  );
};

Я рекомендую также преобразовать ваш обратный вызов handleLogIn в функцию async, чтобы вы могли awaitlogIn разрешать состояние loading и лучше управлять им, т. е. устанавливать loading false только после того, как попытка входа в систему была успешной или неудачной.

const handleLogIn = async (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  if (!/^[a-zA-Z\s].*@.*$/.test(logInValues.email)) {
    return setLogInError("email is not correct");
  }
  if (logInValues.password === "") {
    return setLogInError("password field can not be empty");
  }

  try {
    setLogInError("");
    setLoading(true);

    await logIn(logInValues.email, logInValues.password);

    navigate("/");
  } catch (errors) {
    setLogInError("Failed to log-in in account");
  } finally {
    setLoading(false);
  }
};

Кажется, все еще существует небольшая проблема с синхронизацией между тем, когда logIn или auth.signInWithEmailAndPassword успешно аутентифицирует пользователя, и когда Firebase может отправить изменение аутентификации вашему onAuthStateChange прослушивателю, который обновляет currentUser состояние на стороне клиента.

Вот еще пара вещей, которые вы можете попробовать:

  • Захватите значение userCredential.user (см. UserCredential) из разрешенного вызова signInWithEmailAndPassword и обновите состояние currentUser.

    const ContextProvider = ({ children }: { children: React.ReactNode }) => {
      const navigate = useNavigate();
      const [currentUser, setCurrentUser] = useState<firebase.User | undefined | null>(undefined);
    
      ...
    
      const logIn = (email: string, password: string) => {
        return auth.signInWithEmailAndPassword(email, password);
      };
    
      const handleLogIn = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        if (!/^[a-zA-Z\s].*@.*$/.test(logInValues.email)) {
          return setLogInError("email is not correct");
        }
        if (logInValues.password === "") {
          return setLogInError("password field can not be empty");
        }
    
        try {
          setLogInError("");
          setLoading(true);
    
          const userCredential = await logIn(logInValues.email, logInValues.password);
          setCurrentUser(userCredential.user);
    
          navigate("/");
        } catch (errors) {
          setLogInError("Failed to log-in in account");
        } finally {
          setLoading(false);
        }
      };
    
    ...
    
  • Поместите вызов navigate в конец очереди событий Javascript. Обратите внимание, что это своего рода хак, но задержка вызова navigate позволяет Javascript сначала обработать любые другие дополнительные обратные вызовы, поставленные в очередь, а затем выполнить действие навигации.

    const handleLogIn = async (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault();
      if (!/^[a-zA-Z\s].*@.*$/.test(logInValues.email)) {
        return setLogInError("email is not correct");
      }
      if (logInValues.password === "") {
        return setLogInError("password field can not be empty");
      }
    
      try {
        setLogInError("");
        setLoading(true);
    
        await logIn(logInValues.email, logInValues.password);
    
        setTimeout(() => navigate("/"));
      } catch (errors) {
        setLogInError("Failed to log-in in account");
      } finally {
        setLoading(false);
      }
    };