Tengo un componente muy simple con un campo de texto y un botón:
Toma una lista como entrada y permite al usuario recorrer la lista.
El componente tiene el siguiente código:
import * as React from "react";
import {Button} from "@material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}
Este componente funciona muy bien, excepto que no he manejado el caso cuando el estado cambia. Cuando el estado cambia, me gustaría restablecer currentNameIndex
a cero.
¿Cuál es la mejor manera de hacer esto?
Opciones que he considerado:
Utilizando componentDidUpdate
Esta solución es incómoda, porque se componentDidUpdate
ejecuta después del renderizado, por lo que necesito agregar una cláusula en el método de renderizado para "no hacer nada" mientras el componente está en un estado no válido, si no tengo cuidado, puedo causar una excepción de puntero nulo .
He incluido una implementación de esto a continuación.
Utilizando getDerivedStateFromProps
El getDerivedStateFromProps
método es static
y la firma solo le da acceso al estado actual y a los próximos accesorios. Esto es un problema porque no se puede saber si los accesorios han cambiado. Como resultado, esto te obliga a copiar los accesorios en el estado para que puedas verificar si son iguales.
Hacer que el componente esté "totalmente controlado"
No quiero hacer esto Este componente debe ser el propietario privado del índice seleccionado actualmente.
Hacer que el componente "esté totalmente descontrolado con una llave"
Estoy considerando este enfoque, pero no me gusta cómo hace que el padre necesite comprender los detalles de implementación del niño.
Misceláneos
He pasado mucho tiempo leyendo Probablemente no necesites el estado derivado, pero no estoy contento con las soluciones propuestas allí.
Sé que las variaciones de esta pregunta se han hecho varias veces, pero no creo que ninguna de las respuestas evalúe las posibles soluciones. Algunos ejemplos de duplicados:
- Cómo restablecer el estado en un componente en el cambio de apoyo
- Actualizar el estado del componente cuando cambian los accesorios
Apéndice
Solución usando componetDidUpdate
(ver descripción arriba)
import * as React from "react";
import {Button} from "@material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
if(this.state.currentNameIndex >= this.props.names.length){
return "Cannot render the component - after compoonentDidUpdate runs, everything will be fixed"
}
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>): void {
if(prevProps.names !== this.props.names){
this.setState({
currentNameIndex: 0
})
}
}
}
Solución usando getDerivedStateFromProps
:
import * as React from "react";
import {Button} from "@material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
copyOfProps?: Props
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
static getDerivedStateFromProps(props: Props, state: State): Partial<State> {
if( state.copyOfProps && props.names !== state.copyOfProps.names){
return {
currentNameIndex: 0,
copyOfProps: props
}
}
return {
copyOfProps: props
}
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}
fuente
currentIndex
. Cuandonames
se actualizan en el padre, simplemente reiniciecurrentIndex
next
al componente. El botón muestra el nombre y onClick solo llama a la siguiente función, ubicada en el padre, que maneja el ciclismoRespuestas:
¿Se puede usar un componente funcional? Podría simplificar un poco las cosas.
useEffect
es similar acomponentDidUpdate
donde tomará una matriz de dependencias (variables de estado y prop) como segundo argumento. Cuando esas variables cambian, se ejecuta la función en el primer argumento. Simple como eso. Puede hacer verificaciones lógicas adicionales dentro del cuerpo de la función para establecer variables (por ejemplo,setCurrentNameIndex
).Solo tenga cuidado si tiene una dependencia en el segundo argumento que se cambia dentro de la función, entonces tendrá infinitos rerenders.
Echa un vistazo a los documentos useEffect , pero probablemente nunca quieras volver a usar un componente de clase después de acostumbrarte a los ganchos.
fuente
useEffect
. Si actualiza para explicar por qué esto funciona, lo marcaré como la respuesta aceptada.Como dije en los comentarios, no soy fanático de estas soluciones.
Los componentes no deben preocuparse por lo que está haciendo el padre o cuál es la corriente
state
del padre, simplemente deben aceptarprops
y generar algunosJSX
, de esta manera son realmente reutilizables, compostables y aislados, lo que también hace que las pruebas sean mucho más fáciles.Podemos hacer que el
NamesCarousel
componente contenga los nombres del carrusel junto con la funcionalidad del carrusel y el nombre visible actual y hacer que unName
componente que solo haga una cosa, muestre el nombre que entraprops
Para restablecer
selectedIndex
cuando los elementos cambian, agregue unuseEffect
elemento con como una dependencia, aunque si solo agrega elementos al final de la matriz, puede ignorar esta parteAhora bien, ¿pero es
NamesCarousel
reutilizable? no, elName
componente es peroCarousel
está acoplado con elName
componente.Entonces, ¿qué podemos hacer para que sea realmente reutilizable y ver los beneficios de diseñar componentes de forma aislada ?
Podemos aprovechar el patrón de accesorios de renderizado .
Vamos a hacer un
Carousel
componente genérico que tomará una lista genéricaitems
e invocará lachildren
función que pasa en el seleccionadoitem
¿Qué nos da este patrón en realidad?
Nos da la capacidad de renderizar el
Carousel
componente de esta maneraAhora que están aislados y son realmente reutilizables, puede pasarlos
items
como un conjunto de imágenes, videos o incluso usuarios.Puede llevarlo más lejos y darle al carrusel la cantidad de elementos que desea mostrar como accesorios e invocar la función secundaria con una variedad de elementos.
fuente
selectedIndex
cuando cambien los accesorios, ¿verdad? ¿Si es así, cómo?Pregunta cuál es la mejor opción, la mejor opción es convertirla en un componente controlado.
El componente es demasiado bajo en la jerarquía para saber cómo manejar el cambio de sus propiedades, y si la lista cambiara pero solo un poco (quizás agregando un nuevo nombre), el componente que realiza la llamada podría querer mantener la posición original.
En todos los casos, puedo pensar que estamos mejor si el componente principal puede decidir cómo debe comportarse el componente cuando se le proporciona una nueva lista.
También es probable que dicho componente sea parte de un todo más grande y necesite pasar la selección actual a su padre, tal vez como parte de un formulario.
Si realmente es inflexible en no convertirlo en un componente controlado, hay otras opciones:
useEffect
como Asaf Aviv sugirió, es una forma muy limpia de hacerlo.getDerivedStateFromProps
, y sí, eso significa mantener una referencia a la lista de nombres en el estado y compararla. Puede verse un poco mejor si lo escribe de esta manera:(probablemente también deberías usar
state.names
el método de renderizado si eliges esta ruta)Pero el componente realmente controlado es el camino a seguir, probablemente lo hará tarde o temprano de todos modos cuando las demandas cambien y el padre necesite saber el elemento seleccionado.
fuente
The component is too low in the hierarchy to know how to handle it's properties changing
esta es la mejor respuesta. Las alternativas que proporciona son interesantes y útiles. La razón principal por la que no quiero exponer este accesorio es que este componente se usará en múltiples proyectos y quiero que la API sea mínima.If you are ok with hooks, than useEffect as Asaf Aviv suggested is a very clean way to do it.
- Esto no resuelve el problema: a pesar de tener 4 votos, esa pregunta no restablece el estado en el cambio de accesorios.useEffect(() => { setSelectedIndex(0) }, [items])
El segundo argumento de UseEffect define cuándo debe ejecutarse, de manera tan efectiva que se ejecutarásetSelectedIndex(0)
cada vez que los elementos cambien. Cuál es su requisito hasta donde yo entiendo.useEffect
en mi solución restablecerá el índice cuandonames
cambie, Tunn'suseEffect
no es válido y mostrará errores de linting y arrojará errores cuando cambien los nombres.names
directamente, lo cual es otro problema.