¿A qué nivel de anidamiento deben leer los componentes las entidades de las tiendas en Flux?

89

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 muchos. Algunos de ellos son grandes ( Article), otros son pequeños y simples ( UserAvatar, UserLink).

He estado luchando con el lugar de la jerarquía de componentes en el que debería leer los datos de las tiendas.
Probé dos enfoques extremos, ninguno de los cuales me gustó del todo:

Todos los componentes de la entidad leen sus propios datos

Cada componente que necesita algunos datos de la Tienda recibe solo el ID de entidad y recupera la entidad por sí misma.
Por ejemplo, Articlese pasa articleId, UserAvatary UserLinkse pasan userId.

Este enfoque tiene varias desventajas importantes (discutidas en el ejemplo de 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 de 100 que potencialmente se suscriban a las 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 ve obligado a pasar su ID a los niños, quienes la recuperarán nuevamente (o romperán la consistencia).

Todos los datos se leen una vez en el nivel superior y se transmiten a los componentes

Cuando estaba cansado de rastrear errores, traté de poner toda la recuperación de datos en el nivel superior. Sin embargo, esto resultó imposible porque para algunas entidades tengo varios niveles de anidación.

Por ejemplo:

  • A Categorycontiene UserAvatars de personas que contribuyen a esa categoría;
  • An Articlepuede tener varios Categorys.

Por lo tanto, si quisiera recuperar todos los datos de las tiendas al nivel de an Article, necesitaría:

  • Recuperar artículo de ArticleStore;
  • Recuperar todas las categorías de artículos de CategoryStore;
  • Recupere por separado los contribuyentes de cada categoría de UserStore;
  • De alguna manera, transfiera todos esos datos a los componentes.

Aún más frustrante, siempre que necesite una entidad profundamente anidada, necesitaría agregar código a cada nivel de anidación para transmitirlo adicionalmente.

Resumiendo

Ambos enfoques parecen defectuosos. ¿Cómo soluciono este problema de la manera más elegante?

Mis objetivos:

  • Las tiendas no deberían tener una cantidad increíble de suscriptores. Es estúpido que cada uno UserLinkescuche UserStoresi los componentes principales ya lo hacen.

  • Si el componente principal ha recuperado algún objeto de la tienda (por ejemplo user), no quiero que ningún componente anidado tenga que recuperarlo de nuevo. 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 introducir 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 una curator).

Dan Abramov
fuente
1
Gracias por publicar esto. Acabo de publicar una pregunta muy similar aquí: groups.google.com/forum/… Todo lo que he visto ha tratado con estructuras de datos planas en lugar de datos asociados. Me sorprende no ver ninguna de las mejores prácticas al respecto.
uberllama

Respuestas:

37

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 una gran cantidad de 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 hacerlo con prudencia.

Sin embargo, prefiero escuchar siempre en la parte superior y simplemente transmitir todos los datos. A veces, incluso tomaré el estado completo de la tienda y lo pasaré a través de la jerarquía como un solo objeto, y lo haré para varias tiendas. Entonces tendría un accesorio para elArticleStore estado de, y otro para el UserStoreestado de, etc. Encuentro que evitar 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 resultar difícil de depurar.

La verificación de tipo es más difícil con esta estrategia, pero puede configurar una "forma", o plantilla de tipo, para el objeto grande como accesorio con 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 -validación

Tenga en cuenta que es posible que desee poner la lógica de asociar datos entre tiendas en las tiendas mismas. Por lo que su ArticleStorepoder waitFor()la UserStore, e incluyen los usuarios relevantes con cada Articleregistro que proporciona a través getArticles(). Hacer esto en sus vistas suena como introducir la lógica en la capa de la vista, que es una práctica que debe evitar siempre que sea posible.

También podría tener la tentación de usar transferPropsTo() , y a muchas personas les gusta hacer esto, pero prefiero mantener todo explícito para facilitar la lectura y, por lo tanto, el mantenimiento.

FWIW, tengo entendido que David Nolen adopta un enfoque similar con su marco Om (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()estructuras de datos inmutables que se pueden comparar por referencia, con ===. Para estructuras de datos inmutables, consulte el mori de David o el inmutable-js de Facebook . Mi conocimiento limitado de Om proviene principalmente de The Future of JavaScript MVC Frameworks

Fisherwebdev
fuente
4
¡Gracias por una respuesta detallada! Consideré transmitir ambas instantáneas completas del estado de las tiendas. Sin embargo, esto podría darme un problema de rendimiento porque cada actualización de UserStore hará que todos los artículos en la pantalla se vuelvan a representar, y PureRenderMixin no podrá decir qué artículos deben actualizarse y cuáles no. En cuanto a su segunda sugerencia, también lo pensé, pero no veo cómo podría mantener la igualdad de referencia del artículo si el usuario del artículo se "inyecta" en cada llamada a getArticles (). Esto nuevamente me daría potencialmente problemas de rendimiento.
Dan Abramov
1
Veo tu punto, pero hay soluciones. Por un lado, puede registrar shouldComponentUpdate () si ha cambiado una serie de ID de usuario para un artículo, que básicamente es simplemente poner en práctica la funcionalidad y el comportamiento que realmente desea en PureRenderMixin en este caso específico.
fisherwebdev
3
Derecho, y de todos modos yo sólo tendrá que hacerlo cuando estoy seguro de que no hay problemas Potencia del cual no debería ser el caso de las tiendas que se actualizan varias veces por minuto como máximo. ¡Gracias de nuevo!
Dan Abramov
8
Dependiendo del tipo de aplicación que esté creando, mantener un único punto de entrada será doloroso tan pronto como comience a tener más "widgets" en pantalla que no pertenezcan directamente a la misma raíz. Si intenta hacerlo a la fuerza de esa manera, ese nodo raíz tendrá demasiada responsabilidad. KISS y SOLID, aunque sea del lado del cliente.
Rygu
37

El enfoque al que llegué es que cada componente reciba sus datos (no ID) como apoyo. Si algún componente anidado necesita una entidad relacionada, depende del componente principal recuperarlo.

En nuestro ejemplo, Articledebería tener un articleaccesorio que sea un objeto (presumiblemente recuperado por ArticleListo ArticlePage).

Porque Articletambién quiere renderizar UserLinky UserAvatarpara el autor del artículo, se suscribirá UserStorey mantendrá author: UserStore.get(article.authorId)en su estado. Luego renderizará UserLinky UserAvatarcon esto this.state.author. Si desean transmitirlo aún más, pueden hacerlo. Ningún componente secundario necesitará recuperar a este usuario nuevamente.

Reiterar:

  • Ningún componente recibe nunca una identificación como 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 hace que los componentes más internos sean estúpidos, pero no nos obliga a complicar mucho los componentes de alto nivel.

Dan Abramov
fuente
Solo para agregar una perspectiva a esta respuesta. Conceptualmente, puede desarrollar una noción de propiedad. Dada una recopilación de datos, finalice en la parte superior más vista quién es el propietario de los datos (por lo tanto, escucha su almacenamiento). Algunos ejemplos: 1. AuthData debe ser propiedad del Componente de la aplicación; cualquier cambio en Auth debe propagarse como accesorios a todos los elementos secundarios por el Componente de la aplicación. 2. ArticleData debe ser propiedad de ArticlePage o Article List como se sugiere en la respuesta. ahora cualquier hijo de ArticleList puede recibir AuthData y / o ArticleData de ArticleList independientemente del componente que realmente haya obtenido esos datos.
Saumitra R. Bhave
2

Mi solución es mucho más sencilla. Cada componente que tiene su propio estado puede hablar y escuchar las tiendas. Estos son componentes muy parecidos a un controlador. No se permiten componentes anidados más profundos que no mantienen el estado sino que solo renderizan cosas. Solo reciben accesorios para renderizado puro, muy parecido a una vista.

De esta manera, todo fluye de componentes con estado a componentes sin estado. Mantener el recuento de estado bajo.

En su caso, el artículo tendría estado y, por lo tanto, las conversaciones con las tiendas y UserLink, etc., solo se mostrarían para recibir article.user como prop.

Rygu
fuente
1
Supongo que esto funcionaría si el artículo tuviera una propiedad de objeto de usuario, pero en mi caso solo tiene userId (porque el usuario real está almacenado en UserStore). ¿O me estoy perdiendo tu punto? ¿Compartirías un ejemplo?
Dan Abramov
Inicie una búsqueda para el usuario en el artículo. O cambie su API backend para hacer que la identificación de usuario sea "expandible" para que obtenga el usuario de una vez. De cualquier manera, mantenga los componentes anidados más profundos en vistas simples y dependiendo solo de los accesorios.
Rygu
2
No puedo permitirme iniciar la búsqueda en el artículo porque es necesario renderizarlo en conjunto. En cuanto a las API expandibles, que se utiliza para tenerlos, pero son muy problemáticos para analizar las tiendas. Incluso escribí una biblioteca para aplanar las respuestas por esta misma razón.
Dan Abramov
0

Los problemas descritos en sus 2 filosofías son comunes a cualquier aplicación de una sola página.

Se discuten 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 un componente se usa 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 describe. Creo que la idea errónea de Flux es qué es una tienda. El concepto de tienda no existe para ser el lugar donde se guarda un modelo o una colección de objetos. Las tiendas son lugares temporales donde su aplicación coloca los datos antes de que se procese la vista. La verdadera razón por la que existen es para resolver el problema de las dependencias entre los datos que van a diferentes tiendas.

Lo que Flux no especifica es cómo se relaciona una tienda con el concepto de modelos y colección de objetos (a la Backbone). En ese sentido, algunas personas están haciendo que una tienda flux sea un lugar donde poner una colección de objetos de un tipo específico que no está al ras durante todo el tiempo que el usuario mantiene el navegador abierto pero, según tengo entendido flux, eso no es lo que una tienda supuestamente es.

La solución es tener otra capa donde se almacenan y mantienen actualizadas las entidades necesarias para representar su vista (y potencialmente más). Si tiene esta capa que abstrae modelos y colecciones, no es un problema si los subcomponentes tienen que consultar nuevamente para obtener sus propios datos.

Chris Cinelli
fuente
1
Por lo que entiendo, el enfoque de Dan de mantener diferentes tipos de objetos y hacer referencia a ellos a través de identificadores nos permite mantener el caché de esos objetos y actualizarlos solo en un lugar. ¿Cómo se puede lograr esto si, por ejemplo, tiene varios artículos que hacen referencia al mismo usuario y luego se actualiza el nombre del usuario? La única forma es actualizar todos los artículos con nuevos datos de usuario. Este es exactamente el mismo problema que surge al elegir entre la base de datos documental o relacional para su backend. Que piensas sobre eso? Gracias.
Alex Ponomarev