React & Frontend

Guia Completo de Formulários Multi-step em React: Estado, Validação e Navegação

20 min de leitura

Guia Completo de Formulários Multi-step em React: Estado, Validação e Navegação

Entendendo Formulários Multi-step em React Um formulário multi-step, também conhecido como formulário em etapas ou wizard, é um padrão de interface que divide um formulário longo em várias páginas ou etapas menores. Em vez de apresentar todos os campos de uma vez, o usuário passa por etapas sequenciais, preenchendo informações gradualmente. Este padrão melhora a experiência do usuário ao reduzir a cognitiva carga, aumentar a taxa de conclusão e permitir validações parciais. Em React, implementar um formulário multi-step exige gerenciar três aspectos principais: o estado atual do formulário (qual etapa estamos), os dados preenchidos em cada etapa, e a lógica de navegação entre elas. A abordagem mais comum é usar um componente pai que mantém o estado completo do formulário e renderiza condicionalmente a etapa ativa, enquanto componentes filhos lidam com a exibição de campos específicos. Por que não é trivial? Diferentemente de um formulário simples, você precisa decidir: os dados preenchidos devem ser mantidos quando o usuário navega entre

<h2>Entendendo Formulários Multi-step em React</h2>

<p>Um formulário multi-step, também conhecido como formulário em etapas ou wizard, é um padrão de interface que divide um formulário longo em várias páginas ou etapas menores. Em vez de apresentar todos os campos de uma vez, o usuário passa por etapas sequenciais, preenchendo informações gradualmente. Este padrão melhora a experiência do usuário ao reduzir a cognitiva carga, aumentar a taxa de conclusão e permitir validações parciais.</p>

<p>Em React, implementar um formulário multi-step exige gerenciar três aspectos principais: o estado atual do formulário (qual etapa estamos), os dados preenchidos em cada etapa, e a lógica de navegação entre elas. A abordagem mais comum é usar um componente pai que mantém o estado completo do formulário e renderiza condicionalmente a etapa ativa, enquanto componentes filhos lidam com a exibição de campos específicos.</p>

<h3>Por que não é trivial?</h3>

<p>Diferentemente de um formulário simples, você precisa decidir: os dados preenchidos devem ser mantidos quando o usuário navega entre etapas? Como validar sem bloquear a navegação para trás? Como gerenciar o estado de forma escalável quando o formulário cresce? Essas questões definem a qualidade da experiência final.</p>

<h2>Gerenciamento de Estado</h2>

<h3>Estruturando o estado do formulário</h3>

<p>O estado deve armazenar não apenas os valores dos campos, mas também metadados úteis como erros de validação, estados de toque (se o campo foi visitado) e a etapa atual. Uma estrutura bem organizada facilita depuração, testes e manutenção futura.</p>

<pre><code class="language-jsx">const [formData, setFormData] = useState({

// Etapa 1

firstName: &#039;&#039;,

lastName: &#039;&#039;,

email: &#039;&#039;,

// Etapa 2

phone: &#039;&#039;,

address: &#039;&#039;,

city: &#039;&#039;,

// Etapa 3

cardNumber: &#039;&#039;,

expiryDate: &#039;&#039;,

cvv: &#039;&#039;

});

const [currentStep, setCurrentStep] = useState(1);

const [errors, setErrors] = useState({});

const [touched, setTouched] = useState({});</code></pre>

<p>Esta estrutura simples permite que você mantenha todos os dados simultaneamente, evitando perda de informações quando o usuário navega. O objeto <code>touched</code> rastreia quais campos o usuário já interagiu, útil para mostrar erros apenas após o usuário sair do campo.</p>

<h3>Atualizando valores de campos</h3>

<p>Crie uma função genérica que atualiza o estado do formulário mantendo os valores anteriores. Esta abordagem é escalável e evita repetição de código.</p>

<pre><code class="language-jsx">const handleInputChange = (e) =&gt; {

const { name, value } = e.target;

setFormData(prevState =&gt; ({

...prevState,

[name]: value

}));

};

const handleFieldTouched = (fieldName) =&gt; {

setTouched(prevState =&gt; ({

...prevState,

[fieldName]: true

}));

};</code></pre>

<p>Quando o usuário digita em um campo, <code>handleInputChange</code> atualiza seu valor. Quando sai do campo (evento <code>onBlur</code>), <code>handleFieldTouched</code> marca-o como tocado. Essa separação permite que você mostre mensagens de erro apenas para campos já visitados, melhorando a experiência.</p>

<h2>Validação em Formulários Multi-step</h2>

<h3>Estratégia de validação por etapa</h3>

<p>A validação deve ocorrer em dois momentos: quando o usuário tenta avançar para a próxima etapa e, opcionalmente, em tempo real após o campo ser tocado. Validar apenas na etapa atual evita overhead desnecessário e oferece feedback contextualizado.</p>

<pre><code class="language-jsx">const validationRules = {

step1: {

firstName: (value) =&gt; {

if (!value.trim()) return &#039;Nome é obrigatório&#039;;

if (value.length &lt; 2) return &#039;Nome deve ter pelo menos 2 caracteres&#039;;

return null;

},

lastName: (value) =&gt; {

if (!value.trim()) return &#039;Sobrenome é obrigatório&#039;;

return null;

},

email: (value) =&gt; {

if (!value.trim()) return &#039;E-mail é obrigatório&#039;;

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

if (!emailRegex.test(value)) return &#039;E-mail inválido&#039;;

return null;

}

},

step2: {

phone: (value) =&gt; {

if (!value.trim()) return &#039;Telefone é obrigatório&#039;;

const phoneRegex = /^[\d\s\-()]{10,}$/;

if (!phoneRegex.test(value)) return &#039;Telefone inválido&#039;;

return null;

},

address: (value) =&gt; {

if (!value.trim()) return &#039;Endereço é obrigatório&#039;;

return null;

},

city: (value) =&gt; {

if (!value.trim()) return &#039;Cidade é obrigatória&#039;;

return null;

}

},

step3: {

cardNumber: (value) =&gt; {

if (!value.trim()) return &#039;Número do cartão é obrigatório&#039;;

const cardRegex = /^[\d\s]{13,19}$/;

if (!cardRegex.test(value)) return &#039;Número de cartão inválido&#039;;

return null;

},

expiryDate: (value) =&gt; {

if (!value.trim()) return &#039;Data de expiração é obrigatória&#039;;

const dateRegex = /^(0[1-9]|1[0-2])\/\d{2}$/;

if (!dateRegex.test(value)) return &#039;Formato: MM/AA&#039;;

return null;

},

cvv: (value) =&gt; {

if (!value.trim()) return &#039;CVV é obrigatório&#039;;

if (!/^\d{3,4}$/.test(value)) return &#039;CVV deve ter 3 ou 4 dígitos&#039;;

return null;

}

}

};

const validateStep = (stepNumber) =&gt; {

const stepKey = step${stepNumber};

const stepFields = validationRules[stepKey];

const newErrors = {};

Object.keys(stepFields).forEach(fieldName =&gt; {

const validator = stepFields[fieldName];

const error = validator(formData[fieldName]);

if (error) {

newErrors[fieldName] = error;

}

});

setErrors(newErrors);

return Object.keys(newErrors).length === 0;

};</code></pre>

<p>Essa estrutura separa as regras de validação da lógica de aplicação. Cada campo tem uma função que valida seu valor e retorna uma mensagem de erro ou <code>null</code>. A função <code>validateStep</code> aplica todas as regras de uma etapa específica e retorna um booleano indicando sucesso.</p>

<h3>Validação em tempo real</h3>

<p>Para melhorar a experiência, valide campos individuais enquanto o usuário digita, mas apenas se o campo já foi tocado.</p>

<pre><code class="language-jsx">const validateField = (fieldName, value) =&gt; {

const fieldValidators = Object.entries(validationRules).reduce(

(acc, [stepKey, fields]) =&gt; ({ ...acc, ...fields }),

{}

);

const validator = fieldValidators[fieldName];

return validator ? validator(value) : null;

};

const handleInputChange = (e) =&gt; {

const { name, value } = e.target;

setFormData(prevState =&gt; ({

...prevState,

[name]: value

}));

// Validar em tempo real apenas se o campo foi tocado

if (touched[name]) {

const error = validateField(name, value);

setErrors(prevErrors =&gt; ({

...prevErrors,

[name]: error

}));

}

};</code></pre>

<p>Agora, à medida que o usuário digita em um campo já visitado, os erros aparecem ou desaparecem em tempo real. Isso oferece feedback imediato sem ser intrusivo para campos ainda não tocados.</p>

<h2>Navegação e Fluxo de Etapas</h2>

<h3>Funções de navegação</h3>

<p>Implemente funções que gerenciam a transição entre etapas, validando a etapa atual antes de avançar e permitindo voltar sem validação.</p>

<pre><code class="language-jsx">const handleNext = () =&gt; {

if (validateStep(currentStep)) {

setCurrentStep(prevStep =&gt; prevStep + 1);

window.scrollTo(0, 0); // Scroll para o topo

}

};

const handlePrevious = () =&gt; {

setCurrentStep(prevStep =&gt; Math.max(1, prevStep - 1));

window.scrollTo(0, 0);

};

const handleSubmit = (e) =&gt; {

e.preventDefault();

// Validar a última etapa também

if (validateStep(currentStep)) {

console.log(&#039;Formulário completo:&#039;, formData);

// Enviar para servidor, salvar em banco de dados, etc.

}

};</code></pre>

<p>A função <code>handleNext</code> valida a etapa atual usando <code>validateStep</code>. Se há erros, a etapa não muda. A função <code>handlePrevious</code> permite voltar sem validação, preservando dados. O <code>window.scrollTo(0, 0)</code> melhora a experiência rolando para o topo quando a etapa muda.</p>

<h3>Componente de renderização condicional</h3>

<p>Estruture o componente principal para renderizar diferentes componentes de etapa conforme <code>currentStep</code> muda.</p>

<pre><code class="language-jsx">function MultiStepForm() {

// ... estado definido anteriormente ...

const renderStep = () =&gt; {

switch (currentStep) {

case 1:

return (

&lt;StepOne

data={formData}

errors={errors}

touched={touched}

onChange={handleInputChange}

onBlur={handleFieldTouched}

/&gt;

);

case 2:

return (

&lt;StepTwo

data={formData}

errors={errors}

touched={touched}

onChange={handleInputChange}

onBlur={handleFieldTouched}

/&gt;

);

case 3:

return (

&lt;StepThree

data={formData}

errors={errors}

touched={touched}

onChange={handleInputChange}

onBlur={handleFieldTouched}

/&gt;

);

default:

return null;

}

};

return (

&lt;div className=&quot;form-container&quot;&gt;

&lt;div className=&quot;progress-bar&quot;&gt;

&lt;div

className=&quot;progress-fill&quot;

style={{ width: ${(currentStep / 3) * 100}% }}

/&gt;

&lt;/div&gt;

&lt;form onSubmit={handleSubmit}&gt;

{renderStep()}

&lt;div className=&quot;button-group&quot;&gt;

{currentStep &gt; 1 &amp;&amp; (

&lt;button

type=&quot;button&quot;

onClick={handlePrevious}

className=&quot;btn-secondary&quot;

&gt;

Voltar

&lt;/button&gt;

)}

{currentStep &lt; 3 ? (

&lt;button

type=&quot;button&quot;

onClick={handleNext}

className=&quot;btn-primary&quot;

&gt;

Próximo

&lt;/button&gt;

) : (

&lt;button

type=&quot;submit&quot;

className=&quot;btn-success&quot;

&gt;

Enviar

&lt;/button&gt;

)}

&lt;/div&gt;

&lt;/form&gt;

&lt;/div&gt;

);

}</code></pre>

<p>Este padrão é claro e escalável. Um <code>switch</code> renderiza diferentes componentes conforme a etapa. Os botões mudam de &quot;Próximo&quot; para &quot;Enviar&quot; na última etapa. A barra de progresso visual melhora a orientação do usuário.</p>

<h3>Componentes de etapa individuais</h3>

<p>Cada etapa é um componente reutilizável que exibe campos específicos. Mantê-los separados melhora legibilidade e testabilidade.</p>

<pre><code class="language-jsx">function StepOne({ data, errors, touched, onChange, onBlur }) {

return (

&lt;div className=&quot;step-container&quot;&gt;

&lt;h2&gt;Informações Pessoais&lt;/h2&gt;

&lt;div className=&quot;form-group&quot;&gt;

&lt;label htmlFor=&quot;firstName&quot;&gt;Nome *&lt;/label&gt;

&lt;input

id=&quot;firstName&quot;

type=&quot;text&quot;

name=&quot;firstName&quot;

value={data.firstName}

onChange={onChange}

onBlur={() =&gt; onBlur(&#039;firstName&#039;)}

className={errors.firstName &amp;&amp; touched.firstName ? &#039;input-error&#039; : &#039;&#039;}

/&gt;

{errors.firstName &amp;&amp; touched.firstName &amp;&amp; (

&lt;span className=&quot;error-message&quot;&gt;{errors.firstName}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div className=&quot;form-group&quot;&gt;

&lt;label htmlFor=&quot;lastName&quot;&gt;Sobrenome *&lt;/label&gt;

&lt;input

id=&quot;lastName&quot;

type=&quot;text&quot;

name=&quot;lastName&quot;

value={data.lastName}

onChange={onChange}

onBlur={() =&gt; onBlur(&#039;lastName&#039;)}

className={errors.lastName &amp;&amp; touched.lastName ? &#039;input-error&#039; : &#039;&#039;}

/&gt;

{errors.lastName &amp;&amp; touched.lastName &amp;&amp; (

&lt;span className=&quot;error-message&quot;&gt;{errors.lastName}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div className=&quot;form-group&quot;&gt;

&lt;label htmlFor=&quot;email&quot;&gt;E-mail *&lt;/label&gt;

&lt;input

id=&quot;email&quot;

type=&quot;email&quot;

name=&quot;email&quot;

value={data.email}

onChange={onChange}

onBlur={() =&gt; onBlur(&#039;email&#039;)}

className={errors.email &amp;&amp; touched.email ? &#039;input-error&#039; : &#039;&#039;}

/&gt;

{errors.email &amp;&amp; touched.email &amp;&amp; (

&lt;span className=&quot;error-message&quot;&gt;{errors.email}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;/div&gt;

);

}

function StepTwo({ data, errors, touched, onChange, onBlur }) {

return (

&lt;div className=&quot;step-container&quot;&gt;

&lt;h2&gt;Endereço&lt;/h2&gt;

&lt;div className=&quot;form-group&quot;&gt;

&lt;label htmlFor=&quot;phone&quot;&gt;Telefone *&lt;/label&gt;

&lt;input

id=&quot;phone&quot;

type=&quot;tel&quot;

name=&quot;phone&quot;

value={data.phone}

onChange={onChange}

onBlur={() =&gt; onBlur(&#039;phone&#039;)}

className={errors.phone &amp;&amp; touched.phone ? &#039;input-error&#039; : &#039;&#039;}

placeholder=&quot;(XX) XXXXX-XXXX&quot;

/&gt;

{errors.phone &amp;&amp; touched.phone &amp;&amp; (

&lt;span className=&quot;error-message&quot;&gt;{errors.phone}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div className=&quot;form-group&quot;&gt;

&lt;label htmlFor=&quot;address&quot;&gt;Endereço *&lt;/label&gt;

&lt;input

id=&quot;address&quot;

type=&quot;text&quot;

name=&quot;address&quot;

value={data.address}

onChange={onChange}

onBlur={() =&gt; onBlur(&#039;address&#039;)}

className={errors.address &amp;&amp; touched.address ? &#039;input-error&#039; : &#039;&#039;}

/&gt;

{errors.address &amp;&amp; touched.address &amp;&amp; (

&lt;span className=&quot;error-message&quot;&gt;{errors.address}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div className=&quot;form-group&quot;&gt;

&lt;label htmlFor=&quot;city&quot;&gt;Cidade *&lt;/label&gt;

&lt;input

id=&quot;city&quot;

type=&quot;text&quot;

name=&quot;city&quot;

value={data.city}

onChange={onChange}

onBlur={() =&gt; onBlur(&#039;city&#039;)}

className={errors.city &amp;&amp; touched.city ? &#039;input-error&#039; : &#039;&#039;}

/&gt;

{errors.city &amp;&amp; touched.city &amp;&amp; (

&lt;span className=&quot;error-message&quot;&gt;{errors.city}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;/div&gt;

);

}

function StepThree({ data, errors, touched, onChange, onBlur }) {

return (

&lt;div className=&quot;step-container&quot;&gt;

&lt;h2&gt;Informações de Pagamento&lt;/h2&gt;

&lt;div className=&quot;form-group&quot;&gt;

&lt;label htmlFor=&quot;cardNumber&quot;&gt;Número do Cartão *&lt;/label&gt;

&lt;input

id=&quot;cardNumber&quot;

type=&quot;text&quot;

name=&quot;cardNumber&quot;

value={data.cardNumber}

onChange={onChange}

onBlur={() =&gt; onBlur(&#039;cardNumber&#039;)}

className={errors.cardNumber &amp;&amp; touched.cardNumber ? &#039;input-error&#039; : &#039;&#039;}

placeholder=&quot;XXXX XXXX XXXX XXXX&quot;

/&gt;

{errors.cardNumber &amp;&amp; touched.cardNumber &amp;&amp; (

&lt;span className=&quot;error-message&quot;&gt;{errors.cardNumber}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div className=&quot;form-row&quot;&gt;

&lt;div className=&quot;form-group&quot;&gt;

&lt;label htmlFor=&quot;expiryDate&quot;&gt;Data de Expiração *&lt;/label&gt;

&lt;input

id=&quot;expiryDate&quot;

type=&quot;text&quot;

name=&quot;expiryDate&quot;

value={data.expiryDate}

onChange={onChange}

onBlur={() =&gt; onBlur(&#039;expiryDate&#039;)}

className={errors.expiryDate &amp;&amp; touched.expiryDate ? &#039;input-error&#039; : &#039;&#039;}

placeholder=&quot;MM/AA&quot;

/&gt;

{errors.expiryDate &amp;&amp; touched.expiryDate &amp;&amp; (

&lt;span className=&quot;error-message&quot;&gt;{errors.expiryDate}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div className=&quot;form-group&quot;&gt;

&lt;label htmlFor=&quot;cvv&quot;&gt;CVV *&lt;/label&gt;

&lt;input

id=&quot;cvv&quot;

type=&quot;text&quot;

name=&quot;cvv&quot;

value={data.cvv}

onChange={onChange}

onBlur={() =&gt; onBlur(&#039;cvv&#039;)}

className={errors.cvv &amp;&amp; touched.cvv ? &#039;input-error&#039; : &#039;&#039;}

placeholder=&quot;XXX&quot;

/&gt;

{errors.cvv &amp;&amp; touched.cvv &amp;&amp; (

&lt;span className=&quot;error-message&quot;&gt;{errors.cvv}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;/div&gt;

&lt;/div&gt;

);

}</code></pre>

<p>Cada componente de etapa recebe props para dados, erros, campos tocados e callbacks. Condicionalmente exibe mensagens de erro apenas para campos tocados. Essa estrutura modular permite reutilização e testes independentes.</p>

<h2>Aprimoramentos Avançados</h2>

<h3>Persistência de dados com LocalStorage</h3>

<p>Para formulários longos, salve os dados periodicamente no navegador para evitar perda de informações se a página for recarregada.</p>

<pre><code class="language-jsx">useEffect(() =&gt; {

// Salvar formData no localStorage a cada mudança

localStorage.setItem(&#039;multiStepFormData&#039;, JSON.stringify(formData));

}, [formData]);

useEffect(() =&gt; {

// Recuperar formData ao montar o componente

const savedData = localStorage.getItem(&#039;multiStepFormData&#039;);

if (savedData) {

try {

setFormData(JSON.parse(savedData));

} catch (error) {

console.error(&#039;Erro ao carregar dados salvos:&#039;, error);

}

}

}, []);</code></pre>

<p>Este efeito salva automaticamente os dados do formulário sempre que mudam. Ao recarregar a página, os dados são restaurados. Embora simples, evita frustrações de perda de dados.</p>

<h3>Desabilitar campos condicionais</h3>

<p>Alguns campos podem ser opcionais ou habilitados apenas se outras condições forem atendidas. Implemente lógica condicional nos componentes de etapa.</p>

<pre><code class="language-jsx">function StepTwo({ data, errors, touched, onChange, onBlur }) {

const [isInternational, setIsInternational] = useState(false);

return (

&lt;div className=&quot;step-container&quot;&gt;

&lt;h2&gt;Endereço&lt;/h2&gt;

&lt;div className=&quot;form-group&quot;&gt;

&lt;label&gt;

&lt;input

type=&quot;checkbox&quot;

checked={isInternational}

onChange={(e) =&gt; setIsInternational(e.target.checked)}

/&gt;

Endereço Internacional

&lt;/label&gt;

&lt;/div&gt;

{isInternational &amp;&amp; (

&lt;div className=&quot;form-group&quot;&gt;

&lt;label htmlFor=&quot;country&quot;&gt;País *&lt;/label&gt;

&lt;input

id=&quot;country&quot;

type=&quot;text&quot;

name=&quot;country&quot;

// ... props ...

/&gt;

&lt;/div&gt;

)}

{/ ... restante dos campos ... /}

&lt;/div&gt;

);

}</code></pre>

<p>Campos condicionais melhoram a experiência ocultando campos irrelevantes até que se tornem necessários.</p>

<h3>Usando Context API para formulários globais</h3>

<p>Em aplicações maiores, considere usar Context API para evitar prop drilling excessivo.</p>

<pre><code class="language-jsx">const FormContext = createContext();

function FormProvider({ children }) {

const [formData, setFormData] = useState({

firstName: &#039;&#039;,

lastName: &#039;&#039;,

email: &#039;&#039;,

phone: &#039;&#039;,

address: &#039;&#039;,

city: &#039;&#039;,

cardNumber: &#039;&#039;,

expiryDate: &#039;&#039;,

cvv: &#039;&#039;

});

const [currentStep, setCurrentStep] = useState(1);

const [errors, setErrors] = useState({});

const [touched, setTouched] = useState({});

const value = {

formData, setFormData,

currentStep, setCurrentStep,

errors, setErrors,

touched, setTouched

};

return (

&lt;FormContext.Provider value={value}&gt;

{children}

&lt;/FormContext.Provider&gt;

);

}

export function useForm() {

const context = useContext(FormContext);

if (!context) {

throw new Error(&#039;useForm deve ser usado dentro de FormProvider&#039;);

}

return context;</code></pre>

<p>Components filhos podem acessar o contexto sem receber props manualmente:</p>

<pre><code class="language-jsx">function StepOne() {

const { formData, errors, touched, setFormData, setTouched } = useForm();

const handleInputChange = (e) =&gt; {

const { name, value } = e.target;

setFormData(prev =&gt; ({ ...prev, [name]: value }));

};

const handleFieldTouched = (fieldName) =&gt; {

setTouched(prev =&gt; ({ ...prev, [fieldName]: true }));

};

// ... restante do código ...

}</code></pre>

<p>Context API reduz complexidade em formulários de múltiplas etapas distribuídos em vários componentes.</p>

<h2>Conclusão</h2>

<p>Dominar formulários multi-step em React exige compreender três pilares: <strong>gerenciar estado centralizado</strong> que persiste dados entre etapas, <strong>validar parcialmente</strong> em cada transição enquanto oferece feedback em tempo real apenas para campos tocados, e <strong>implementar navegação fluida</strong> que respeita o fluxo sem perder informações. A combinação desses elementos cria uma experiência robusta e profissional. Conforme seus projetos crescem, considere abstrair a lógica em hooks customizados ou usar bibliotecas como Formik ou React Hook Form para reduzir boilerplate e aumentar manutenibilidade.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://react.dev/reference/react-dom/components/input" target="_blank" rel="noopener noreferrer">React Official Documentation - Forms</a></li>

<li><a href="https://react-hook-form.com/" target="_blank" rel="noopener noreferrer">React Hook Form - Building Forms Better</a></li>

<li><a href="https://formik.org/" target="_blank" rel="noopener noreferrer">Formik - Build forms in React, without the tears</a></li>

<li><a href="https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation" target="_blank" rel="noopener noreferrer">MDN Web Docs - Client-side form validation</a></li>

<li><a href="https://css-tricks.com/multi-step-form-using-react-and-styled-components/" target="_blank" rel="noopener noreferrer">CSS-Tricks - Multi-Step Form Tutorial</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Guia Completo de Arquiteturas de Estado em React: Local, Global, Server e URL State
Guia Completo de Arquiteturas de Estado em React: Local, Global, Server e URL State

Introdução: Os Quatro Pilares do Gerenciamento de Estado O gerenciamento de e...

Upload de Arquivos em React: Preview, Progress e Validação de Tipo na Prática
Upload de Arquivos em React: Preview, Progress e Validação de Tipo na Prática

Upload de Arquivos em React: Fundamentos e Arquitetura Upload de arquivos é u...

Implementando um Mini React do Zero: createElement, render e Hooks: Do Básico ao Avançado
Implementando um Mini React do Zero: createElement, render e Hooks: Do Básico ao Avançado

Entendendo a Arquitetura do React React é uma biblioteca que revolucionou a f...