javascript - elemento - jquery
¿A qué nivel de anidación deberían los componentes leer entidades de Stores in Flux? (4)
Estoy reescribiendo mi aplicación para usar Flux y tengo un problema con la recuperación de datos de las tiendas. Tengo muchos componentes y anidan mucho. Algunos de ellos son grandes ( Article
), algunos son pequeños y simples ( UserAvatar
, UserLink
).
He estado luchando con dónde en la jerarquía de componentes debería leer los datos de las tiendas.
Probé dos enfoques extremos, ninguno de los cuales me gustó bastante:
Todos los componentes de la entidad leen sus propios datos
Cada componente que necesita algunos datos de la Tienda recibe solo ID de entidad y recupera la entidad por sí mismo.
Por ejemplo, el Article
se pasa articleId
, UserAvatar
y UserLink
se pasan userId
.
Este enfoque tiene varias desventajas significativas (discutidas en la muestra del código).
var Article = React.createClass({
mixins: [createStoreMixin(ArticleStore)],
propTypes: {
articleId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
article: ArticleStore.get(this.props.articleId);
}
},
render() {
var article = this.state.article,
userId = article.userId;
return (
<div>
<UserLink userId={userId}>
<UserAvatar userId={userId} />
</UserLink>
<h1>{article.title}</h1>
<p>{article.text}</p>
<p>Read more by <UserLink userId={userId} />.</p>
</div>
)
}
});
var UserAvatar = React.createClass({
mixins: [createStoreMixin(UserStore)],
propTypes: {
userId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
user: UserStore.get(this.props.userId);
}
},
render() {
var user = this.state.user;
return (
<img src={user.thumbnailUrl} />
)
}
});
var UserLink = React.createClass({
mixins: [createStoreMixin(UserStore)],
propTypes: {
userId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
user: UserStore.get(this.props.userId);
}
},
render() {
var user = this.state.user;
return (
<Link to=''user'' params={{ userId: this.props.userId }}>
{this.props.children || user.name}
</Link>
)
}
});
Desventajas de este enfoque:
- Es frustrante tener componentes 100s que se suscriban potencialmente a Tiendas;
- Es difícil hacer un seguimiento de cómo se actualizan los datos y en qué orden porque cada componente recupera sus datos de forma independiente;
- Aunque es posible que ya tenga una entidad en estado, se le obliga a pasar su ID a los hijos, quienes la recuperarán nuevamente (o si no, romperán la consistencia).
Todos los datos se leen una vez en el nivel superior y se pasan a los componentes
Cuando estaba cansado de rastrear errores, traté de poner toda la recuperación de datos en el nivel superior. Esto, sin embargo, resultó imposible porque para algunas entidades tengo varios niveles de anidación.
Por ejemplo:
- Una
Category
contieneUserAvatar
de personas que contribuyen a esa categoría; - Un
Article
puede tener variasCategory
s.
Por lo tanto, si quisiera recuperar todos los datos de las tiendas en el nivel de un Article
, tendría que:
- Recuperar artículo de
ArticleStore
; - Recupere todas las categorías de artículos de
CategoryStore
; -
UserStore
porUserStore
los contribuyentes de cada categoría deUserStore
; - De alguna manera pasa toda esa información a los componentes.
Aún más frustrante, siempre que necesito una entidad profundamente anidada, necesitaría agregar código a cada nivel de anidación para pasarlo.
Resumiendo
Ambos enfoques parecen defectuosos. ¿Cómo resuelvo este problema de la manera más elegante?
Mis objetivos:
Las tiendas no deberían tener una cantidad insana de suscriptores. Es estúpido que cada
UserLink
escucheUserStore
si los componentes principales ya lo hacen.Si el componente padre ha recuperado algún objeto de la tienda (por ejemplo, el
user
), no quiero que ningún componente anidado tenga que volver a buscarlo. Debería poder pasarlo a través de accesorios.No debería tener que buscar todas las entidades (incluidas las relaciones) en el nivel superior porque complicaría agregar o eliminar relaciones. No quiero presentar nuevos accesorios en todos los niveles de anidación cada vez que una entidad anidada obtiene una nueva relación (por ejemplo, la categoría obtiene un
curator
).
El enfoque al que llegué es hacer que cada componente reciba sus datos (no ID) como un accesorio. Si algún componente anidado necesita una entidad relacionada, depende del componente padre recuperarla.
En nuestro ejemplo, el Article
debe tener un elemento de article
que sea un objeto (presumiblemente recuperado por ArticleList
o ArticlePage
).
Dado que Article
también desea representar UserLink
y UserAvatar
para el autor del artículo, se suscribirá a UserStore
y mantendrá author: UserStore.get(article.authorId)
en su estado. Luego renderizará UserLink
y UserAvatar
con this this.state.author
. Si desean transmitirlo más, pueden hacerlo. Ningún componente secundario necesitará recuperar este usuario nuevamente.
Reiterar:
- Ningún componente recibe ID como un accesorio; todos los componentes reciben sus respectivos objetos.
- Si los componentes secundarios necesitan una entidad, es responsabilidad de los padres recuperarla y pasarla como un accesorio.
Esto resuelve mi problema bastante bien. Ejemplo de código reescrito para usar este enfoque:
var Article = React.createClass({
mixins: [createStoreMixin(UserStore)],
propTypes: {
article: PropTypes.object.isRequired
},
getStateFromStores() {
return {
author: UserStore.get(this.props.article.authorId);
}
},
render() {
var article = this.props.article,
author = this.state.author;
return (
<div>
<UserLink user={author}>
<UserAvatar user={author} />
</UserLink>
<h1>{article.title}</h1>
<p>{article.text}</p>
<p>Read more by <UserLink user={author} />.</p>
</div>
)
}
});
var UserAvatar = React.createClass({
propTypes: {
user: PropTypes.object.isRequired
},
render() {
var user = this.props.user;
return (
<img src={user.thumbnailUrl} />
)
}
});
var UserLink = React.createClass({
propTypes: {
user: PropTypes.object.isRequired
},
render() {
var user = this.props.user;
return (
<Link to=''user'' params={{ userId: this.props.user.id }}>
{this.props.children || user.name}
</Link>
)
}
});
Esto mantiene los componentes más internos estúpidos, pero no nos obliga a complicar el infierno de los componentes de nivel superior.
La mayoría de las personas comienzan escuchando las tiendas relevantes en un componente de vista de controlador cerca de la parte superior de la jerarquía.
Más tarde, cuando parezca que muchos accesorios irrelevantes se transmiten a través de la jerarquía a algún componente profundamente anidado, algunas personas decidirán que es una buena idea dejar que un componente más profundo escuche los cambios en las tiendas. Esto ofrece una mejor encapsulación del dominio del problema del que trata esta rama más profunda del árbol de componentes. Hay buenos argumentos para hacer juiciosamente.
Sin embargo, prefiero escuchar siempre en la parte superior y simplemente pasar todos los datos. Algunas veces incluso tomaré todo el estado de la tienda y la pasaré a través de la jerarquía como un solo objeto, y lo haré para múltiples tiendas. Así que tendría un apoyo para el estado de la UserStore
ArticleStore
, y otro para el UserStore
la UserStore
de UserStore
, etc. Considero que evitar las vistas de controlador profundamente anidadas mantiene un punto de entrada singular para los datos y unifica el flujo de datos. De lo contrario, tengo varias fuentes de datos, y esto puede ser difícil de depurar.
La comprobación de tipos es más difícil con esta estrategia, pero puede configurar una "forma" o tipo de plantilla para el objeto grande como prop con los PropTypes de React. Ver: https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop-validation
Tenga en cuenta que es posible que desee establecer la lógica de asociación de datos entre las tiendas en las propias tiendas. Por lo tanto, su ArticleStore
podría waitFor()
la UserStore
e incluir a los usuarios relevantes con cada registro de Article
que proporcione a través de getArticles()
. Hacer esto en tus vistas suena como empujar la lógica a la capa de vista, que es una práctica que debes evitar siempre que sea posible.
También podría estar tentado de usar transferPropsTo()
, y a muchas personas les gusta hacer esto, pero prefiero mantener todo explícito para que sea legible y, por lo tanto, mantenible.
FWIW, entiendo que David Nolen adopta un enfoque similar con su Om framework (que es algo compatible con Flux ) con un único punto de entrada de datos en el nodo raíz; el equivalente en Flux sería tener solo una vista de controlador. escuchando todas las tiendas. Esto se hace eficiente mediante el uso de shouldComponentUpdate()
y las estructuras de datos inmutables que se pueden comparar por referencia, con ===. Para estructuras de datos inmutables, revisa el mori de David o el immutable-js Facebook. Mi conocimiento limitado de Om proviene principalmente de The Future of JavaScript MVC Frameworks
Los problemas descritos en sus 2 filosofías son comunes a cualquier aplicación de una sola página.
Se analizan brevemente en este video: https://www.youtube.com/watch?v=IrgHurBjQbg y Relay ( https://facebook.github.io/relay ) fue desarrollado por Facebook para superar la compensación que usted describe.
El enfoque de Relay está muy centrado en los datos. Es una respuesta a la pregunta "¿Cómo obtengo solo los datos necesarios para cada componente en esta vista en una consulta al servidor?" Y al mismo tiempo, Relay se asegura de que haya poco acoplamiento en el código cuando se usa un componente en varias vistas.
Si Relay no es una opción , "Todos los componentes de la entidad leen sus propios datos" me parece un mejor enfoque para la situación que describes. Creo que la idea errónea en Flux es qué es una tienda. El concepto de tienda no existe para ser el lugar donde se guardan un modelo o una colección de objetos. Las tiendas son lugares temporales donde su aplicación coloca los datos antes de que se visualice la vista. La verdadera razón por la que existen es para resolver el problema de las dependencias a través de los datos que se encuentran en diferentes tiendas.
Lo que Flux no especifica es cómo una tienda se relaciona con el concepto de modelos y colección de objetos (a la Backbone). En ese sentido, algunas personas están creando una tienda flux en un lugar donde colocar colecciones de objetos de un tipo específico que no está al ras durante todo el tiempo que el usuario mantiene el navegador abierto pero, como entiendo el flujo, eso no es lo que una tienda supuestamente es.
La solución es tener otra capa donde usted esté donde se almacenan y mantienen actualizadas las entidades necesarias para representar su vista (y potencialmente más). Si esta capa que resume modelos y colecciones, no es un problema si los subcomponentes tienen que consultar de nuevo para obtener sus propios datos.
Mi solución es mucho más simple. Cada componente que tiene su propio estado puede hablar y escuchar en las tiendas. Estos son componentes muy similares a controladores. No se permiten componentes anidados más profundos que no mantienen el estado sino que simplemente procesan material. Solo reciben accesorios para renderizado puro, muy similares a las vistas.
De esta forma, todo fluye desde componentes con estado a componentes sin estado. Mantener baja la cuenta de estado.
En su caso, el artículo sería explícito y, por lo tanto, las conversaciones con las tiendas y UserLink, etc. solo se procesarían para que reciba article.user como prop.