Formularios y peticiones HTTP
Una parte fundamental de cualquier aplicación web es la interacción con el usuario. En este sentido, los formularios juegan un papel importante, ya que permiten a los usuarios enviar información a un servidor.
De igual manera, debemos abordar como se manejarán las peticiónes HTTP mediante diferentes métodos, como GET, POST, PUT, DELETE, entre otros.
Formularios
En materias anteriores, se abordó el uso de formularios en HTML. Sin embargo, como sabemos, React establece ciertas convenciones y prácticas recomendadas cuando trabajamos con jsx, lo cual incluye el manejo de formularios.
En React, los formularios se comportan de manera diferente a como lo hacen en HTML. En lugar de que el DOM maneje el estado de los elementos de formulario, React puede manejar estados para cada campo de formulario o, como veremos más adelante, mediante referencias.
Implementación de formularios
En HTML, el envío de un formulario se realiza mediante la propiedad action
de un elemento <form>
.
1<form action="/submit" method="post">2 <input type="text" name="username" />3 <button type="submit">Enviar</button>4</form>
Además, el uso de formularios ofrece ciertos mecanismos de accesibilidad:
- Un elemento
<button>
dentro de un formulario de tiposubmit
triggerea el envío del formulario al igual que lo haría la propiedadaction
. Cabe destacar que, si no se indica el tipo de botón, este se considera de tiposubmit
por defecto. - Un campo de texto
<input>
dentro de un formulario triggerea el envío del formulario al presionar la teclaEnter
.
En React, el envío de formularios se maneja de manera diferente. En lugar de enviar el formulario mediante la propiedad action
, se puede manejar el envío de formularios mediante el evento onSubmit
del elemento <form>
.
1function MyForm() {2 function handleSubmit(event) {3 console.log("Formulario enviado");4 }5
6 return (7 <form onSubmit={handleSubmit}>8 <input type="text" name="username" />9 <button type="submit">Enviar</button>10 </form>11 );12}
Otro aspecto a tener en cuenta es que, los formularios tienen un comportamiento por defecto que recarga la página al enviar el formulario. Si este comportamiento no es deseado, se puede prevenir mediante el método preventDefault
del evento.
1function MyForm() {2 function handleSubmit(event) {3 event.preventDefault();4 console.log("Formulario enviado");5 }6
7 return (8 <form onSubmit={handleSubmit}>9 <input type="text" name="username" />10 <button type="submit">Enviar</button>11 </form>12 );13}
Estado de los formularios
En React, los formularios pueden manejar su estado mediante el uso de hooks. Para ello, se puede utilizar el hook useState
para definir el estado de los campos de formulario.
1function MyForm() {2 const [username, setUsername] = useState("");3
4 function handleSubmit(event) {5 event.preventDefault();6 console.log(username);7 }8
9 function handleChange(event) {10 setUsername(event.target.value);11 }12
13 return (14 <form onSubmit={handleSubmit}>15 <label for="username">Nombre de usuario:</label>16 <input17 type="text"18 id="username"19 name="username"20 value={username}21 onChange={handleChange} />22 <button type="submit">Enviar</button>23 </form>24 );25}
Si optamos por esta forma de manejar el estado de los formularios, será necesario definir un manejador de eventos para cada campo de formulario. Este manejador de eventos se encargará de actualizar el estado del campo de formulario cada vez que se modifique su valor. Como podemos observar en el ejemplo anterior, el manejador de eventos handleChange
se encarga de actualizar el estado del campo de texto username
mediante el evento onChange
.
Como consideraciones adicionales, es importante tener en cuenta que:
- El atributo
value
del campo de formulario se utiliza para establecer el valor del campo de formulario. - El atributo
onChange
del campo de formulario se utiliza para establecer el manejador de eventos que se ejecutará cada vez que el valor del campo de formulario cambie. - El atributo
htmlFor
del elemento<label>
se utiliza para asociar el texto del label con el campo de formulario. - El atributo
id
del campo de formulario se utiliza para asociar el campo de formulario con el label. - Podemos establecer un valor por defecto para un campo de formulario mediante el atributo
defaultValue
. - Los elementos
<select>
y<textarea>
también pueden manejar su estado de la misma manera que los campos de texto.- Para los elementos
<select>
, el atributovalue
se utiliza para establecer el valor seleccionado. Por supuesto, este valor debe coincidir con el valor de uno de los elementos<option>
asociados. - Para los elementos
<textarea>
, el atributovalue
se utiliza para establecer el valor del campo de texto.
- Para los elementos
Envío de formularios
Una vez que se ha definido el estado de los campos de formulario y se ha establecido el manejador de eventos para el envío del formulario, es posible enviar el formulario mediante una petición HTTP.
Actualmente, existen diferentes formas de realizar una petición HTTP, mediante la función fetch
, la librería axios
, incluso existen hooks dentro de React que estan diseñados específicamente para esto, como useFormStatus
de react-dom
. No obstante, en esta materia hemos enfatizado el análisis de todos estos mecanismos, por lo cual buscaremos realizar una implementación propia.
Con este fin, volveremos a utilizar el hook personalizado useFetch
que hemos estado desarrollando recientemente.
1import { useEffect, useReducer } from "react";2
3const ACTIONS = {4 FETCH_INIT: "FETCH_INIT",5 FETCH_SUCCESS: "FETCH_SUCCESS",6 FETCH_FAILURE: "FETCH_FAILURE",7};8
9function reducer(state, action) {10 switch (action.type) {11 case ACTIONS.FETCH_INIT:12 return {13 isError: false,14 isLoading: true,15 };16 case ACTIONS.FETCH_SUCCESS:17 return {18 data: action.payload.data,19 isError: false,20 isLoading: false,21 };22 case ACTIONS.FETCH_FAILURE:23 return {24 isError: true,25 isLoading: false,26 };27 default:28 return state;29 }30}31
32function useFetch(url, options = {}) {33 const [state, dispatch] = useReducer(reducer, {34 data: null,35 isError: false,36 isLoading: true,37 });38
39 useEffect(() => {40 dispatch({ type: ACTIONS.FETCH_INIT });41
42 fetch(url, { ...options })43 .then((response) => {44 if (response.ok) {45 return response.json();46 }47 throw Error("Error al relizar la petición");48 })49 .then((data) => {50 dispatch({ type: ACTIONS.FETCH_SUCCESS, payload: { data } });51 })52 .catch((e) => {53 dispatch({ type: ACTIONS.FETCH_FAILURE });54 });55 }, [url]);56
57 return state;58}59
60export default useFetch;
Hasta el momento, hemos empleado el hook useFetch
únicamente para realizar peticiones GET. Sin embargo, es posible extender su funcionalidad para realizar peticiones POST, PUT, DELETE, entre otras.
Adicionalmente, es importante tener en cuenta que, al utilizar el hook useFetch
este ejecuta la petición HTTP cada vez que el componente que lo contiene se monta o actualiza. Este comportamiento puede tener un impacto negativo en el rendimiento de la aplicación y, particularmente en el caso de los formularios, puede resultar en un envío de formulario no deseado.
Con esto en mente, buscaremos modificar el hook useFetch
para que la petición HTTP se realice condicionalmente. Para ello, analizaremos diferentes estrategias:
Emplear un trigger
En esta estrategia, se empleará un estado adicional para controlar si se debe realizar la petición HTTP. No obstante, el estado debe ser proporcionado por el componente que contiene el formulario mediante un prop.
1import { useEffect, useReducer } from "react";2
3const ACTIONS = {4 FETCH_INIT: "FETCH_INIT",5 FETCH_SUCCESS: "FETCH_SUCCESS",6 FETCH_FAILURE: "FETCH_FAILURE",7};8
9function reducer(state, action) {10 switch (action.type) {11 case ACTIONS.FETCH_INIT:12 return {13 isError: false,14 isLoading: true,15 };16 case ACTIONS.FETCH_SUCCESS:17 return {18 data: action.payload.data,19 isError: false,20 isLoading: false,21 };22 case ACTIONS.FETCH_FAILURE:23 return {24 isError: true,25 isLoading: false,26 };27 default:28 return state;29 }30}31
32function useFetch(url, options = {}, trigger = true) {33 const [state, dispatch] = useReducer(reducer, {34 data: null,35 isError: false,36 isLoading: true,37 });38
39 useEffect(() => {40 if (trigger) {41 dispatch({ type: ACTIONS.FETCH_INIT });42
43 fetch(url, { ...options })44 .then((response) => {45 if (response.ok) {46 return response.json();47 }48 throw Error("Error al relizar la petición");49 })50 .then((data) => {51 dispatch({52 type: ACTIONS.FETCH_SUCCESS,53 payload: { data },54 });55 })56 .catch((e) => {57 dispatch({ type: ACTIONS.FETCH_FAILURE });58 });59 }60 }, [url, trigger]);61
62 return state;63}64
65export default useFetch;
Mediante una sentencia condicional, se evalúa si el trigger es verdadero. En caso afirmativo, se ejecuta la petición HTTP. De lo contrario, la petición no se realiza.
Emplear callbacks
En esta estrategia, se empleará un callback para realizar la petición HTTP.
Por este motivo, el hook useFetch
delegará la responsabilidad de iniciar la petición al componente que contiene el formulario.
Para esto, se modificará el hook useFetch
para que retorne una función que permita realizar la petición HTTP, además del estado de la petición.
1import { useReducer } from "react";2
3const ACTIONS = {4 FETCH_INIT: "FETCH_INIT",5 FETCH_SUCCESS: "FETCH_SUCCESS",6 FETCH_FAILURE: "FETCH_FAILURE",7};8
9function reducer(state, action) {10 switch (action.type) {11 case ACTIONS.FETCH_INIT:12 return {13 isError: false,14 isLoading: true,15 };16 case ACTIONS.FETCH_SUCCESS:17 return {18 data: action.payload.data,19 isError: false,20 isLoading: false,21 };22 case ACTIONS.FETCH_FAILURE:23 return {24 isError: true,25 isLoading: false,26 };27 default:28 return state;29 }30}31
32function useFetch(url, options = {}) {33 const [state, dispatch] = useReducer(reducer, {34 data: null,35 isError: false,36 isLoading: true,37 });38
39 const doFetch = (newOptions) => {40 dispatch({ type: ACTIONS.FETCH_INIT });41
42 fetch(url, { ...options, ...newOptions })43 .then((response) => {44 if (response.ok) {45 return response.json();46 }47 throw Error("Error al relizar la petición");48 })49 .then((data) => {50 dispatch({51 type: ACTIONS.FETCH_SUCCESS,52 payload: { data },53 });54 })55 .catch((e) => {56 dispatch({ type: ACTIONS.FETCH_FAILURE });57 });58 };59
60 return [state, doFetch];61}62
63export default useFetch;