Actualmente trabajando en un proyecto react + redux.

También estoy usando normalizr para manejar la estructura de datos y reselect para recopilar los datos correctos para los componentes de la aplicación.

Todo parece estar funcionando bien.

Me encuentro en una situación en la que un componente en forma de hoja necesita datos de la tienda y, por lo tanto, necesito connect() el componente para implementarlo.

Como ejemplo simplificado, imagine que la aplicación es un sistema de edición de libros con múltiples usuarios reuniendo comentarios.

Book
    Chapters
        Chapter
            Comments
        Comments
    Comments

En diferentes niveles de la aplicación, los usuarios pueden contribuir al contenido y / o proporcionar comentarios.

Considere que estoy representando un Capítulo, tiene contenido (y un autor) y comentarios (cada uno con su propio contenido y autor).

Actualmente quisiera connect() y reselect el contenido del capítulo basado en la ID.

Debido a que la base de datos está normalizada con normalizr, en realidad solo obtengo los campos de contenido básicos del capítulo y la identificación de usuario del autor.

Para representar los comentarios, usaría un componente conectado que puede volver a seleccionar los comentarios vinculados al capítulo, y luego representar cada componente de comentario individualmente.

Nuevamente, debido a que la base de datos está normalizada con normalizr, realmente solo obtengo el contenido básico y la identificación de usuario del autor del comentario.

Ahora, para renderizar algo tan simple como una insignia de autor, necesito usar otro componente conectado para obtener los detalles del usuario de la ID de usuario que tengo (tanto al representar el autor del capítulo como para cada autor de comentario individual).

El componente sería algo simple como esto:

@connect(
    createSelector(
        (state) => state.entities.get('users'),
        (state,props) => props.id,
        (users,id) => ( { user:users.get(id)})
    )
)
class User extends Component {

    render() {
        const { user } = this.props

        if (!user)
            return null
        return <div className='user'>
                    <Avatar name={`${user.first_name} ${user.last_name}`} size={24} round={true}  />
                </div>

    }
}

User.propTypes = {
    id : PropTypes.string.isRequired
}

export default User

Y aparentemente funciona bien.

He intentado hacer lo contrario y volver a normalizar los datos a un nivel superior para que, por ejemplo, los datos de los capítulos incorporen los datos del usuario directamente, en lugar de solo la ID del usuario, y se los pasen directamente al Usuario, pero eso solo parecía que solo hacía selectores realmente complicados, y debido a que mis datos son inmutables, simplemente recrea objetos cada vez.

Entonces, mi pregunta es, ¿tiene un componente en forma de hoja (como el Usuario anterior) connect() en la tienda para mostrar un signo de antipatrón?

¿Estoy haciendo lo correcto o lo veo de manera incorrecta?

5
Ben 9 may. 2016 a las 09:09

3 respuestas

La mejor respuesta

Creo que tu intuición es correcta. No hay nada de malo en conectar componentes a cualquier nivel (incluidos los nodos hoja), siempre que la API tenga sentido, es decir, dados algunos accesorios, puede razonar sobre la salida del componente.

La noción de componentes inteligentes vs tontos está un poco desactualizada. Más bien, es mejor pensar en componentes conectados y no conectados. Al considerar si crea componentes conectados o no conectados, hay algunas cosas a tener en cuenta.

Límites del módulo

Si divide su aplicación en módulos más pequeños, generalmente es mejor restringir sus interacciones a una superficie API pequeña. Por ejemplo, digamos que users y comments están en módulos separados, entonces diría que tiene más sentido que el componente <Comment> use un componente <User id={comment.userId}/> conectado en lugar de tenerlo tomar los datos del usuario por sí mismo.

Principio de responsabilidad única

Un componente conectado que tiene demasiada responsabilidad es un olor a código. Por ejemplo, la responsabilidad del componente <Comment> puede ser obtener datos de comentarios y representarlos, y manejar la interacción del usuario (con el comentario) en forma de despachos de acciones. Si necesita manejar capturar datos de usuario y manejar interacciones con el módulo de usuario, entonces está haciendo demasiado. Es mejor delegar responsabilidades relacionadas a otro componente conectado.

Esto también se conoce como el problema del "controlador gordo".

Actuación

Al tener un gran componente conectado en la parte superior que transmite los datos, en realidad impacta negativamente en el rendimiento. Esto se debe a que cada cambio de estado actualizará la referencia de nivel superior, luego cada componente se volverá a representar y React necesitará realizar una reconciliación para todos los componentes.

Redux optimiza los componentes conectados asumiendo que son puros (es decir, si las referencias de apoyo son las mismas, omita el re-renderizado). Si conecta los nodos hoja, entonces un cambio en el estado solo volverá a representar los nodos hoja afectados, omitiendo mucha reconciliación. Esto se puede ver en acción aquí: https://github.com /mweststrate/redux-todomvc/blob/master/components/TodoItem.js

Reutilización y comprobabilidad

Lo último que quiero mencionar es reutilizar y probar. Un componente conectado no es reutilizable si necesita 1) conectarlo a otra parte del átomo de estado, 2) pasar los datos directamente (por ejemplo, ya tengo datos user, por lo que solo quiero un render puro). En el mismo token, los componentes conectados son más difíciles de probar porque primero debe configurar su entorno antes de poder renderizarlos (por ejemplo, crear tienda, pasar tienda a <Provider>, etc.).

Este problema puede mitigarse exportando componentes conectados y no conectados en lugares donde tengan sentido.

export const Comment = ({ comment }) => (
  <p>
    <User id={comment.userId}/>
   { comment.text }
  </p>
)

export default connect((state, props) => ({
  comment: state.comments[props.id]
}))(Comment)


// later on...
import Comment, { Comment as Unconnected } from './comment'
10
jay_soo 10 may. 2016 a las 22:06

Redux sugiere que solo conecte sus contenedores de nivel superior a la tienda. Puede pasar todos los accesorios que desee para las hojas de los contenedores. De esta manera, es más fácil rastrear el flujo de datos.

Esto es solo una cuestión de preferencia personal, no hay nada de malo en conectar un componente en forma de hoja a la tienda, solo agrega algo de complejidad a su flujo de datos, lo que aumenta la dificultad de depuración.

Si descubres que en tu aplicación, es mucho más fácil conectar un componente en forma de hoja a la tienda, entonces te sugiero que lo hagas. Pero no debería suceder muy a menudo.

0
Kevin He 9 may. 2016 a las 08:03

Estoy de acuerdo con @Kevin Él responde que no es realmente un antipatrón, pero generalmente hay mejores enfoques que hacen que sus datos Flujo más fácil de rastrear.

Para lograr lo que está buscando sin conectar sus componentes en forma de hoja, puede ajustar sus selectores para obtener conjuntos de datos más completos. Por ejemplo, para su componente contenedor <Chapter/>, puede usar lo siguiente:

export const createChapterDataSelector = () => {
  const chapterCommentsSelector = createSelector(
    (state) => state.entities.get('comments'),
    (state, props) => props.id,
    (comments, chapterId) => comments.filter((comment) => comment.get('chapterID') === chapterId)
  )

  return createSelector(
    (state, props) => state.entities.getIn(['chapters', props.id]),
    (state) => state.entities.get('users'),
    chapterCommentsSelector,
    (chapter, users, chapterComments) => I.Map({
      title: chapter.get('title'),
      content: chapter.get('content')
      author: users.get(chapter.get('author')),
      comments: chapterComments.map((comment) => I.Map({
        content: comment.get('content')
        author: users.get(comment.get('author'))
      }))
    })
  )
}

Este ejemplo utiliza una función que devuelve un selector específicamente para una ID de capítulo dada para que cada componente <Chapter /> obtenga su propio selector memorizado, en caso de que tenga más de uno. (Varios componentes <Chapter /> diferentes que comparten el mismo selector arruinarían la memorización). También he dividido chapterCommentsSelector en un selector de reselección separado para que se memorice, porque transforma (filtra, en este caso) los datos del estado.

En su componente <Chapter />, puede llamar a createChapterDataSelector(), que le dará un selector que proporciona un Mapa inmutable que contiene todos los datos que necesitará para ese <Chapter /> y todos sus descendientes . Entonces puedes simplemente pasar los accesorios normalmente.

Dos de los principales beneficios de pasar los accesorios de la forma React normal son el flujo de datos rastreable y la reutilización de componentes. Un componente <Comment /> al que se le pasa 'contenido', 'authorName' y 'authorAvatar' es fácil de entender y usar. Puede usarlo en cualquier lugar de su aplicación en el que desee mostrar un comentario. Imagine que su aplicación muestra una vista previa de un comentario mientras se está escribiendo. Con un componente "tonto", esto es trivial. Pero si su componente requiere una entidad coincidente en su tienda Redux, eso es un problema porque ese comentario puede no existir en la tienda todavía si todavía se está escribiendo.

Sin embargo, puede llegar un momento en que tenga más sentido para connect() componentes más adelante en la línea. Un caso sólido para esto sería si descubres que estás pasando una tonelada de accesorios a través de componentes de intermediarios que no los necesitan, solo para llevarlos a su destino final.

De los documentos de Redux:

Intente mantener sus componentes de presentación separados. Cree componentes de contenedor conectándolos cuando sea conveniente. Siempre que sienta que está duplicando código en componentes principales para proporcionar datos para el mismo tipo de hijos, es hora de extraer un contenedor. En general, tan pronto como sienta que un padre sabe demasiado acerca de los datos "personales" o las acciones de sus hijos, es hora de extraer un contenedor. En general, intente encontrar un equilibrio entre el flujo de datos comprensible y las áreas de responsabilidad con sus componentes.

El enfoque recomendado parece ser comenzar con menos componentes de contenedor conectados, y luego solo extraer más contenedores cuando sea necesario.

1
Community 23 may. 2017 a las 12:08