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
}
})
}
}
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 ciclismo
currentIndex
. Cuandonames
se actualizan en el padre, simplemente reiniciecurrentIndex