Eu tenho um componente muito simples com um campo de texto e um botão:
Leva uma lista como entrada e permite ao usuário percorrer a lista.
O componente possui o seguinte 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
}
})
}
}
Esse componente funciona muito bem, exceto que eu não lidei com o caso quando o estado muda. Quando o estado mudar, eu gostaria de redefinir o valor currentNameIndexpara zero.
Qual é a melhor maneira de fazer isso?
Opções que eu concordei:
Usando componentDidUpdate
Esta solução é desagradável, porque componentDidUpdateé executada após a renderização, portanto, preciso adicionar uma cláusula no método de renderização para "não fazer nada" enquanto o componente estiver em um estado inválido. Se não tomar cuidado, posso causar uma exceção de ponteiro nulo .
Eu incluí uma implementação disso abaixo.
Usando getDerivedStateFromProps
O getDerivedStateFromPropsmétodo é statice a assinatura apenas fornece acesso ao estado atual e aos próximos objetos. Este é um problema porque você não pode dizer se os objetos foram alterados. Como resultado, isso força você a copiar os adereços no estado para poder verificar se são os mesmos.
Tornando o componente "totalmente controlado"
Eu não quero fazer isso. Esse componente deve ser o proprietário privado do índice atualmente selecionado.
Tornando o componente "totalmente descontrolado com uma chave"
Estou considerando essa abordagem, mas não gosto de como ela faz com que os pais precisem entender os detalhes de implementação da criança.
Diversos
Passei muito tempo lendo Você provavelmente não precisa de um estado derivado, mas estou bastante descontente com as soluções propostas lá.
Sei que variações desta pergunta foram feitas várias vezes, mas não acho que nenhuma das respostas pesa as possíveis soluções. Alguns exemplos de duplicatas:
- Como redefinir o estado em um componente na alteração de prop
- Atualizar estado do componente quando os objetos mudam
Apêndice
Solução usando componetDidUpdate(veja a descrição acima)
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
})
}
}
}
Solução 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
}
})
}
}
nextpara o componente. O botão exibe o nome, e seu onClick apenas chama a função seguinte, localizada no pai, que lida com o ciclismo

currentIndex. Quandonamesestão sendo atualizados no pai simplesmente redefinircurrentIndex