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 currentNameIndex
para 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 getDerivedStateFromProps
método é static
e 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
}
})
}
}
fonte
currentIndex
. Quandonames
estão sendo atualizados no pai simplesmente redefinircurrentIndex
next
para o componente. O botão exibe o nome, e seu onClick apenas chama a função seguinte, localizada no pai, que lida com o ciclismoRespostas:
Você pode usar um componente funcional? Pode simplificar um pouco as coisas.
useEffect
é semelhante ao localcomponentDidUpdate
onde será usada uma matriz de dependências (variáveis de estado e prop) como o segundo argumento. Quando essas variáveis mudam, a função no primeiro argumento é executada. Simples assim. Você pode fazer verificações lógicas adicionais dentro do corpo da função para definir variáveis (por exemplo,setCurrentNameIndex
).Apenas tome cuidado se você tiver uma dependência no segundo argumento que é alterado dentro da função, então você terá repetições infinitas.
Confira os documentos useEffect , mas provavelmente você nunca mais desejará usar um componente de classe novamente depois de se acostumar com os ganchos.
fonte
useEffect
. Se você atualizar para explicar por que isso funciona, marcarei isso como a resposta aceita.Como eu disse nos comentários, não sou fã dessas soluções.
Os componentes não devem se importar com o que os pais estão fazendo ou com a corrente
state
dos pais, eles devem simplesmente absorverprops
e produzir algunsJSX
, dessa forma eles são verdadeiramente reutilizáveis, composíveis e isolados, o que também facilita muito o teste.Podemos fazer com que o
NamesCarousel
componente mantenha os nomes do carrossel, juntamente com a funcionalidade do carrossel e o nome visível atual, e criar umName
componente que faça apenas uma coisa, exibindo o nome que apareceprops
Para redefinir
selectedIndex
quando os itens estão mudando, adicione auseEffect
com itens como uma dependência, embora se você apenas adicionar itens ao final da matriz, poderá ignorar esta parteAgora isso está bem, mas é
NamesCarousel
reutilizável? não, oName
componente é masCarousel
está acoplado aoName
componente.Então, o que podemos fazer para torná-lo verdadeiramente reutilizável e ver os benefícios de projetar um componente isoladamente ?
Podemos tirar proveito do padrão de adereços de renderização .
Vamos criar um
Carousel
componente genérico que terá uma lista genéricaitems
e chamará achildren
função que passa no campo selecionado.item
Agora, o que esse padrão realmente nos dá?
Isso nos dá a capacidade de renderizar o
Carousel
componente assimAgora, eles são isolados e realmente reutilizáveis, você pode transmitir
items
como uma variedade de imagens, vídeos ou até usuários.Você pode ir além e fornecer ao carrossel o número de itens que deseja exibir como acessórios e chamar a função filho com uma variedade de itens
fonte
selectedIndex
quando os adereços mudarem, não é? Se sim, como?Você pergunta qual é a melhor opção, a melhor opção é torná-lo um componente controlado.
O componente está muito baixo na hierarquia para saber como lidar com as alterações nas propriedades - e se a lista mudasse apenas um pouco (talvez adicionando um novo nome) - o componente de chamada pode querer manter a posição original.
Em todos os casos, posso pensar que estamos em melhor situação se o componente pai puder decidir como o componente deve se comportar quando receber uma nova lista.
Também é provável que esse componente faça parte de um todo maior e precise passar a seleção atual para seu pai - talvez como parte de um formulário.
Se você é realmente inflexível em não torná-lo um componente controlado, existem outras opções:
useEffect
como sugeriu Asaf Aviv, é uma maneira muito limpa de fazer isso.getDerivedStateFromProps
- e sim, isso significa manter uma referência à lista de nomes no estado e compará-la. Pode parecer um pouco melhor se você escrever algo assim:(você provavelmente também deve usar
state.names
o método render se escolher esta rota)Mas o componente realmente controlado é o caminho a percorrer, provavelmente você o fará mais cedo ou mais tarde, quando as demandas mudarem e os pais precisarem conhecer o item selecionado.
fonte
The component is too low in the hierarchy to know how to handle it's properties changing
esta é a melhor resposta. As alternativas que você fornece são interessantes e úteis. A principal razão pela qual não quero expor essa proposta é que esse componente será usado em vários projetos e quero que a API seja mínima.If you are ok with hooks, than useEffect as Asaf Aviv suggested is a very clean way to do it.
- Isso não resolve o problema - apesar de ter 4 votos, essa pergunta não redefine o estado na alteração de adereços.useEffect(() => { setSelectedIndex(0) }, [items])
O segundo argumento do UseEffect define quando deve ser executado - com a mesma eficácia que será executadosetSelectedIndex(0)
sempre que os itens forem alterados. Qual é a sua exigência, tanto quanto eu o entendo.useEffect
na minha solução redefinirá o índice quandonames
houver alterações, o TunnuseEffect
é inválido e mostrará erros de fiapos e lançará erros quando os nomes estiverem mudando.names
diretamente, o que é outro problema.