Saltearse al contenido

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 tipo submit triggerea el envío del formulario al igual que lo haría la propiedad action. Cabe destacar que, si no se indica el tipo de botón, este se considera de tipo submit por defecto.
  • Un campo de texto <input> dentro de un formulario triggerea el envío del formulario al presionar la tecla Enter.

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>.

1
function 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.

1
function 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.

1
function 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
<input
17
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 atributo value 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 atributo value se utiliza para establecer el valor del campo de texto.

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.

1
import { useEffect, useReducer } from "react";
2
3
const ACTIONS = {
4
FETCH_INIT: "FETCH_INIT",
5
FETCH_SUCCESS: "FETCH_SUCCESS",
6
FETCH_FAILURE: "FETCH_FAILURE",
7
};
8
9
function 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
32
function 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
60
export 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.

1
import { useEffect, useReducer } from "react";
2
3
const ACTIONS = {
4
FETCH_INIT: "FETCH_INIT",
5
FETCH_SUCCESS: "FETCH_SUCCESS",
6
FETCH_FAILURE: "FETCH_FAILURE",
7
};
8
9
function 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
32
function 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
65
export 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.

1
import { useReducer } from "react";
2
3
const ACTIONS = {
4
FETCH_INIT: "FETCH_INIT",
5
FETCH_SUCCESS: "FETCH_SUCCESS",
6
FETCH_FAILURE: "FETCH_FAILURE",
7
};
8
9
function 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
32
function 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
63
export default useFetch;

Bibliografía