Feature completa de login no CrazyStack Next.js

Dev Doido - Mar 10 '23 - - Dev Community

Nesta aula, você irá aprender a construir uma feature completa de login, incluindo validação de formulários com Yup e gerenciamento de estado com React Hook Form. O objetivo é construir uma interface de login reutilizável e fácil de usar com validações de formulários eficientes e gerenciamento de estado simplificado.

O vídeo dessa aula está publicada no bootcamp CrazyStack, se você ainda não garantiu sua vaga clique aqui

Antes de começarmos, é importante ter uma compreensão básica de hooks e Yup. Se você ainda não tem essa compreensão, recomendo fazer uma pesquisa básica sobre esses conceitos antes de prosseguir.

Começaremos importando as dependências necessárias e criando um schema de validação com Yup. O schema define as regras de validação que serão aplicadas aos dados do formulário antes de enviá-los para o servidor. Em seguida, utilizaremos o React Hook Form para gerenciar o estado do formulário e registrar os campos de entrada.

Em seguida, adicionaremos as validações Yup aos campos do formulário usando o método "register" e "handleSubmit" fornecidos pelo React Hook Form. Ao clicar no botão "Enviar", o formulário será submetido e as validações Yup serão aplicadas aos dados do formulário. Se houver algum erro, será exibido uma mensagem de erro para o usuário.

Finalmente, adicionaremos um recurso de "loading" ao botão de envio para fornecer feedback visual ao usuário durante a submissão do formulário.

Com isso, você terá uma feature completa de login usando React Hook Form e Yup, além de ter aproveitado o formulário genérico construído na aula anterior. Essa solução é escalável e fácil de manter, além de ser altamente reutilizável em outros projetos.

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { SubmitHandler, useForm } from "react-hook-form";

export interface LoginFormData {
  email: string;
  password: string;
}
export type SubmitLoginHandler = SubmitHandler<LoginFormData>;

export const loginSchema = yup.object().shape({
  email: yup.string().email("Email inválido").required("Email é obrigatório"),
  password: yup.string().required("Senha é obrigatória"),
});

export const useLoginLib = () => {
  const formProps = useForm({ resolver: yupResolver(loginSchema) });
  return { ...formProps };
}; 
Enter fullscreen mode Exit fullscreen mode

O código acima define uma biblioteca de login utilizando react-hook-form e yup para validação de formulários.

A interface LoginFormData define o formato dos dados do formulário de login, que consiste em um email e uma senha.

A constante loginSchema é uma especificação do yup que define as regras de validação dos dados do formulário de login. Ela requer que o email seja válido e obrigatório e que a senha seja obrigatória.

A função useLoginLib é a biblioteca em si. Ela usa o useForm do react-hook-form e o yupResolver para informar ao react-hook-form que a validação dos dados do formulário será feita com o loginSchema do yup.

Assim, a biblioteca retorna as propriedades do formulário gerado pelo useForm, que poderá ser utilizado em um componente de formulário de login.

import { useAuth } from "shared/libs";
import { useLoginLib, SubmitLoginHandler } from "./login.lib";

export const useLogin = () => {
  const { login } = useAuth();
  const { handleSubmit, register, formState } = useLoginLib();
  const handleLogin: SubmitLoginHandler = async (data) => {
    await login(data);
  };
  return { formState, register, handleSubmit, handleLogin };
}; 
Enter fullscreen mode Exit fullscreen mode

Este código é uma função de hook, useLogin, que retorna informações úteis para a renderização e manipulação de um formulário de login. Ele usa o hook useAuth da biblioteca compartilhada shared/libs e o hook useLoginLib que importa o hook useForm e a definição de uma interface e um esquema de validação Yup para os dados do formulário de login.

A função useLogin faz uso dos hooks importados para definir a função handleLogin, que será chamada quando o formulário for enviado. Esta função chama a função login da biblioteca compartilhada useAuth e passa os dados do formulário para ela. Além disso, a função useLogin retorna os valores retornados pelos hooks useLoginLib e useAuth, incluindo formState, register, handleSubmit e handleLogin.

formState é o estado atual do formulário, incluindo informações como se o formulário está sendo enviado, se há erros de validação, entre outros. register é a função que permite registrar inputs no formulário para que sejam validados. handleSubmit é a função que controla o envio do formulário e handleLogin é a função que será chamada quando o formulário for enviado.

import { Form } from "shared/ui";
import { useLogin } from "./login.hook";
export const LoginForm = () => {
  const { formState, handleSubmit, register, handleLogin } = useLogin();
  const formProps = {
    formState,
    register,
    handleCustomSubmit: handleLogin,
    handleSubmit,
    formControls: [
      { type: "email", label: "Email", name: "email" },
      { type: "password", label: "Senha", name: "password" },
    ],
    buttonLabel: "Entrar",
  };
  return <Form {...formProps} />;
}; 
Enter fullscreen mode Exit fullscreen mode

O código acima é uma implementação de um formulário de login usando a biblioteca React Hook Form e o componente genérico Form.

Ele começa importando o componente Form da biblioteca shared/ui e o hook useLogin de ./login.hook. Em seguida, a função LoginForm é definida.

Dentro da função LoginForm, o hook useLogin é invocado para retornar as informações necessárias para configurar o formulário:

  • formState: objeto que contém informações sobre o estado atual do formulário, como se ele está sendo enviado, se há erros, entre outros.
  • handleSubmit: função que é invocada quando o formulário é submetido.
  • register: função que permite registrar os inputs no formulário para serem gerenciados pelo React Hook Form.
  • handleLogin: função que é chamada quando o formulário é submetido e contém a lógica de autenticação do usuário.

Em seguida, as propriedades do formulário são definidas em formProps. Essas propriedades incluem:

  • formState: objeto que contém informações sobre o estado atual do formulário.
  • register: função que permite registrar os inputs no formulário.
  • handleCustomSubmit: função que é invocada quando o formulário é submetido. Nesse caso, é a função handleLogin.
  • handleSubmit: função que é invocada quando o formulário é submetido.
  • formControls: array de objetos que representam os controles (inputs) do formulário. Cada objeto possui informações sobre o tipo de controle, o rótulo, o nome e outras informações.
  • buttonLabel: texto que será exibido no botão de envio do formulário.

Por fim, o componente Form é retornado e suas propriedades são passadas para ele através do spread operator ({...formProps}).

Tá mas... como eu transformo tudo isso num componente ainda mais genérico?

Para transformar em um componente genérico, você pode fazer algumas alterações nas suas libs, hooks e componentes. Aqui estão algumas sugestões:

  1. Em login.lib, você pode separar as validações Yup para uma lib separada e importá-la no login.lib. Desta forma, é possível reutilizar a mesma validação em outros formulários.

  2. Em login.hook, você pode criar um hook genérico que possa ser usado em outros formulários. Por exemplo, você pode criar um hook useFormWithAuth que receba a função de autenticação como uma propriedade e usar a mesma estrutura que você usou em useLogin.

  3. No componente LoginForm, você pode criar um componente genérico que possa ser reutilizado em outros formulários. Por exemplo, você pode criar um componente FormWithAuth que receba o formulário em si, as validações, o formato da resposta e a função de autenticação. Este componente pode então passar essas propriedades para o componente Form que você criou anteriormente.

Essas são apenas algumas sugestões e existem várias maneiras de tornar o código genérico, mas a ideia é passar as propriedades relevantes para o componente genérico, como o formulário em si, as validações, o formato da resposta e a função de autenticação, para que ele possa ser reutilizado em outros formulários.
O código ficaria da seguinte forma:

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { SubmitHandler, useForm } from "react-hook-form";
import { Form } from "shared/ui";

interface FormData {
  [key: string]: string;
}

interface FormConfig<T> {
  fields: { type: string; label: string; name: keyof T }[];
  buttonLabel: string;
  validationSchema: yup.ObjectSchema<yup.Shape<{}, T>>;
  onSubmit: SubmitHandler<T>;
}

export const useGenericForm = <T extends FormData>(config: FormConfig<T>) => {
  const formProps = useForm({ resolver: yupResolver(config.validationSchema) });
  const formState = formProps.formState;
  const register = formProps.register;
  const handleSubmit = formProps.handleSubmit;
  return { formState, register, handleSubmit, config };
};

export const GenericForm = <T extends FormData>(props: {
  config: FormConfig<T>;
}) => {
  const { formState, register, handleSubmit, config } = useGenericForm(
    props.config
  );
  const formProps = {
    formState,
    register,
    handleSubmit,
    formControls: config.fields,
    buttonLabel: config.buttonLabel,
  };
  return <Form {...formProps} />;
};

Enter fullscreen mode Exit fullscreen mode

E para utilizar esse componente, basta passar a configuração do formulário como propriedade, por exemplo:

import { useAuth } from "shared/libs";
import { GenericForm } from "./generic.form";

const loginSchema = yup.object().shape({
  email: yup.string().email("Email inválido").required("Email é obrigatório"),
  password: yup.string().required("Senha é obrigatória"),
});

const handleLogin = async (data: LoginFormData) => {
  await useAuth().login(data);
};

const LoginForm = () => {
  const config = {
    fields: [
      { type: "email", label: "Email", name: "email" },
      { type: "password", label: "Senha", name: "password" },
    ],
    buttonLabel: "Entrar",
    validationSchema: loginSchema,
    onSubmit: handleLogin,
  };
  return <GenericForm config={config} />;
};

Enter fullscreen mode Exit fullscreen mode

E a seguir temos o exemplo de uso do LoginForm anterior:

import Router from "next/router";
import type { NextPage } from "next";
import { Head, Flex, Text } from "shared/ui";
import { useAuth } from "shared/libs";
import { useEffect } from "react";
import { LoginForm } from "features/auth/login";
export const Login: NextPage = () => {
  const { isAuthenticated } = useAuth();
  useEffect(() => {
    if (isAuthenticated) {
      Router.push("/home");
    }
  }, [isAuthenticated]);
  return (
    <>
      <Head
        title="Belezix Admin | Login"
        description="Página de login do painel Admin do Belezix"
      />
      <Flex minW="100%" justifyContent="center">
        {!isAuthenticated && (
          <Flex mt="15%">
            <LoginForm />
          </Flex>
        )}
      </Flex>
    </>
  );
}; 
Enter fullscreen mode Exit fullscreen mode

Este código define a página de login para o painel administrativo de um aplicativo. O código usa bibliotecas da interface do usuário (shared/ui) e a biblioteca de autenticação (shared/libs).

A página de login é criada como um componente NextPage, que é uma forma de definir páginas dinâmicas usando a biblioteca Next.js. O componente NextPage é importado do next e é definido como Login.

O componente useAuth é usado para obter informações sobre o estado de autenticação do usuário. isAuthenticated é uma das informações retornadas pelo useAuth.

O gancho useEffect é usado para verificar se o usuário está autenticado a cada renderização. Se o usuário estiver autenticado, o Router.push é usado para redirecioná-lo para a página inicial do aplicativo (/home).

A página de login é composta de um cabeçalho (Head), um contêiner (Flex) e um formulário de login (LoginForm). O cabeçalho Head é usado para definir o título e a descrição da página. O contêiner é usado para centralizar o formulário na tela.

O formulário de login é exibido apenas se o usuário não estiver autenticado. Se o usuário estiver autenticado, a página não será exibida.

O formulário é importado do arquivo features/auth/login e é renderizado dentro do contêiner. A renderização do formulário é controlada pelos parâmetros passados ao componente Form usando a sintaxe de espalhamento (...formProps).

Como a metodologia Feature Sliced Design se encaixa nisso?

A metodologia Feature Sliced Design se baseia em desenvolver as funcionalidades do seu aplicativo de forma separada e autônoma, para depois juntá-las e formar o produto final. No exemplo de login form, a pasta features/auth/login contém todos os componentes, hooks e validações necessários para o formulário de login. Dessa forma, se for necessário fazer alguma alteração no formulário, será possível fazê-lo apenas nessa pasta, sem precisar mexer em outras partes do aplicativo. Além disso, se a equipe precisar desenvolver uma nova feature, será mais fácil fazê-lo de forma autônoma sem interferir nas outras funcionalidades já existentes.

O arquivo LoginForm é o componente visual responsável por exibir o formulário, enquanto o hook useLogin é responsável por lidar com o submit do formulário e com a validação dos dados. Já o hook useLoginLib é responsável por inicializar e configurar o hook useForm do React Hook Form, passando a validação através do Yup.

Dessa forma, ao separar o formulário de login em uma pasta específica, é possível desenvolver, testar e manter a feature de forma mais organizada, mantendo a responsabilidade de cada arquivo claramente separada.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .