Pomodoro con React Hooks
Tags: react, pomodoro, spanish, beginners
Published: 2020-06-30 00:48:14

¡Acompáñame a construir un Pomodoro con React Hooks! 

1. <a href="#que-es">¿Qué es Pomodoro?</a>
2. <a href="#preparando">Preparando el terreno</a>
3. <a href="#componentes">Componentes</a>
* <a href="#break-session">Componentes: Break y Session</a>
* <a href="#timer">Componentes: Timer</a>
* <a href="#app">Componentes: App</a>

**Nota**: Este tutorial no va a presuponer que tengas ningún conocimiento, así que si ya sabes React es posible que resulte aburrido y quieras ir directamente al [código del proyecto terminado](https://codepen.io/drowvoloper/pen/MWKpyEN?editors=0010).

## <a name="que-es">¿Qué es Pomodoro?</a>

Se trata de una metodología que consiste en intercalar tiempos más o menos prolongados de concentración, con unos minutos de descanso.

Se utiliza mucho para estudiar, pero muchas otras personas lo usan también para programar, ya que ayuda a no olvidarnos de beber, comer, descansar la vista o simplemente estirar las piernas. 

Hoy en día disponemos de muchas aplicaciones que nos permiten seguir este método de una forma muy cómoda. Este es el funcionamiento que tienen casi todas en común:

1. Marcas el tiempo que quieres que dure la sesión y el de los descansos.
2. Pulsas "Start" y empieza la cuenta atrás.
3. Cuando el tiempo se agota, recibes una notificación que por lo general suele ser auditiva. 
4. Aquí hay diferentes opciones. Algunas aplicaciones simplemente cambian el modo (de sesión a descanso y viceversa) y empiezan la cuenta atrás del modo correspondiente. 
Otras detienen el temporizador pero no siguen corriendo hasta que no es activado manualmente. En este tutorial vamos a hacerlo de la primera manera.
5. ¡Y ya está! Puedes añadir más o menos características, pero solo con eso ya tienes una aplicación funcional.

## <a name="preparando">Preparando el terreno</a>

[ Si no puedes o quieres realizar el proyecto en el dispositivo en el que te encuentras, también puedes hacer fork a [este proyecto en Codepen](https://codepen.io/drowvoloper/pen/xxZXyve?editors=0010). El botón de fork lo encuentras en la parte de abajo de la página, por la zona de la derecha.

Puedes, entonces, pasar a la sección de <a href="#componentes">Componentes</a>. Aquí no necesitas importar nada, y ya he declarado los componentes que necesitas de React Bootstrap, así que solo tienes que usarlos.]

Así que vamos a empezar. Lo primero que necesitamos es crear un proyecto con React en local:

```
npx create-react-app pomodoro-react
cd pomodoro-react
npm start
```
**! Ten en cuenta** que necesitas tener **Node** instalado para que funcione.

Esto hará que se abra un servidor en http://localhost:3000/ donde podrás ver que React ya está funcionando. Para terminar de comprobar que todo va correctamente, vamos a ir a la carpeta `src` y abrir `App.js` con nuestro IDE o editor de texto favorito y a cambiar el código que tenemos por este:

``` javascript
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <p>Holi, soy React</p>
    </div>
  );
}

export default App;
```

Al guardar el archivo, el servidor debería actualizarse y mostrarnos solamente "*Holi, soy React*". Si es así, podemos pasar al siguiente paso: instalar **React Bootstrap**.

Para ello vamos a usar el siguiente comando:
```npm install react-bootstrap bootstrap```

Comprobemos de nuevo de una manera sencilla que Bootstrap funciona  cambiando algunas cosas en `App.js`:
```javascript
import React from 'react';
import logo from './logo.svg';
import './App.css';
import {Button} from 'react-bootstrap';

function App() {
  return (
    <div className="App">
      <Button>Holi, soy React</Button>
    </div>
  );
}

export default App;
```
Para que funcione, necesitas importar los estilos de Bootstrap en el archivo `index.js` (también en la carpeta `dist`) incluyendo la siguiente línea justo antes del `index.css`: 

```javascript
import 'bootstrap/dist/css/bootstrap.css';
```

Ahora nuestro texto simplón debería haberse convertido en un botón azul. Si es así, ¡enhorabuena! Ya podemos empezar a programar.

## <a name="componentes">Componentes</a>

Vamos a utilizar cuatro componentes: **App**, **Break**, **Session** y **Timer**. Dado que salvo App van a ser muy pocas líneas de código, vamos a escribir todo en el propio archivo `App.js`, pero si crees que para ti es más cómodo o vas a añadir más funcionalidades es posible que quieras tener cada componente en un archivo separado. En este caso lo único que tienes que recordar es importarlos a `App.js`.

### # <a name="break-session">Break y Session</a>

Aunque son dos componentes distintos, la lógica de ambos es la misma y con que construyamos uno con solo cambiar un par de nombres tendremos el otro.

Vamos a utilizar **Break** de ejemplo. Este componente solo va a encargarse de recibir y mostrar la cantidad de minutos que queremos para los descansos.

Para ello, usamos nuestro primer Hook: **useState**. Para nuestra comodidad, vamos a importarlo directamente junto a React. Entre unas cosas y otras, `App.js` se quedaría en algo parecido a esto a estas alturas:
```javascript
import React, {useState} from 'react';
import logo from './logo.svg';
import './App.css';
import {Button, Col, Container, Row} from 'react-bootstrap';

const Break = () => {
	const [breakLength, setBreakLength] = useState(5 * 60);
	return (
		<Col>
			<p>Break</p>
			<Button variant="info">-</Button>
			<span>{breakLength / 60}</span>
			<Button variant="info">+</Button>
		</Col>
	)
}

const App = () => {
  return (
    <Container className="App">
      <Row>
		<Break/>
	  </Row>
    </Container>
  );
}

export default App;
```
Fíjate que **hemos importado Col, Container y Row de React Bootstrap** también para que todo gire correctamente. 

Tenemos dos botones para sumar y restar, pero aún no hacen nada. ¡solucionemos eso creando dos funciones!
```javascript
const Break = () => {
	const [breakLength, setBreakLength] = useState(5 * 60);
	
	function decrementBreakLength() {
		const decreasedBreakLength = breakLength - 60 > 60 ? breakLength - 60 : 60;
		setBreakLength(decreasedBreakLength);
	}

	function incrementBreakLength() {
		const incrementedBreakLength =
		breakLength + 60 <= 60 * 60 ? breakLength + 60 : 60 * 60;
		setBreakLength(incrementedBreakLength);
	}
	return (
		<Col>
			<p>Break</p>
			<Button variant="info" onClick={decrementBreakLength}>-</Button>
			<span>{breakLength / 60}</span>
			<Button variant="info" onClick={incrementBreakLength}>+</Button>
		</Col>
	)
}
```

Cuando los botones de restar o sumar son pulsados, llamamos a `decrementBreakLength` o `incrementBreakLength` respectivamente. Ambas funciones hacen el cálculo correspondiente (restando o sumando 1 minuto) y actualizan el valor de `breakLength` usando `setBreakLength`. ¡ya tenemos un valor que cambia dinámicamente! Hemos puesto un límite para que no pueda ser menor de un minuto ni mayor a una hora.

Como dije antes, para tener nuestro componente **Session** solo tenemos que copiar/pegar **Break** y cambiar los términos correspondientes para que todo coincida:
```javascript
const Session = () => {
	const [sessionLength, setSessionLength] = useState(25 * 60);
	
	function decrementSessionLength() {
		const decreasedSessionLength = sessionLength - 60 > 60 ? sessionLength - 60 : 60;
		setSessionLength(decreasedSessionLength);
	}

	function incrementSessionLength() {
		const incrementedSessionLength =
		sessionLength + 60 <= 60 * 60 ? sessionLength + 60 : 60 * 60;
		setSessionLength(incrementedSessionLength);
	}
	return (
		<Col>
			<p>Session</p>
			<Button variant="info" onClick={decrementSessionLength}>-</Button>
			<span>{sessionLength / 60}</span>
			<Button variant="info" onClick={incrementSessionLength}>+</Button>
		</Col>
	)
}
```
**! recuerda** añadir **Session** a tu componente App, justo debajo de **Break**.

### # <a name="timer">Timer</a>

Usaremos este componente para mostrar los minutos y segundos que quedan para que termine la sesión o el descanso activados.

Aquí vamos a empezar a ver la necesidad de llevar las funcionalidades de **Break** y **Session** al componente padre (**App**), ya que necesitaremos acceder al valor de `breakLength` y `sessionLength` para que desde **Timer** sepamos cuál es el tiempo que tenemos que mostrar.

Así que lo primero que vamos a hacer va a ser llevarnos los hooks y las funciones y a pasar lo que sea necesario como propiedades. El código completo sería el siguiente:
```javascript
const Break = (props) => {
	const { increment, decrement, length } = props;
	return (
		<Col>
			<p>Break</p>
			<Button variant="info" onClick={decrement}>-</Button>
			<span>{length / 60}</span>
			<Button variant="info" onClick={increment}>+</Button>
		</Col>
	)
}

const Session = (props) => {
	const { increment, decrement, length } = props;
	return (
		<Col>
			<p>Session</p>
			<Button variant="info" onClick={decrement}>-</Button>
			<span>{length / 60}</span>
			<Button variant="info" onClick={increment}>+</Button>
		</Col>
	)
}

const App = () => {
	const [breakLength, setBreakLength] = useState(5 * 60);
	const [sessionLength, setSessionLength] = useState(25 * 60);
	
	function decrementBreakLength() {
		const decreasedBreakLength = breakLength - 60 > 60 ? breakLength - 60 : 60;
		setBreakLength(decreasedBreakLength);
	}

	function incrementBreakLength() {
		const incrementedBreakLength =
		breakLength + 60 <= 60 * 60 ? breakLength + 60 : 60 * 60;
		setBreakLength(incrementedBreakLength);
	}
	
	function decrementSessionLength() {
		const decreasedSessionLength = sessionLength - 60 > 60 ? sessionLength - 60 : 60;
		setSessionLength(decreasedSessionLength);
	}

	function incrementSessionLength() {
		const incrementedSessionLength =
		sessionLength + 60 <= 60 * 60 ? sessionLength + 60 : 60 * 60;
		setSessionLength(incrementedSessionLength);
	}
  return (
    <Container className="App">
      <Row>
		<Break
		  length={breakLength}
          decrement={decrementBreakLength}
          increment={incrementBreakLength}
		/>
		<Session
		  length={sessionLength}
          decrement={decrementSessionLength}
          increment={incrementSessionLength}
		/>
	  </Row>
    </Container>
  );
}
```

Todo debería seguir funcionando igual. No necesitaremos volver a preocuparnos más por los componentes **Break** y **Session**. Ahora podemos centrarnos en lo que **Timer** nos puede aportar.

De momento, vamos a pasarle `sessionLength` como un valor estático:
```javascript
    <Container className="App">
      <Timer time={sessionLength}/>
      <Row>
```
y a mostrarlo:
```javascript
const Timer = (props) => {
	const {time} = props;
	
	return (
		<div>
		  <p>Session</p>
		  <p>
			{time / 60}:00
		  </p>
		</div>
	);
}
```
Volveremos a él luego para actualizarlo, tras conseguir que **App** pueda proporcionarle datos dinámicos. 

### # <a name="app">App</a>

Aquí va todo el meollo de nuestra aplicación. No te preocupes, no es difícil y yo te lo voy a explicar lo mejor que sé. Vamos a dividir mentalmente este Componente en tres partes: useState, useEffect y funciones.

Usando **useState** vamos a declarar lo siguiente, justo debajo de `breakLength` y `sessionLength`:
```javascript
	  const [mode, setMode] = useState("session");
	  const [timeLeft, setTimeLeft] = useState();
	  const [isActive, setIsActive] = useState(false);
	  const [timeSpent, setTimeSpent] = useState(0);
	  const [beep] = useState(
		new Audio("https://freesound.org/data/previews/523/523960_350703-lq.mp3")
	  );
	  const [beepPlaying, setBeepPlaying] = useState(false);
```
· `mode` nos va a indicar en qué modo se encuentra ahora mismo la aplicación (¿modo 'session' o modo 'break'?
· `timeLeft` es un parámetro para saber cuánto tiempo queda
· `isActive` va a marcar si el temporizador está corriendo o no
· `timeSpent` puede ser menos intuitivo fuera de contexto, pero es la variable que nos va a ir guardando el tiempo que ha pasado desde que pulsamos "Start". Es lo que nos sirve para calcular cuánto nos falta.
· `beep` es el sonido que usaremos para avisar cuando el temporizador llega a 0
· `beepPlaying` nos indica si el sonido está sonando o no.

Ahora vamos a por los **useEffect**. Recuerda importarlo en la primera línea junto a React y useState de esta forma:
```javascript
import React, {useState, useEffect} from 'react';
```
`timeLeft` va a ser nuestra referencia para saber cuánto tiempo nos queda y será el parámetro que pasemos al componente **Timer**, pero necesitamos una forma de que al actualizar `breakLength` y `sessionLength` `timeLeft` cambie su valor acorde a una u otra variable dependiendo del modo en el que estemos:
```javascript
  useEffect(() => {
    setTimeLeft(mode == "session" ? sessionLength * 1000 : breakLength * 1000);
  }, [sessionLength, breakLength]);
```
El **useEffect** que viene a continuación es probablemente el bloque más largo que va a tener nuestra aplicación, así que te lo voy a enseñar e ir explicando por partes:

· Necesitamos "escuchar" los cambios en `isActive` y `timeSpent` para saber cuándo se activa el temporizador y cómo van pasando los segundos.
```javascript
useEffect(() => { }, [isActive, timeSpent]);
```
· Lo primero que vamos a hacer va a ser declarar una variable `interval` a **null**.
```javascript
let interval = null;
```
· Comprobamos que el temporizador está activo y que además `timeLeft` es mayor a 1. Es decir, que aún queda tiempo que descontar. Si ambas condiciones se cumplen calcularemos el tiempo que queda y le sumaremos un **segundo** a `timeSpent`. Lo haremos además asignando todo esto a un intervalo para que el cambio se produzca tras 1 segundo. Y como este efecto está escuchando a `timeSpent`, se seguirá ejecutando hasta que `isActive` cambie y pase a ser **false**. Cuando esto ocurra, limpiaremos el intervalo.
```javascript
    if (isActive && timeLeft > 1) {
      setTimeLeft(
        mode == "session"
          ? sessionLength * 1000 - timeSpent
          : breakLength * 1000 - timeSpent
      );

      interval = setInterval(() => {
        setTimeSpent((timeSpent) => timeSpent + 1000);
      }, 1000);
    } else {
      clearInterval(interval);
    }
```
Ahora solamente necesitamos saber cuándo `timeLeft` ha llegado a cero, que será el momento de hacer que nuestro `beep` se reproduzca. También cambiamos el modo y pasamos la longitud del que corresponda a `timeLeft` para que el temporizador siga corriendo:
```javascript
    if (timeLeft === 0) {
      beep.play();
      setBeepPlaying(true);
      setTimeSpent(0);
      setMode((mode) => (mode == "session" ? "break" : "session"));
      setTimeLeft(
        mode == "session" ? sessionLength * 1000 : breakLength * 1000
      );
    }
```
Aquí te dejo el código completo:
```javascript
useEffect(() => {
    let interval = null;

    if (isActive && timeLeft > 1) {
      setTimeLeft(
        mode == "session"
          ? sessionLength * 1000 - timeSpent
          : breakLength * 1000 - timeSpent
      );

      interval = setInterval(() => {
        setTimeSpent((timeSpent) => timeSpent + 1000);
      }, 1000);
    } else {
      clearInterval(interval);
    }
    if (timeLeft === 0) {
      beep.play();
      setBeepPlaying(true);
      setTimeSpent(0);
      setMode((mode) => (mode == "session" ? "break" : "session"));
      setTimeLeft(
        mode == "session" ? sessionLength * 1000 : breakLength * 1000
      );
    }
    return () => clearInterval(interval);
  }, [isActive, timeSpent]);
```
¡Uf! Coge aire y bebe un poco de agua. Ya queda menos. 

Este **useEffect** solamente va a encargarse de poner `beepPlaying` a **false** cuando `beep` deje de sonar.
```javascript
  useEffect(() => {
    beep.addEventListener("ended", () => setBeepPlaying(false));
    return () => {
      beep.addEventListener("ended", () => setBeepPlaying(false));
    };
  }, []);
```
Solo falta añadir algunas **funciones** a nuestro componente para que funcione como necesitamos:

```javascript
function reset() {
    setBreakLength(5 * 60);
    setSessionLength(25 * 60);
    setTimeLeft(mode == "session" ? sessionLength * 1000 : breakLength * 1000);

    if (isActive) {
      setIsActive(false);
      setTimeSpent(0);
    }

    if (beepPlaying) {
      beep.pause();
      beep.currentTime = 0;
      setBeepPlaying(false);
    }
  }

  function toggleIsActive() {
    setIsActive(!isActive);
  }
```
`reset` es un poco larga pero lo único que hace es restaurar los valores de `breakLength` y `sessionLength` a 5 y 25 minutos respectivamente. Además detiene el temporizador poniendo `isActive` a false y lo reinicia poniendo en cero la variable `timeSpent`.

`toggleIsActive` simplemente cambia `isActive` al valor opuesto (de **false** a **true** y viceversa).

Y ya solo nos queda añadir un par de componentes **Button** para que todo esto pueda funcionar.

```javascript
   <Container className="text-center">
      <h1>Pomodoro Clock</h1>

      <Timer time={timeLeft} mode={mode} />
      <div className="buttons">
        <Button onClick={toggleIsActive} id="start_stop">
          {isActive ? "Pause" : "Start"}
        </Button>
        <Button onClick={reset} id="reset" variant="danger">
          Reset
        </Button>
      </div>
      <Row className="options">
        <Break
          length={breakLength}
          decrement={decrementBreakLength}
          increment={incrementBreakLength}
        />
        <Session
          length={sessionLength}
          decrement={decrementSessionLength}
          increment={incrementSessionLength}
        />
      </Row>
    </Container>
```
Puedes ver que hemos añadido dos botones: uno para poner el temporizador en marcha (o pausarlo) y otro para poder resetearlo.

Pero espera, porque aunque nuestro temporizador funciona aún no estamos mostrando nada en pantalla. **Timer** sigue con valores estáticos. Vamos a solucionarlo: 
```javascript
const Timer = (props) => {
  const { time, mode } = props;

  const min = Math.floor(time / 1000 / 60);
  const sec = Math.floor((time / 1000) % 60);
  return (
    <div id="timer">
      <p id="timer-label">{mode}</p>
      <p id="time-left">
        {min}:{sec.toString().length === 1 ? "0" + sec : sec}
      </p>
    </div>
  );
};
```
Simplemente vamos a coger el modo en el que estamos y el tiempo que queda de las propiedades. Para mostrar el tiempo necesitamos calcular los minutos y segundos, y luego poder añadir un cero en caso de que la cifra de segundos solo tenga un dígito.

¡Y ya está! Ya tienes tu Pomodoro. 

```javascript
const Break = (props) => {
  const { increment, decrement, length } = props;
  return (
    <Col md={4}>
      <p id="break-label">Break</p>
      <Button onClick={decrement} id="break-decrement" variant="info">
        -
      </Button>
      <span id="break-length">{length / 60}</span>
      <Button variant="info" onClick={increment} id="break-increment">
        +
      </Button>
    </Col>
  );
};

const Session = (props) => {
  const { increment, decrement, length } = props;
  return (
    <Col md={{ span: 4, offset: 4 }}>
      <p id="session-label">Session</p>
      <Button onClick={decrement} id="session-decrement" variant="info">
        -
      </Button>
      <span id="session-length">{length / 60}</span>
      <Button onClick={increment} id="session-increment" variant="info">
        +
      </Button>
    </Col>
  );
};

const Timer = (props) => {
  const { time, mode } = props;

  const min = Math.floor(time / 1000 / 60);
  const sec = Math.floor((time / 1000) % 60);
  return (
    <div id="timer">
      <p id="timer-label">{mode}</p>
      <p id="time-left">
        {min}:{sec.toString().length === 1 ? "0" + sec : sec}
      </p>
    </div>
  );
};

const App = () => {
  const [breakLength, setBreakLength] = React.useState(5 * 60);
  const [sessionLength, setSessionLength] = React.useState(25 * 60);
  const [mode, setMode] = React.useState("session");
  const [timeLeft, setTimeLeft] = React.useState();
  const [isActive, setIsActive] = React.useState(false);
  const [timeSpent, setTimeSpent] = React.useState(0);
  const [beep] = React.useState(
    new Audio("https://freesound.org/data/previews/523/523960_350703-lq.mp3")
  );
  const [beepPlaying, setBeepPlaying] = React.useState(false);

  /* ########## USE EFFECT HOOKS ########## */
  React.useEffect(() => {
    setTimeLeft(mode == "session" ? sessionLength * 1000 : breakLength * 1000);
  }, [sessionLength, breakLength]);

  React.useEffect(() => {
    let interval = null;

    if (isActive && timeLeft > 1) {
      setTimeLeft(
        mode == "session"
          ? sessionLength * 1000 - timeSpent
          : breakLength * 1000 - timeSpent
      );

      interval = setInterval(() => {
        setTimeSpent((timeSpent) => timeSpent + 1000);
      }, 1000);
    } else {
      clearInterval(interval);
    }
    if (timeLeft === 0) {
      beep.play();
      setBeepPlaying(true);
      setTimeSpent(0);
      setMode((mode) => (mode == "session" ? "break" : "session"));
      setTimeLeft(
        mode == "session" ? sessionLength * 1000 : breakLength * 1000
      );
    }
    return () => clearInterval(interval);
  }, [isActive, timeSpent]);

  React.useEffect(() => {
    beep.addEventListener("ended", () => setBeepPlaying(false));
    return () => {
      beep.addEventListener("ended", () => setBeepPlaying(false));
    };
  }, []);

  /* ########## FUNCTIONS ########## */
  function decrementBreakLength() {
    const decreasedBreakLength = breakLength - 60 > 60 ? breakLength - 60 : 60;
    setBreakLength(decreasedBreakLength);
  }

  function incrementBreakLength() {
    const incrementedBreakLength =
      breakLength + 60 <= 60 * 60 ? breakLength + 60 : 60 * 60;
    setBreakLength(incrementedBreakLength);
  }

  function decrementSessionLength() {
    const decreasedSessionLength =
      sessionLength - 60 > 60 ? sessionLength - 60 : 60;

    setSessionLength(decreasedSessionLength);
  }

  function incrementSessionLength() {
    const incrementedSessionLength =
      sessionLength + 60 <= 60 * 60 ? sessionLength + 60 : 60;
    setSessionLength(incrementedSessionLength);
  }

  function reset() {
    setBreakLength(5 * 60);
    setSessionLength(25 * 60);
    setTimeLeft(mode == "session" ? sessionLength * 1000 : breakLength * 1000);

    if (isActive) {
      setIsActive(false);
      setTimeSpent(0);
    }

    if (beepPlaying) {
      beep.pause();
      beep.currentTime = 0;
      setBeepPlaying(false);
    }
  }

  function toggleIsActive() {
    setIsActive(!isActive);
  }

  return (
    <Container className="text-center">
      <h1>Pomodoro Clock</h1>

      <Timer time={timeLeft} mode={mode} />
      <div className="buttons">
        <Button onClick={toggleIsActive} id="start_stop">
          {isActive ? "Pause" : "Start"}
        </Button>
        <Button onClick={reset} id="reset" variant="danger">
          Reset
        </Button>
      </div>
      <Row className="options">
        <Break
          length={breakLength}
          decrement={decrementBreakLength}
          increment={incrementBreakLength}
        />
        <Session
          length={sessionLength}
          decrement={decrementSessionLength}
          increment={incrementSessionLength}
        />
      </Row>
    </Container>
  );
};
```

Ahora puedes trastear con él, como por ejemplo:

- Cambiar el sonido o dar a elegir entre varios diferentes
- Añadir otra forma de notificar el cambio de modo, ya que tal y como está ahora no podría ser usado por personas sordas. A mí se me ocurre, por ejemplo, cambiar el <title> de la página dinámicamente, pero no he visto que sea posible en Codepen.
- Fusionar tu Pomodoro con un proyecto de To-do list, de manera que puedas planificar en qué vas a invertir las sesiones.

Te dejo aquí de nuevo [mi proyecto terminado](https://codepen.io/drowvoloper/pen/MWKpyEN?editors=0010) y espero que hayas disfrutado construyendo esta pequeña aplicación.