Fundamentos de navegación en la web

1

Navegación Web con React Router: Fundamentos y Prácticas Avanzadas

2

Server-side Rendering vs Single Page Applications: Ventajas y Desventajas

3

Uso de React Router DOM 6 en Proyectos React

Introducción a React Router DOM 6

4

Instalación de React Router DOM 6 en un proyecto React

5

Uso de Hash Router en Aplicaciones Web con React Router DOM

6

Creación de Rutas Dinámicas con React Router DOM 6

7

Navegación en React Router: Uso de Link y NavLink

8

Rutas dinámicas con React Router DOM y useParams

9

Uso de useNavigate en React Router DOM para navegación dinámica

10

Uso de Outlet y Nested Routes en React Router DOM 6

Fake authentication con React Router DOM 6

11

Autenticación y Autorización en Apps con React Router y Hooks

12

Control de Acceso en Menú con Autenticación React

13

Protección de Rutas con React Router y Hooks

14

Roles y permisos en aplicaciones web: Autenticación y autorización

15

Retos avanzados en React: manejo de estado y composición de componentes

16

Mejorando la Redirección Post-Login en Aplicaciones Web

17

Roles y Permisos Avanzados en React Router v6

React Router en TODO Machine

18

Migración de Todo Machine a React Router 6

19

Organización de carpetas y rutas en React con React Router DOM 6

20

Maquetación de Botón Editar en Lista de Tareas con React

21

Generación de IDs únicos para gestionar tareas en React

22

Migración de modales a rutas en React: implementación práctica

23

Editar ToDos en React con Custom Hook y URL Parameters

24

Mejora de la Experiencia del Usuario al Editar To Do's en React

25

Implementación de React Router en Proyectos Legacy

Próximos pasos

26

Filtrado de Búsquedas en URL con React Router

27

Migración de React Router: de la versión 6 a la 5 en proyectos empresariales

28

Clonación de Platzi Movies usando React y React Router

29

Clonación de React Router en Componentes React

30

Navegación Avanzada con React Router DOM 6

No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Mejora de la Experiencia del Usuario al Editar To Do's en React

24/30
Recursos

¿Cómo mejorar la experiencia de usuario al editar un To Do?

Mejorar la experiencia de usuario (UX) es esencial en cualquier aplicación. Al implementar una aplicación de To Do, es fundamental ofrecer una interacción fluida y conveniente. Un ejemplo claro es permitir que los usuarios vean automáticamente el texto de una tarea que desean editar, evitando el esfuerzo de escribirlo de nuevo desde cero. Esto no solo ahorra tiempo, sino que también hace que la experiencia sea más intuitiva y agradable. Vamos a ver cómo podemos lograr esto.

¿Cómo enviar un valor por defecto al TextArea?

Cuando los usuarios hacen clic en "editar", es fundamental que el TextArea muestre el texto del To Do actual. Para ello, debemos enviar un valor por defecto cuando la página de edición se carga. Lo primero será dotar al componente de la opción para recibir un valor predefinido mediante props, que se usará para inicializar el estado del TextArea. Esto se consigue mediante la creación de una variable o función que encuentre el To Do específico por ID y pase el texto correspondiente al componente de edición.

const [defaultToDoText, setDefaultToDoText] = useState("");

useEffect(() => {
  const toDo = getToDoById(id); // Suponiendo que esta función existe
  if (toDo) {
    setDefaultToDoText(toDo.text);
  }
}, [id]);

¿Cómo encontrar el To Do específico por ID?

Para encontrar el To Do que se desea editar, es esencial contar con una función que busque dentro de la lista de tareas completas por ID. Esto puede hacerse utilizando métodos como .find(), que retornan el elemento que coincide con el criterio.

function getToDoById(id) {
  return toDos.find(toDo => toDo.id === id);
}

Con esta función, extraemos el texto del To Do que deseamos editar para pasarlo al TextArea como valor por defecto.

¿Qué desafíos presenta el uso del almacenamiento local?

Uno de los problemas al trabajar con useLocalStorage es la naturaleza asíncrona de las operaciones que simulan una API real. Esto significa que al cargar la página, es posible que el data request no esté completo, lo cual dificulta conseguir el texto del To Do inmediatamente. Para solucionar esto, se puede utilizar un estado de carga (loading) para mostrar un indicador antes de que la data esté lista.

if (loading) {
  return <div>Cargando...</div>;
}

return (
  <TextArea defaultValue={defaultToDoText} />
);

¿Cómo usar React Router para mejorar la navegación y carga de datos?

React Router ofrece la posibilidad de transferir datos entre rutas a través del hook useLocation. Si ya tenemos los datos cargados en una ruta, los podemos pasar a la siguiente para evitar tiempos de carga innecesarios.

import { useLocation, useNavigate } from 'react-router-dom';

const navigate = useNavigate();
const location = useLocation();

function handleEdit() {
  navigate(`/edit/${toDo.id}`, { state: { toDo } });
}

const currentToDo = location.state?.toDo || { text: '' };

Esto nos permite cargar datos eficientemente, reduciendo el tiempo de espera cuando el usuario navega a la página de edición. Si navegamos al editar desde una lista de To Dos ya cargados, pasar el estado como se muestra arriba ahorra tiempo y mejora la UX.

¿Cómo asegurar una implementación eficiente del cambio de estado?

Para lograr una gestión eficiente de estado cuando pasamos datos entre páginas, se debe considerar primero verificar si existe un estado predefinido proveniente de la navegación. Solo si no existe, entonces se carga la data desde origen, como el almacenamiento local.

const [text, setText] = useState('');

useEffect(() => {
  if (!currentToDo.text) {
    fetchToDoText(id);  // Suponiendo que esta función existe y esta bajo condiciones async.
  } else {
    setText(currentToDo.text);
  }
}, [id, currentToDo]);

function fetchToDoText(id) {
  // Lógica para obtener el texto del To Do desde la fuente de datos.
}

Con estos pasos, se logra una aplicación React que no solo es funcional, sino que también proporciona una experiencia de usuario fluida y conveniente. Manteniendo siempre al usuario en mente, podemos mejorar significativamente cómo interactúan con nuestra aplicación. ¡Continúa aprendiendo y perfeccionando tus habilidades en React!

Aportes 17

Preguntas 2

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

Yo he encontrado un error.
.
Resulta que la aplicación cada vez que damos click en los botones de añadir un TODO o editar un TODO, hace uso del custom hook useTodos() y este a su vez hace uso del custom hook useLocalStorage() (y como sabemos este es síncrono, requiere de tiempo).
.
Entonces cada vez que accedemos a las rutas /new o /edit/ debemos esperar que carguen los todos del localStorage nuevamente porque los necesitamos para las funciones addTodo() y editTodo() y aquí es donde está el problema.
.
Si modificamos el setTimeout() de useLocalStorage() a 5 segundos por ejemplo y luego accedemos a /new o /edit y agregamos o editamos información antes de que pasen 5 segundos habrá problemas.

  • En el caso de addTodo() se sobrescribirá la información, porque la constante newTodos va a ser igual a un arreglo vacío.
  • En el caso de editTodo() se romperá la aplicación, porque la constante todoIndex va a ser -1.

.
Para solucionar esto lo que hice fue deshabilitar el <textarea> y el botón de Añadir/Editar usando el state loading de useTodos()

  • En newTodoPage() y EditTodoPage() agregue lo siguiente:
//use el loading y se lo pase al <TodoForm />
const { editTodo, getTodo, loading } = useTodos()

return (
	<TodoForm
		loading={loading}
		...
		...
)

  • En TodoForm() agregue lo siguiente:
function TodoForm(props) {
	...
	...

	return (
		...
		<textarea 
			disabled={props.loading}
			...
		/>
		<button
			disabled={props.loading}
		>
			{props.submitText}
		</button>
	)
}

Previamente, había publicado un código que tenía dos errores:

  • al entrar directamente a una url, state (location) no existía y tiraba un error.
  • si el id del to-do no existía, saltaba un error

Ya corregí esos errores, dejo el nuevo código abajo.

App.js

<Route path="/edit/:id" element={<EditTodoPage />} />

EditTodoPage.js

export function EditTodoPage() {
    const { editToDo, loading, getToDo } = useToDos()
    const { state } = useLocation()
    let { id } = useParams()
    id = Number(id) //useParams converts number to text. Just in case, it is converted to number again

    let toDoText

    if (!state) {
        if (loading) return <p>loading</p>
        let toDo = getToDo(id)
        if (!toDo) return <NotFound />
        toDoText = toDo.text
    } else ({ toDoText } = state)

    return (
        <TodoForm
            submitEvent={newText => editToDo({ id, newText })}
            title="Edit To-Do"
            submitText="Edit"
            defaultText={toDoText}
        />
    )
}

Esta sería mi respuesta al retor, cualquier sugerencia estaré pendiente:

React router v6

React router v5

Para resolver el reto del minuto 13:22, leí la documentación del hook useNavigate() de React Router, y noté que la función navigate() no sólo recibe una ruta, sino también un objeto de opciones:

Y entre las opciones recibe la propiedad state, entonces desde el HomePage, agregué el objeto de opciones con la propiedad state a la función navigate(), asignándole como valor al state el texto del TODO de cada TodoItem que está siendo mapeado por el TodoList.

Luego en la página EditTodoPage, por medio del hook useLocation(), le asigné el valor del state a la propiedad defaultTodoText del componente TodoForm.

Y listo, de esta forma podemos enviar directamente el texto de cada TODO al formulario TodoForm sin necesidad de esperar los segundos para que carge el localStorage.

Toca ver varias veces esta clase🧐🤪

Pensé que iba a poder con este reto, pero una vez más encontré varias formas de Como no hacerlo…XD.
Aprendí que useLocation es inmutable.

useLocation: transferencia de datos por navegación

.
Para concluir la migración de portales y modales a rutas, vamos a recuperar el texto del todo que estamos editando, para no tener que volverlo a escribir si fuera un pequeño cambio.
.
En el componente HomePage vamos a modificar nuestra render prop onEdit. En la navegación vamos a necesitar pasar la información del TODO que estamos modificando por medio de options, el cual vendría a ser un objeto que permite una propiedad state que nos sirve para mandar datos hacia una ruta, en este caso la ruta /edit/:id.
.

onEdit={() => {
              navigate(
                '/edit/' + todo.id,
                {
                  state: { todo }
                },
              );
            }}

.
En el componente EditTodoPage vamos a utilizar el react hook useLocation para recibir la información del TODO que se nos pasó por navegación en state.
.
Dentro de la ruta de editar TODOs tenemos 2 posibilidades para llegar a la ruta /edit/:id; es decir, por medio del botón de editar TODO o de forma directa a través de la url.
.
Sucede que al ingresar por la url, internamente se deben de cargar los TODOs antes de poder realizar cualquier acción. Este proceso se muestra dentro el código del react hook useLocalStorage, donde estamos simulado que estamos realizando peticiones a una API por medio de un setTimeout para obtener los TODOs.
.
Por esta razón, para evitar problemas de sobreescritura o similares, vamos a manejar el estado de loading que nos proporciona useTodos. Esto nos permite saber cuando terminamos de cargar los TODOs, por lo que tendremos una variable todoText que va a cambiar según la situación:
.

  • Si accedemos por medio del botón de editar algún TODO, significa que tendremos la información del mismo en location y podremos tomar el texto de location.state.todo.text.
    .
  • Si no tenemos la información en location, significa que estamos accediendo por url y que los TODOs aún deben cargar. Por lo que si loading es verdadero entonces renderizamos un mensaje de carga.
    .
  • Si loading es falso significa que los TODOs terminaron de cargar, por lo que podemos buscar el TODO por su ID por medio de la función getTodo, y luego recuperar su texto.

.
Cualquiera que sea el modo en que obtengamos el texto del TODO, vamos a pasarlo como prop defaultTodoText a FormTodo.
.

import React from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { TodoForm } from '../../ui/TodoForm';
import { useTodos } from '../useTodos';

function EditTodoPage() {
  const location = useLocation();
  const params = useParams();
  const id = Number(params.id);
  
  const { state, stateUpdaters } = useTodos();
  const { loading, getTodo } = state;
  const { editTodo } = stateUpdaters;

  let todoText;
  
  if (location.state?.todo) {
    todoText = location.state.todo.text;
  } else if (loading) {
    return <p>Cargando...</p>
  } else {
    const todo = getTodo(id);
    todoText = todo.text;
  }

  return (
    <TodoForm
      label="Edita tu TODO"
      defaultTodoText={todoText}
      submitText="Editar"
      submitEvent={(newText) => editTodo(id, newText)}
    />
  );
}

export { EditTodoPage };

.
En el react hook useTodos como se mencionó anteriormente se tiene a una función getTodo que recibe un id y busca el TODO entre todos los TODOs y lo retorna. A su vez, esta función se retorna dentro del objeto state.
.

...

function useTodos() {
	...
  const getTodo = (id) => {
    const todoIndex = todos.findIndex(todo => todo.id === id);
    return todos[todoIndex];
  }
  ...
  
  const state = {
	  ...
    getTodo,
  };
  ...

  return { state, stateUpdaters };
}
...

export { useTodos };

.
Finalmente, en TodoForm el estado incial de newTodoValue será props.defaultTodoText o un string vacío.
.

const [newTodoValue, setNewTodoValue] = React.useState(props.defaultTodoText || '');

Alguien más pensó que no tiene sentido pasar la información por el location.state si de igual forma hay que esperar a que se carguen los TODOs del local storage antes de poder darle click al botón de editar/añadir ?

Por ejemplo, si se demora 10 seg en cargar los TODOs y hacemos click en editar antes de que termine la carga, la aplicación se rompe, esto lo solucioné deshabilitando el botón de editar hasta que terminara la carga… pero esa solución no me convence ya que no hay ninguna ventaja en pasar los datos por el location.state

“si, cambio un poquito el navegador” jajajajaj

Mi solución

Mi solución fue pasar en el NavigateOptions.state el texto del TODO cuando se navega a la ruta ‘/edit/:id’ :

        {todo => (
          <TodoItem
            key={todo.id}
            text={todo.text}
            completed={todo.completed}
            onComplete={() => completeTodo(todo.id)}
            onDelete={() => deleteTodo(todo.id)}
            onEdit={() => navigate('/edit/' + todo.id, {state: {text: todo.text} })}
          />
        )}

Luego en EditTodoPage se accede a ese objeto state mediante useLocation():

function EditTodoPage(){

   const { stateUpdaters } = useTodos();
   const { editTodo } = stateUpdaters;
   const params  = useParams();
   const id = Number(params.id);
   const location = useLocation();

    return(
        <TodoForm 
            label="Edita tu Todo"
            submitText='Actualizar'
            submitEvent={(newText) => editTodo(id, newText)}
            initialText={location.state?.text}
        />
    );
}

Juan dice que esta clase fue díficil…
Para mí lo fue, pero por ese bendito loading que me hizo estar debuggeando un buen rato! 😵

Me estaba sucediendo un error, muestro screenshot:

.
Y sucedía porque me salía del NewPage hacia el HomePage sin que terminarán de cargar los todos, así que, cuando ya habían cargado y el estado loading cambia a true, el componente TodoForm ya está desmontado, y ocasiona el error.
.
La solución a esto es DETENER la carga de los todos al salirnos del NewPage (y EditPage también). Cómo hacemos eso?
.
En efecto, como nos dice el error, debemos cancelar esa carga de todos desde una Cleanup Function en nuestro useEffect que genera el setTimeout dentro de useLocalStorage. Esto es lo equivalente a un willUnmount del antiguo React.

React.useEffect(() => {
		const timeout = setTimeout(() => {
			try {
				const localStorageItem = localStorage.getItem(itemName);
				let parsedItem;

				if (!localStorageItem) {
					localStorage.setItem(itemName, JSON.stringify(initialValue));
					parsedItem = initialValue;
				} else {
					parsedItem = JSON.parse(localStorageItem);
				}

				onSuccess(parsedItem);
			} catch (error) {
				onError(error);
			}
		}, 3000);

		//CLEANUP FUNCTION
		return () => {
			clearInterval(timeout);
		};
	}, [sincronizedItem]);

Yo me propuse lograr la funcionalidad antes de ver el video de esta clase, les comparto mi solución:

  • Primero edité la propiedad path de la etiqueta Route que nos lleva al componente EditTodoPage para que aceptara un parámetro más:
  • Luego le agregé a la función navigate de onEdit el parámetro todo.text
  • Y por último definí ese texto como valor por defecto de newTodoValue en el componente TodoForm 😃

    Y listo 😃
Hello warriors! Dejo mi **branch/commit** en inglés para enriquecer esas notas: \- \[English version branch]\(https://github.com/SebaMat3/react-todo/tree/feat/improve-todo-edit-ux) **Branch name:** feat/improve-todo-edit-ux **Commit message:** feat: enhance UX for editing todos with preloaded text and loading improvements \- Implement useLocation to pass todo state from HomePage to EditTodoPage \- Preload todo text in EditTodoPage for better user experience \- Add loading state handling in EditTodoPage \- Update useTodos hook to support ID-based todo operations \- Improve overall navigation and state management for todos
Para tener una planeación del curso, parece que no se tenia un approach para resolver ciertos problemas de la app desde el principio
**Mi solucion:** **<u>HomePage.js</u>** ![](https://static.platzi.com/media/user_upload/image-fcf32558-9113-4565-b2b8-54f96bba8393.jpg) ![]()**<u>EditTodoPage.js</u>** ![](https://static.platzi.com/media/user_upload/image-7d5f49df-bd8c-4c05-a022-6a2fc574b735.jpg)![]()