Aprende todo un fin de semana sin pagar una suscripción 🔥

Aprende todo un fin de semana sin pagar una suscripción 🔥

Regístrate

Comienza en:

3D
4H
9M
14S
Curso de Introducción a React.js

Curso de Introducción a React.js

Juan David Castro Gallego

Juan David Castro Gallego

Reto: loading skeletons

20/23

Lectura

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.

...

Regístrate o inicia sesión para leer el resto del contenido.

Aportes 47

Preguntas 5

Ordenar por:

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

o inicia sesión.

Loading and Empty state screens:
.

.
Error State Screen:
.

Opte por los clásicos punticos de carga

Hola 😀

Para que nos muestre varios componentes TodosLoading podemos hacer esto

{loading && new Array(5).fill(1).map((a, i) => <TodosLoading key={i} />)}

resultado

{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;
  }
  }

yo solo lo puse una animación de carga

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);
}

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

Qué tal?

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:

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>
    )
}

Tiene una pequeña animación que no supe como cargarla

decidí crear algo mas expresivo
EmptyTodos

TodosErrot

TodoLoading

Mi Todo List

La verdad no hice muchos cambios al css :C


Aca pongo como va quedando al mio, todavía estoy definiendo que colores y cosas adicionales debería tener el proyecto. 😃

Bueno amigos, yo no me quise quebrar la cabeza demasiado, porque apenas estoy aprendiendo el concepto, sin embargo, use Loading Sleletons sin mayor problema, solamente genere un Hook personalizado para que se mostrara la animación de la carga, y en logar de la <p> que se usó en clase, coloque el hook en el loading.
Acá les comparto mi evidencia:

/* Declaración del Hook para el Loading Skeleton */
import { Carga } from '../Carga';
import { Vacio } from '../Vacio';
import { Error } from '../Error';
{/* Sustituyendo el <p> por el Loading Skeleton */}
{loading && <MyLoader />}
import React from "react"
import ContentLoader from "react-content-loader"

function Carga({props}){
    
    return (
        <ContentLoader 
            rtl
            speed={1}
            width={500}
            height={160}
            viewBox="0 0 500 160"
            backgroundColor="#e8e8e8"
            foregroundColor="#d2ff3a"
            {...props}
        >
            <rect x="8" y="16" rx="4" ry="4" width="15" height="25" />
            <rect x="30" y="55" rx="4" ry="4" width="457" height="23" />
            <rect x="8" y="94" rx="4" ry="4" width="15" height="25" />
        </ContentLoader>
    );

}

export {Carga};

¡OJO!: Necesitan usar npm install react-content-loader para que les ejecute correctamente.
Despues de leer la solucion si me atrevi a hacer lo mismo.

import React from "react"
import img from "../images/registro.png";
import './Vacio.css';

function Vacio(){
    
    return (
        <figure className="vacio">
            <figcaption>¡Crea tu primer TODO!</figcaption>
            <img className="img" src={img} alt="¡Crea tu primer TODO!" />
        </figure>
    );

}

export {Vacio};
import React from "react"
import img from "../images/error.png";
import './Error.css';

function Error(){
    
    return (
        <figure className="error">
            <figcaption>Desespérate, hubo un error...</figcaption>
            <img className="img" src={img} alt="Desespérate, hubo un error..." />
        </figure>
    );

}

export {Error};

Espero y les sirva.

Estado de carga:

Ya todo cargado:

Hola! Así va quedando mi app:

  • Loading

    (los puntitos están animados)
  • Empty
  • Error

Aquí mi propuesta de LOADING…

Aquí dejo mis pantallas, no son la gran cosa pero es trabajo honesto 😃

Esta pantalla es una animación van apareciendo y desapareciendo, pero no supe como poner un gif aquí

El resto si son imágenes estáticas

Este ha sido mi resultado

repositorio 👉 https://github.com/Misael-GC/intreoduccion-react

Aqui mi reto:

Loading Page:

Error Page:

Empty Page:

Utilice algo diferente, aquí por si quieren aprender a cómo hacerlo: https://css-tricks.com/single-element-loaders-the-dots/

Carga

Sin TODO’s

TODOS!

Despues de hacer el reto de esta clase me encontre con el pequeño problema de que cuando buscamos los TODOs el componente de <EmptyTodos /> se activa porque la expresion si es valida:

  • EmptyTodo activo:

Entonces para arreglar esto cambie el evaluador de los todos para que no use searchedTodos que va a cambiar con el estado sino totalTodos, asi cuando hayan TODOs y estemos buscando no va saltar el componente de empty.

  • EmptyTodo no activo:

  • Codigo:

/* Tambien puse condicionales extras para que no se interfieran entre si las expresiones*/
				{loading && <TodosLoading />}
				{!loading && error && <TodosError />}
				{!error && !loading && totalTodos < 1 && <EmptyTodos />}

Ustedes tambien encontraron este problema probando sus componentes?
Y si es asi como lo solucionaron?

En este enlace encuentran varios estilos de loaders solo con CSS y HTML. Un solución rápida, fácil y que no requiere instalaciones.

<TodoContainer>
          {/* {value.error && <p>Desesperate, hubo un error...</p>}
          {value.loading && <p>Estamos cargando, no desesperes...</p>}
          {(!value.loading && !value.filtradoTodos.length) && <p>Crea tu primer TODO</p>} */}
          {/* {value.filtradoTodos.map(todo => (
          <TodoItem 
          key={todo.id} 
          tarea={todo.tarea} 
          fecha={todo.fecha} 
          todoEstado={todo.estado}
          completeTodo={()=> completeTodo(todo.tarea)}
          deleteTodo={()=> deleteTodo(todo.tarea)}
          editTodo={()=> editTodo(todo.tarea)}
          />))} */}
          {(value.loading)? <Loader />
            :
            value.todos.map(todo => <TodoItem 
              key={todo.id} 
              todo={todo}
              tarea={todo.tarea} 
              fecha={todo.fecha} 
              todoEstado={todo.estado}
              completeTodo={value.completeTodo}
              deleteTodo={value.deleteTodo}
            />)
          }
        </TodoContainer>

Reto:
.

Para poner tres barras solo copie 3 veces la etiqueta!

     <TodoList>
            {error && <TodosError error={error} />}
            {loading && <TodosLoading />}
            {loading && <TodosLoading />}
            {loading && <TodosLoading />}
            {(!loading && !searchedTodos.length) && <EmptyTodos />}
</TodoList>

Loading Screen:

Aca estan mis estilos:

Hola a todos este es mi aporte y lo hice de 2 maneras

  1. por eseleton desde Create React Content Loader pero no fue de mi preferencia

  2. lo hice de forma personalizada que es algo muy secillo aqui se los dejo:

Eskeleton personalizado:

CODIGO REACT

import React from "react";
//import ContentLoader from "react-content-loader";
import "bootstrap";
import "../styles/TodosLoading.css"

function TodosLoading(props) {
  return (
    <div className="container">
      <div className="background">
        <ul className="listado">
          <li className="item">Loading...</li>
          <li className="item">Loading...</li>
          <li className="item">Loading...</li>
          <li className="item">Loading...</li>
          <li className="item">Loading...</li>
        </ul>
      </div>
    </div>
  );
}

export { TodosLoading };

CODIGO CSS

*{
   
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    color: grey;
 }
 
 .container{
 padding: 10px;
 margin: 1%;
 border: 0px solid grey;
 text-align: left;
    
 }
 
 .listado{
 
  margin: 0;
  padding: 0 0 36px 0;
  list-style: none;
 
 }
 
 
 .item{
  border: 0px solid grey;
  padding: 20px;
  margin-top: 24px;
  box-shadow: 0px 10px 60px  rgba(32, 35, 41, 0.15);
  border-radius: 5px;
     
 }
 .background {
     animation-duration: 5s;
     animation-fill-mode: forwards;
     animation-iteration-count: infinite;
     animation-name: preloadAnimation;
     animation-timing-function: linear;
     background: #f6f7f8;
     background: linear-gradient(to right, #eeeeee 8%, #dddddd 18%, #eeeeee 33%);
     display: absolute;
         min-height: 150px;
 }
 
 @keyframes preloadAnimation{
     0%{
         background-position: -769px 0
     }
     100%{
         background-position: 769px 0
     }
 }

load{
  width: 100%;
  justify-content: center;
  display: flex;
 align-content: center;
  height: 53vh!important;

}
.load span{
  margin-left: 10px;
  color:#8bc6ec;
  font-size: 10px;
 
}
#spn1{
   animation-name: example;
  animation-duration: 4s ;
  animation-iteration-count: infinite
}

#spn2{
  animation-name: example;
 animation-duration: 4s ;
 animation-iteration-count: infinite;
 animation-delay: 1s;
}

#spn3{
  animation-name: example;
 animation-duration: 4s ;
 animation-delay: 2s;
 animation-iteration-count: infinite
}

#spn4{
  animation-name: example;
 animation-duration: 4s ;
 animation-delay: 3s;
 animation-iteration-count: infinite
}
@keyframes example {
    0%   {color:rgb(255, 255, 255);  font-size: 10px;}
    50% {color:rgb(25, 153, 192); font-size: 20px}; 
    100% {color:rgb(255, 255, 255); font-size: 20px}; 
  
}

Creé una app muy simple en React desde 0 (la primera que hago) y le añadí Skeleton. Les dejo el resultado:
https://jorgemacper.github.io/random-user/

Aquí les dejo el repositorio del proyecto:
https://github.com/jorgemacper/random-user

Me ayudó mucho esta serie de tutoriales donde se explica muy bien como usar skeletons con React:
https://www.youtube.com/watch?v=cg_tmJBisp8&list=PL4cUxeGkcC9i6bZhMuAzQpC6YgLmB4k4-

Mi diseño con Skeleton

Acá les dejo otro ejemplo de como crear un skeleton:

https://platzi.com/comentario/2570313/

Este es el resultado del reto. Saludos !

import React from "react";
import ContentLoader from "react-content-loader";
import "./LoadingSkeleton.css";

function LoadingSkeleton(props) {
  return (
    <li className="item">
      <span
        className={`${
          props.completed ? "fas fa-undo todo-undo" : "check fas fa-check"
        }`}
        onClick={props.onComplete}
      ></span>
      <p className="loading_description">
        <ContentLoader className="content_loader"
        speed={1}
        width={130}
        height={20}
        viewBox="0 0 130 20"
        backgroundColor="#f3f3f3"
        foregroundColor="#ecebeb"
        {...props}
      >
        <circle cx="20" cy="10" r="10" />
        <circle cx="50" cy="10" r="10" />
        <circle cx="80" cy="10" r="10" />
      </ContentLoader>
      </p>
      <span
        className={"delete-todo fas fa-trash"}
        onClick={props.onDelete}
      ></span>
    </li>
  );
}

export { LoadingSkeleton };

Aquí mi resultado hasta ahora:

Muy buen ejercicio

import React, { useContext} from 'react';
import { Box, Stack, Text, Skeleton } from '@chakra-ui/react'
import { TodoContext } from '../Context';
import { TodoItem, ModalTodo } from '../../components'

const TodoList = () => {
    const { searchTodos: todos, error, loading } = useContext(TodoContext);
    return (
            <Box
                rounded={'lg'}
                bg={'todo.1000'}
                backdropFilter={'blur(10px)'}
                boxShadow={'lg'}
                p={8}
                mt={10}
				mb={20}>
                <Stack spacing={4}>
                    {error && <Text>Error: {error}</Text>}
                    {loading && <Stack>
                        <Skeleton height='20px' />
                        <Skeleton height='20px' />
                        <Skeleton height='20px' />
                        <Skeleton height='20px' />
                        </Stack>
                    }
                    {(!todos.length && !loading) &&  <Text align={'center'}>No hay Todos</Text>}
                    <ModalTodo />
                    {
                        todos.map((todo, index) => {
                            return (
                                <TodoItem
                                    key={index} 
                                    todo={todo}
                                />
                            )
                        })
                    }
                </Stack>
            </Box>          
    )
}

export default TodoList;