javascript - framework - react seo 2018
Componentes erróneos renderizados por Preact (1)
Estoy usando Preact (para todos los efectos, React) para representar una lista de elementos, guardada en una matriz de estado. Cada elemento tiene un botón de eliminación junto a él. Mi problema es: cuando se hace clic en el botón, se elimina el elemento adecuado (lo verifiqué varias veces), pero los elementos se vuelven a representar con el último elemento faltante, y el elemento eliminado sigue allí. Mi código (simplificado):
import { h, Component } from ''preact'';
import Package from ''./package'';
export default class Packages extends Component {
constructor(props) {
super(props);
let packages = [
''a'',
''b'',
''c'',
''d'',
''e''
];
this.setState({packages: packages});
}
render () {
let packages = this.state.packages.map((tracking, i) => {
return (
<div className="package" key={i}>
<button onClick={this.removePackage.bind(this, tracking)}>X</button>
<Package tracking={tracking} />
</div>
);
});
return(
<div>
<div className="title">Packages</div>
<div className="packages">{packages}</div>
</div>
);
}
removePackage(tracking) {
this.setState({packages: this.state.packages.filter(e => e !== tracking)});
}
}
¿Qué estoy haciendo mal? ¿Necesito volver a renderizar activamente de alguna manera? ¿Es este un caso de n + 1 de alguna manera?
Aclaración : Mi problema no es con la sincronicidad del estado. En la lista anterior, si elijo eliminar ''c'', el estado se actualiza correctamente a [''a'',''b'',''d'',''e'']
, pero los componentes representados son [''a'',''b'',''c'',''d'']
. En cada llamada para removePackage
el removePackage
el correcto se elimina de la matriz, se muestra el estado correcto, pero se muestra una lista incorrecta. ( console.log
declaraciones de console.log
, por lo que no parece que sean mi problema).
Este es un problema clásico que está totalmente desatendido por la documentación de Preact, ¡así que me gustaría disculparme personalmente por eso! Siempre estamos buscando ayuda para escribir mejor documentación si alguien está interesado.
Lo que sucedió aquí es que está usando el índice de su Array como una clave (en su mapa dentro del render). De hecho, esto simplemente emula cómo funciona una diferencia de VDOM de forma predeterminada: las teclas siempre son 0-n
donde n
es la longitud de la matriz, por lo que eliminar cualquier elemento simplemente elimina la última clave de la lista.
Explicación: Las claves trascienden los renders.
En su ejemplo, imagine cómo se verá el DOM (virtual) en el procesamiento inicial y luego, después de eliminar el elemento "b" (índice 3). A continuación, supongamos que su lista solo tiene 3 elementos ( [''a'', ''b'', ''c'']
):
Esto es lo que produce el render inicial:
<div>
<div className="title">Packages</div>
<div className="packages">
<div className="package" key={0}>
<button>X</button>
<Package tracking="a" />
</div>
<div className="package" key={1}>
<button>X</button>
<Package tracking="b" />
</div>
<div className="package" key={2}>
<button>X</button>
<Package tracking="c" />
</div>
</div>
</div>
Ahora, cuando hacemos clic en "X" en el segundo elemento de la lista, "b" se pasa a removePackage()
, que establece los state.packages
de state.packages
en [''a'', ''c'']
. Eso activa nuestro render, que produce el siguiente DOM (virtual):
<div>
<div className="title">Packages</div>
<div className="packages">
<div className="package" key={0}>
<button>X</button>
<Package tracking="a" />
</div>
<div className="package" key={1}>
<button>X</button>
<Package tracking="c" />
</div>
</div>
</div>
Dado que la biblioteca VDOM solo conoce la nueva estructura que le da en cada renderización (no cómo cambiar de la estructura anterior a la nueva), lo que han hecho las claves es básicamente decirle que los elementos 0
y 1
permanecieron en su lugar. sabe que esto es incorrecto, porque queríamos que el elemento en el índice 1
fuera eliminado.
Recuerde: la key
tiene prioridad sobre la semántica predeterminada de reordenamiento de diferencias de niño. En este ejemplo, dado que la key
siempre es solo el índice de matriz basado en 0, el último elemento ( key=2
) simplemente se elimina porque es el que falta en el procesamiento posterior.
La solución
Entonces, para corregir su ejemplo, debe usar algo que identifique el elemento en lugar de su desplazamiento como su clave. Este puede ser el elemento en sí (cualquier valor es aceptable como clave) o una propiedad .id
(se prefiere porque evita las referencias de objetos de dispersión alrededor de las cuales se puede evitar GC):
let packages = this.state.packages.map((tracking, i) => {
return (
// ↙️ a better key fixes it :)
<div className="package" key={tracking}>
<button onClick={this.removePackage.bind(this, tracking)}>X</button>
<Package tracking={tracking} />
</div>
);
});
Por cierto, eso era mucho más largo de lo que yo pretendía que fuera.
TL, DR: nunca utilice un índice de matriz (índice de iteración) como key
. En el mejor de los casos, imita el comportamiento predeterminado (reordenación descendente de niños), pero más a menudo solo empuja todo lo que difiere sobre el último niño.
edit: @tommy recomendó este excelente enlace a los documentos de eslint-plugin-react , que hace un mejor trabajo explicándolo de lo que hice anteriormente.