State es un feature fundamental de React. Cada componente puede almacenar su propio estado de manera isolada al resto y a partir de dicho estado, modificar el HTML que se muestra en pantalla.
State es lo que permite interacción en nuestras apps de React.
Pero State no tiene nada de especial, es simplemente un objeto plano de JavaScript donde describimos los datos que influyen en el render de nuestro componente.
React ya tiene mecanismos para manejar State: la función setState que está disponible en componentes escritos con clases y el Hook de useState, para componentes escritos como funciones.
Existen otras alternativas muy populares para manejar State en React son librerías como Redux o MobX. La utilidad de estas librerías es innegable pero muchas veces su inclusión en proyectos de React es casi dogmática, sin considerar la familiaridad del equipo de desarrollo o el overhead de conceptos nuevos.
Dan Abramov, creador de Redux, está de acuerdo en que quizás no necesites Redux.
Aislado en un componente.
import React from 'react';
import { render } from 'react-dom';
class Counter extends React.Component {
constructor () {
super();
this.state = {
count: 0
};
this.increment = this.increment.bind(this);
this.decrement = this.decrement.bind(this);
}
increment () {
this.setState({
count: this.state.count + 1
})
}
decrement () {
this.setState({
count: this.state.count - 1
})
}
render () {
return (
<div>
You clicked { this.state.count } times!
<button type="button" onClick={ this.increment }>+</button>
<button type="button" onClick={ this.decrement }>-</button>
</div>
)
}
}Bueno, ¿y si quiero compartir el estado con otro componente?
Si tienes dos componentes uno al lado del otro y quieres compartir estado con ambos, lo mejor es declarar el estado un nivel más arriba y pasarlo como props a los componentes originales, junto con los métodos para manipular el State.
Por ejemplo, acá tenemos dos calculadoras de temperaturas (de Celsius a Fahrenheit y de Celsius a Kelvin), cada una maneja su propio estado.
import React from 'react'
class CelsiusToFahrenheit extends React.Component {
constructor () {
super()
this.state = {
temperature: 0
}
this.handleChange = this.handleChange.bind(this);
}
handleChange (event) {
this.setState({
temperature: Number(event.target.value)
})
}
render () {
const { temperature } = this.state;
return (
<div>
<label>Introduce la temperatura en Celsius</label>
<input onChange={ this.handleChange } type="number" value={ temperature }/>
<p>{temperature} grados Celsius son { (temperature * 1.8) + 32 } grados Fahrenheit</p>
</div>
)
}
}
class CelsiusToKelvin extends React.Component {
constructor () {
super()
this.state = {
temperature: 0
}
this.handleChange = this.handleChange.bind(this);
}
handleChange (event) {
this.setState({
temperature: Number(event.target.value)
})
}
render () {
const { temperature } = this.state;
return (
<div>
<label>Introduce la temperatura en Celsius</label>
<input onChange={ this.handleChange } type="number" value={ temperature }/>
<p>{temperature} grados Celsius son { temperature + 237.15 } grados Kelvin</p>
</div>
)
}
}Usando la filosofía de "lifting state up", podemos sacar el State de cada una de las calculadoras a un componente superior y sincronizar ambos componentes.
import React from 'react'
class App extends React.Component {
constructor () {
super()
this.state = {
temperature: 0
}
this.handleChange = this.handleChange.bind(this)
}
handleChange (event) {
this.setState({
temperature: Number(event.target.value)
})
}
render () {
return (
<div>
<CelsiusToFahrenheit temperature={ this.state.temperature } handleChange={ this.handleChange } />
<CelsiusToKelvin temperature={ this.state.temperature } handleChange={ this.handleChange } />
</div>
)
}
}
class CelsiusToFahrenheit extends React.Component {
render () {
const { temperature } = this.props;
return (
<div>
<label>Introduce la temperatura en Celsius</label>
<input onChange={ this.props.handleChange } type="number" value={ temperature }/>
<p>{temperature} grados Celsius son { (temperature * 1.8) + 32 } grados Fahrenheit</p>
</div>
)
}
}
class CelsiusToKelvin extends React.Component {
render () {
const { temperature } = this.props;
return (
<div>
<label>Introduce la temperatura en Celsius</label>
<input onChange={ this.props.handleChange } type="number" value={ temperature }/>
<p>{temperature} grados Celsius son { temperature + 237.15 } grados Kelvin</p>
</div>
)
}
}El método anterior es útil cuando se sube el State uno o dos niveles arriba, pero si hay State que debería considerarse "global" (por ejemplo, datos de un usuario o el tema de la app) resulta molesto y tedioso pues tienes que pasar las props manualmente de componente a componente.
Justo para este problema existe Context.
// Creamos un Context con un valor inicial, aunque no es necesario
// Una buena práctica sería tener cada uno de nuestros contexto
// en archivos separados e importarlos cuando sea necesario
const ThemeContext = React.createContext({
theme: "light",
toggleTheme: () => {}
});
class App extends React.Component {
constructor() {
super();
this.state = {
theme: "light"
};
this.toggleTheme = this.toggleTheme.bind(this);
}
toggleTheme() {
const { theme } = this.state;
if (theme === "light") {
this.setState({
theme: "dark"
});
} else {
this.setState({
theme: "light"
});
}
}
render() {
return (
<ThemeContext.Provider
value={{
theme: this.state.theme,
toggleTheme: this.toggleTheme
}}
>
<ButtonGroup />
</ThemeContext.Provider>
);
}
}
class ButtonGroup extends React.Component {
render() {
return (
<div>
{/* Muy importante ver como no estamos pasando ninguna prop a estos componentes */}
<Article />
<ThemedButton />
</div>
);
}
}
class Article extends React.Component {
// el Context que le pasemos a contextType, buscará el Provider más cercano
// (en este caso, el que está sobre <App />) y obtendrá los valores de ese Context
static contextType = ThemeContext;
render() {
const styles = {
padding: "1rem 2rem",
width: "100%",
marginBottom: 20
};
const lightStyles = {
background: "#f7f7f7",
color: "#333"
};
const darkStyles = {
background: "#333",
color: "#f7f7f7"
};
// gracias a contextType, tenemos acceso a this.context
const { theme } = this.context;
return (
<div
style={
theme === "light"
? { ...styles, ...lightStyles }
: { ...styles, ...darkStyles }
}
>
<h3>Este es un blogpost</h3>
<p>Este es el cuerpo del blogpost</p>
</div>
);
}
}
class ThemedButton extends React.Component {
// el Context que le pasemos a contextType, buscará el Provider más cercano
// (en este caso, el que está sobre <App />) y obtendrá los valores de ese Context
static contextType = ThemeContext;
render() {
const styles = {
padding: "1rem 2rem",
boxShadow: "0 1px 3px rgba(0,0,0,.25)",
borderRadius: 4,
border: "1px solid #333"
};
const lightStyles = {
background: "#f7f7f7",
color: "#333"
};
const darkStyles = {
background: "#333",
color: "#f7f7f7"
};
// gracias a contextType, tenemos acceso a this.context
const { theme, toggleTheme } = this.context;
return (
<button
type="button"
style={
theme === "light"
? { ...styles, ...lightStyles }
: { ...styles, ...darkStyles }
}
onClick={toggleTheme}
>
Actualizar tema
</button>
);
}
}Si tienes necesidades más complejas de manejar states, quizás sí necesites Redux :P
- setState ftw! slides de Michele Bertoli
- Should I keep something in React state? por Dan Abramov
- Funciones como argumento de
setState - You Might Not Need Redux, de Dan Abramov
- FAQ del State de React
- When should I use Redux?
- Lifting state up
- [Using State Correctly)[https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly]
- Context