Reto: loading skeletons
Clase 20 de 23 • Curso de React 17
Contenido del curso
Clase 20 de 23 • Curso de React 17
Contenido del curso
Muchas aplicaciones se quedan en blanco mientras cargan su contenido. Otras muestran algún mensaje de "cargando" y luego sí renderizan todo el contenido de la aplicación.
Pero para ofrecerle una mejor experiencia a nuestros usuarios (UX) es mejor renderizar todo el contenido posible, incluso si no ha terminado de cargar alguna parte de la aplicación. En TODO Machine, por ejemplo, podemos mostrar varios componentes desde el principio aunque no hayamos terminado de cargar la lista de TODOs.
Pero un párrafo que diga "cargando" definitivamente NO es la mejor forma de comunicarle a los usuarios que estamos cargando (el mensaje es claro, pero podríamos buscar una solución más... estética).
Existen muchas posibles soluciones, desde animaciones sencillas (como 3 puntitos intercalando diferentes niveles de opacidad) hasta loading skeletons (esqueletos de carga). Incluso existen herramientas interactivas como Create React Content Loader para agilizar estos desarrollos.
El reto de esta clase es que maquetes cualquiera de estas soluciones y nos la muestres en la sección de comentarios. En esta lectura te mostraré mi solución, pero te recomiendo que no la veas hasta intentar tu propia solución.
:bulb: Te recomiendo esta lectura: 5 estados clave para crear interfaces coherentes
Tu propio loading skeleton en React
Lo primero que vamos a hacer es crear 3 nuevos componentes para trabajarlos independientemente: TodosError, TodosLoading y EmptyTodos.
Ya que tenemos estos 3 componentes, ahora vamos a llamarlos desde el componente AppUI para conectarlos con la aplicación.
¡Muy bien! Ahora sí podemos concentrarnos mucho mejor para trabajar el estado de carga de TODOS dentro del componente TodosLoading.
Para empezar, voy a crear y conectar un archivo TodosLoading.css para definir los estilos de mi esqueleto:
Llegó el momento más importante: maquetar.
Debemos definir qué elementos necesitamos para el esqueleto y luego les daremos estilos con CSS. Como la idea es replicar la estructura de un TODO, vamos a necesitar una cajita para el contenedor del TODO, una cajita para el ícono de completar, otra cajita para el ícono de borrar y una última cajita para el texto.
:bulb: Le digo a cada elemento "cajita" porque el objetivo de los esqueletos de carga no es replicar todos estos elementos, sino que cada uno será un elemento "fantasma". Deben ser lo suficientemente parecidos a los TODOs reales para que los usuarios entiendan que estos elementos están relacionados con los TODOs, pero lo suficientemente abstractos y grisáceos para que se entienda que aún los estamos cargando.
¡Y ahora debemos crear los estilos!
Primero vamos a definir los tamaños y posiciones de cada elemento (tal cual copiando y pegando los estilos del TodoItem.css, pero cambiando los nombres de las clases y descartando las propiedades innecesarias):
Ahora vamos a todas las cajitas (menos la del texto) para darles un color de fondo con gradiente:
Luego le configuraremos un tamaño de fondo lo suficientemente grande como para que pueda darla vuelta sin dejar espacios vacíos (400% es más que suficiente):
Y finalmente le daremos una animación que cambie la posición del fondo al principio, a la mitad y al final (te recomiendo darle al menos 3 segundos de duración para que tu animación no se vea atropellada, sino por el contrario con un efecto suave e hipnotizante):
:bulb: Puedes ir a
useLocalStorage.jsy cambiar el tiempo que tardamos en llamar a nuestro efecto con la función setTimeout para poder visualizar tu esqueleto de carga correctamente.
Recuerda que puedes tomar esta trilogía de cursos para aprender muchísimo más sobre transformaciones, transiciones y animaciones con CSS:
Curso de Transformaciones y Transiciones con CSS Curso de Animaciones con CSS Curso Práctico de Maquetación y Animaciones con CSS
¡Te espero en la siguiente clase para un nuevo reto!
👉 Aquí puedes encontrar el repositorio de este reto: Clase bonus: loading skeleton
Obed Paz
Luis Alejandro Vera Hernandez
Cristian David Contreras López
Juan Sebastián Poveda Florez
Mauricio Gonzalez Falcon
Marco Antonio Alducin Garcia
Cristian David Rojas Carvajal
Juan Castro
Francisco Israel Jimenez Ramirez
Rodrigo Milesi
Nazareno Aznar Altamiranda
Gustavo Gonzalez Montero
Gustavo Gonzalez Montero
Angel Choque
Stiven Trujillo
Mauricio Chávez
John Orellana
Lean Silva
Gustavo A. González G.
Juan Castro
Alejandro Forero Vanegas
Juan Fernando Yepes Muñoz
Raul Wabe
Alan Dromundo Arias
Kevin Andres García Velez
Lean Silva
Luis Felipe Silgado Cortázar
Javier Alejandro Albornoz Pérez
Juan Castro
Samuel Montoya Gallo
Paula Inés Cudicio
CHRISTIAN OLIVER SOLANO NUÑEZ
Juan Castro
Miguel Angel Armenta Acosta
David Rodriguez
ZANONI ALFREDO SALAS TOBÓN
Loading and Empty state screens: .
. Error State Screen: .
Me gusta mucho! :D
Bro, wow, te mamaste te quedo genial.
Opte por los clásicos punticos de carga
Tu diseño de todo menos clásico, Te quedo genial
Me gusta mucho tu animacion.
Hola 😀
Para que nos muestre varios componentes TodosLoading podemos hacer esto
{loading && new Array(5).fill(1).map((a, i) => <TodosLoading key={i} />)}
resultado
Fino
hola, gracias por el aporte!!, justo me estaba preguntando como colocar varios sin tener que repetir la maquetación hardcodeada por así decirlo haha !
{loading && new Array(4).fill().map((item, index)=>( <LoadingTodo key={index} /> ))}
const LoadingTodo = () => { return ( <li className="TodoItem-loading"> <div className="LoaderBalls"> <span className="LoaderBalls__item"></span> <span className="LoaderBalls__item"></span> <span className="LoaderBalls__item"></span> </div> </li> ) }
.TodoItem-loading { background-color: #FAFAFA; position: relative; display: flex; justify-content: center; align-items: center; margin-top: 24px; box-shadow: 0px 5px 50px rgba(32, 35, 41, 0.15); color: #333; min-height: 4.5rem; } .LoaderBalls { width: 90px; display: flex; justify-content: space-between; align-items: center; } .LoaderBalls__item { width: 20px; height: 20px; border-radius: 50%; background: #bec3c5; } .LoaderBalls__item:nth-child(2) { animation: opacitychange 1s ease-in-out infinite; } .LoaderBalls__item:nth-child(3) { animation: opacitychange 1s ease-in-out 0.33s infinite; } .LoaderBalls__item:nth-child(1) { animation: opacitychange 1s ease-in-out 0.66s infinite } @keyframes opacitychange { 0%, 100% { opacity: 0; } 60% { opacity: 1; } }
Genial la solucion para repetir los todos, no lograba dar con eso ¡Gracias!
Loading state:
Ideal state:
Error state
Empty state:
All completed state:
TodoForm
Loading code:
//TodoLoading.js import React from "react" import ContentLoader from "react-content-loader" import './TodoLoad.css' const TodoLoad = (props) => ( <ContentLoader speed={2} width={280} height={40} viewBox="0 0 280 40" backgroundColor="#3d087b" foregroundColor="#f43bea" {...props} > <rect x="48" y="8" rx="3" ry="3" width="88" height="6" /> <rect x="48" y="26" rx="3" ry="3" width="52" height="6" /> <circle cx="20" cy="20" r="20" /> </ContentLoader> ) export { TodoLoad } //AppUI.js: ... {loading && ( <> <TodoLoad/> <TodoLoad/> <TodoLoad/> </> )}
Error code:
//TodoError.js import React from "react"; import './TodoError.css' const TodoError = () => { return ( <div className="TodoError"> <i className="fa fa-grav" aria-hidden="true"></i> <p>Vaya, parece que encontramos un problema, danos un poco de tiempo y vuelve a intenar en unos par de minutos...</p> </div> ) } export { TodoError } AppUI.js ... {error && ( <Modal> <TodoError /> </Modal> )}
Empty code:
//EmptyTodos.js import React from "react"; import './EmptyTodos.css' const EmptyTodos = () => { return ( <div className="EmptyTodos"> <div className="EmptyTodos-icons"> <i className="fa fa-sun-o" aria-hidden="true"></i> <i className="fa fa-smile-o" aria-hidden="true"></i> </div> <p>¡Crea tu primer TODO!</p> </div> ) } export { EmptyTodos } //AppUI.js ... {(!loading && !searchedTodos.length) && ( <EmptyTodos /> )}
All completed state:
//AllCompleted.js import React from 'react' import './AllCompleted.css' const AllCompleted = () => { return ( <div className="AllCompleted"> <i className="fa fa-coffee" aria-hidden="true"></i> <p>Has completado todos tus TODOs!</p> <p>Es hora de un merecido descanzo</p> </div> ) } export { AllCompleted } //AppUI.js ... {(allCompleted && !!searchedTodos.length) && ( <Modal> <AllCompleted /> </Modal> )}
La variable AllCompleted es un estado que se actualiza al momento de cargar los todos en el custom hook de useLocalStorage, y también se actualiza cada vez que los todos sufren un cambio.
Este es mi custom hook con las mejoras de la variable allCompleted y con el useEffect modificado para que solo se ejecute cuando se carga inicialmente
//useLocalStorage.js import React from "react"; function useLocalStorage(itemName, initialValue) { const [error, setError] = React.useState(false) const [loading, setLoading] = React.useState(true) const [item, setItem] = React.useState(initialValue) const [allCompleted, setAllCompleted] = React.useState(false) const checkAllCompleted = (todos) => { if (Array.isArray(todos)) { const completedTodos = todos.filter(todo => !!todo.completed).length const totalTodos = todos.length if (totalTodos > 0 && (completedTodos === totalTodos)) { setAllCompleted(true) } else { setAllCompleted(false) } } } React.useEffect(() => { setTimeout(() => { try { const localStorageItem = localStorage.getItem(itemName) let parsedItems; if (!localStorageItem) { localStorage.setItem(itemName, JSON.stringify(initialValue)) parsedItems = initialValue } else { parsedItems = JSON.parse(localStorageItem) } setItem(parsedItems) checkAllCompleted(parsedItems) setLoading(false) } catch (error) { setError(error) } }, 1500); },[initialValue, itemName]) const saveItem = (newItem) => { try { localStorage.setItem(itemName, JSON.stringify(newItem)) setItem(newItem) checkAllCompleted(newItem) } catch (error) { setError(error) } } return { item, saveItem, loading, error, allCompleted } } export { useLocalStorage }
Otras mejoras que hice fueron la implementación del hook useRef para que cuando la app cargue el foco esté en el input de búsqueda. Lo mismo cuando se carga el componente de TodoForm para que se ubique automáticamente en el textarea
TodoSearch
//TodoSearch/index.js ... const refInput = React.useRef() React.useEffect(() => { refInput.current.focus() },[]) return ( <section className="TodoSearch"> <input ref={refInput} value={searchValue} onChange={onSearchValueChange} placeholder="Cebolla" /> <i className="fa fa-search" aria-hidden="true"></i> </section> ) ...
TodoForm
En TodoForm también puse una validación para un máximo de 40 caracteres por Todo
const textareaRef = React.useRef() React.useEffect(() => { textareaRef.current.focus() }, []) const onChange = (event) => { setNewTodoValue((prevState) => { let eventValue = '' if (event.target.value.length > 40) { eventValue = prevState } else { eventValue = event.target.value } return eventValue }) } const onCancel = () => { setOpenModal(false) } const onSubmit = (event) => { event.preventDefault() addTodo(newTodoValue) setOpenModal(false) } return ( <form className="TodoForm" onSubmit={onSubmit} > <label>Escribe tu nuevo <span>TODO</span></label> <textarea ref={textareaRef} value={newTodoValue} onChange={onChange} placeholder="Cortar la cebolla para el almuerzo" /> <p>{newTodoValue.length}/40</p> ... </form> ) ...
Transparent Scroll
En el TodoList puse una validación para que el alto no supere cierto máximo y así poder hacer scroll sin que el Botón se desplace demasiado y salga del viewport. Y ya que iba a aparecer un scroll le puse estilos para que luzca más estético:
.TodoList { position: relative; max-height: 450px; margin: 15px 0; padding: 5px 5px; background: var(--background05); border-radius: 10px; overflow-y: scroll; } .TodoList-ul { display: flex; flex-direction: column; width: 280px; } /* * ============================== */ /* * ========== WEBKIT ============ */ /* * ============================== */ ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 25px; } ::-webkit-scrollbar { width: 10px; background: rgba(255, 255, 255, 0.1); border-radius: 25px; } ::-webkit-scrollbar:hover { background: rgba(255, 255, 255, 0.2); box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.2); } ::-webkit-scrollbar:hover:active { background: rgba(255, 255, 255, 0.25); }
Usé este video para entender como usar el useRef: https://egghead.io/lessons/react-focus-an-input-field-in-react-with-the-useref-and-useeffect-hooks
En la clase de deploy con GitHub paso el link del repo ;-)
yo solo lo puse una animación de carga
Uy este esta muy interesante y bonito
Que chimba!
Hola devs !! Puede que haya un error en el Modal en el aspecto de sí al momento de agregar todos, nosotros no escribamos la tarea y le demos al botón de agregar , se agrega una tarea vacía, para evitar esto coloque el atributo required en textarea y así no se va una tarea vacía , haciendo que el usuario no ignore llenar este campo!!
Podría ser contraproducente, ya que editando el html directamente desde el navegador el usuario podría crear y guardar un "ToDo" vacio y guardarlo en LocalStorage, lo mejor sería hacer las respectivas validaciones con operadores.
Qué tal?
10/10
Muy buena la UI!
Pantalla de carga
La hice con la herramienta que nos dejaron en la lectura, bastante sencillo hacerlo con esa herramienta la verdad
Usuario nuevo / o sin TO-DOs
Por si hay un error ya saben donde ir
Vamos avanzando, está muy bueno el proyecto para llevar varios conceptos
Aporte en acción:
Pantalla de empty todos
Pantalla de error todos
Pantalla de loading skeletons
Loading State:
EmptyToDos:
TheProjectWithTodos:
Que elegante! Usaste solo css?
Usando React Content Loader, aunque debo decir que no me siento cómodo usando medidas absolutas. En un futuro veré cómo hacerlo con medidas relativas.
import React from 'react' import ContentLoader from 'react-content-loader' export function TodosLoading(props) { return ( <ContentLoader speed={2} width={272} height={102} viewBox="0 0 272 104" backgroundColor="#d9d9d9" foregroundColor="#ecebeb" {...props} > {/* Línea Izquierda */} <rect x="0" y="16" rx="0" ry="0" width="2" height="102" /> {/* Línea derecha */} <rect x="270" y="16" rx="0" ry="0" width="2" height="102" /> {/* Línea superior */} <rect x="0" y="16" rx="0" ry="0" width="272" height="2" /> {/* Línea inferior */} <rect x="0" y="102" rx="0" ry="0" width="272" height="2" /> {/* Check */} <rect x="24" y="48" rx="0" ry="0" width="24" height="24" /> {/* Texto */} <rect x="72" y="28" rx="0" ry="0" width="176" height="24" /> {/* Texto */} <rect x="72" y="68" rx="0" ry="0" width="176" height="24" /> </ContentLoader> ) }
Seria bueno usar los React-Icons?
Yeah, está perfecto :)
Aquí mi propuesta de LOADING...
Tiene una pequeña animación que no supe como cargarla
decidí crear algo mas expresivo EmptyTodos
TodosErrot
TodoLoading
Love it
Mi Todo List
La verdad no hice muchos cambios al css :C