Redux: Recomendaciones de mejores prácticas sobre creadores de acciones, reductores y selectores

Creado en 22 dic. 2015  ·  106Comentarios  ·  Fuente: reduxjs/redux

Mi equipo ha estado usando Redux durante un par de meses. En el camino, ocasionalmente me he encontrado pensando en una característica y preguntándome "¿pertenece esto a un creador de acción o un reductor?". La documentación parece un poco vaga sobre este hecho. (O tal vez simplemente me perdí donde está cubierto, en cuyo caso me disculpo). Pero a medida que escribí más código y más pruebas, he llegado a tener opiniones más sólidas sobre dónde deberían estar las cosas y pensé que valdría la pena. compartir y discutir con otros.

Así que aquí están mis pensamientos.

Usa selectores en todas partes

Este primero no está estrictamente relacionado con Redux, pero lo compartiré de todos modos, ya que se menciona indirectamente a continuación. Mi equipo usa rackt / reselect . Por lo general, definimos un archivo que exporta selectores para un nodo determinado de nuestro árbol de estado (por ejemplo, MyPageSelectors). Nuestros contenedores "inteligentes" utilizan esos selectores para parametrizar nuestros componentes "tontos".

Con el tiempo, nos hemos dado cuenta de que hay un beneficio adicional al usar estos mismos selectores en otros lugares (no solo en el contexto de reselección). Por ejemplo, los usamos en pruebas automatizadas. También los usamos en thunks devueltos por creadores de acciones (más abajo).

Entonces, mi primera recomendación es: use selectores compartidos _en todas partes_- incluso cuando acceda sincrónicamente a los datos (por ejemplo, prefiera myValueSelector(state) sobre state.myValue ). Esto reduce la probabilidad de variables mal escritas que conducen a valores sutiles indefinidos, simplifica los cambios en la estructura de su tienda, etc.

Haz _más_ en creadores de acción y _menos_ en reductores

Creo que este es muy importante, aunque puede que no sea obvio de inmediato. La lógica empresarial pertenece a los creadores de acciones. Los reductores deben ser estúpidos y simples. En muchos casos individuales no importa, pero la consistencia es buena y, por lo tanto, es mejor hacer esto _consistentemente_. Hay un par de razones por las que:

  1. Los creadores de acciones pueden ser asincrónicos mediante el uso de middleware como redux-thunk . Dado que su aplicación a menudo requerirá actualizaciones asincrónicas en su tienda, alguna "lógica comercial" terminará en sus acciones.
  2. Los creadores de acciones (más exactamente los procesadores que devuelven) pueden usar selectores compartidos porque tienen acceso al estado completo. Los reductores no pueden porque solo tienen acceso a su nodo.
  3. Al usar redux-thunk , un único creador de acciones puede enviar múltiples acciones, lo que simplifica las actualizaciones de estado complicadas y fomenta una mejor reutilización del código.

Imagine que su estado tiene metadatos relacionados con una lista de elementos. Cada vez que se modifica, agrega o elimina un elemento de la lista, es necesario actualizar los metadatos. La "lógica empresarial" para mantener la lista y sus metadatos sincronizados podría residir en algunos lugares:

  1. En los reductores. Cada reductor (agregar, editar, eliminar) es responsable de actualizar la lista _así como_ los metadatos.
  2. En las vistas (contenedor / componente). Cada vista que invoca una acción (agregar, editar, eliminar) también es responsable de invocar una acción updateMetadata . Este enfoque es terrible por (con suerte) razones obvias.
  3. En los creadores de acción. Cada creador de acciones (agregar, editar, eliminar) devuelve un procesador que envía una acción para actualizar la lista y luego otra acción para actualizar los metadatos.

Dadas las opciones anteriores, la opción 3 es sólidamente mejor. Ambas opciones 1 y 3 admiten el uso compartido de código limpio, pero solo la opción 3 admite el caso en el que las actualizaciones de listas y / o metadatos pueden ser asincrónicas. (Por ejemplo, tal vez se base en un trabajador web).

Escriba pruebas de "patos" que se centren en acciones y selectores

La forma más eficiente de probar acciones, reductores y selectores es seguir el enfoque de "patos" al escribir pruebas. Esto significa que debe escribir un conjunto de pruebas que cubra un conjunto determinado de acciones, reductores y selectores en lugar de 3 conjuntos de pruebas que se centren en cada uno de forma individual. Esto simula con mayor precisión lo que sucede en su aplicación real y proporciona el máximo rendimiento.

Al desglosarlo aún más, descubrí que es útil escribir pruebas que se centren en los creadores de acciones y luego verificar el resultado mediante selectores. (No pruebe directamente los reductores). Lo que importa es que una acción determinada dé como resultado el estado que espera. Verificar este resultado usando sus selectores (compartidos) es una forma de cubrir los tres en una sola pasada.

discussion

Comentario más útil

@dtinth @ denis-sokolov También estoy de acuerdo contigo en eso. Por cierto, cuando estaba haciendo referencia al proyecto redux-saga , tal vez no dejé en claro que estoy en contra de la idea de hacer que los creadores de acción crezcan y sean cada vez más complejos con el tiempo.

El proyecto Redux-saga también es un intento de hacer lo que estás describiendo @dtinth, pero hay una sutil diferencia con lo que ambos dicen. Parece que quiere decir que si escribe todos los eventos sin procesar que sucedieron en el registro de acciones, puede calcular fácilmente cualquier estado a partir de los reductores de este registro de acciones. Esto es absolutamente cierto, y he seguido ese camino durante un tiempo hasta que mi aplicación se volvió muy difícil de mantener porque el registro de acciones no se volvió explícito y los reductores fueron demasiado complejos con el tiempo.

Tal vez pueda mirar este punto de la discusión original que condujo a la discusión de la saga Redux: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Caso de uso para resolver

Imagina que tienes una aplicación Todo, con el evento TodoCreated obvio. Luego le pedimos que codifique la incorporación de una aplicación. Una vez que el usuario crea una tarea, debemos felicitarlo con una ventana emergente.

El camino "impuro":

Esto es lo que parece preferir @bvaughn

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

No me gusta este enfoque porque hace que el creador de la acción esté muy acoplado al diseño de la vista de la aplicación. Se asume que actionCreator debe conocer la estructura del árbol de estado de la interfaz de usuario para tomar su decisión.

La forma de "calcular todo desde eventos sin procesar":

Esto es lo que parece preferir @ denis-sokolov @dtinth :

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

Sí, puede crear un reductor que sepa si se debe mostrar la felicitación. Pero luego tiene una ventana emergente que se mostrará sin siquiera una acción que diga que la ventana emergente se ha mostrado. En mi propia experiencia haciendo eso (y todavía tengo código heredado haciendo eso) siempre es mejor hacerlo muy explícito: NUNCA muestre la ventana emergente de felicitación si no se activa ninguna acción DISPLAY_CONGRATULATION. Explícito es mucho más fácil de mantener que implícito.

La forma simplificada de la saga.

La saga redux usa generadores y puede parecer un poco complicado si no está acostumbrado, pero básicamente con una implementación simplificada escribiría algo como:

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

La saga es un actor con estado que recibe eventos y puede producir efectos. Aquí se implementa como un reductor impuro para darte una idea de lo que es, pero en realidad no está en el proyecto redux-saga.

Complicando un poco las reglas:

Si cuidas la regla inicial, no es muy explícito en todo.
Si observa las implementaciones anteriores, notará que la ventana emergente de felicitación se abre cada vez que creamos una tarea durante la incorporación. Lo más probable es que queramos que se abra solo para el primer todo creado que ocurre durante la incorporación y no para todos. Además, queremos permitir que el usuario eventualmente rehaga la incorporación desde el principio.

¿Puedes ver cómo el código se volvería complicado en las 3 implementaciones con el tiempo a medida que la incorporación se vuelve cada vez más complicada?

El camino de redux-saga

Con redux-saga y las reglas de incorporación anteriores, escribirías algo como

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

Creo que resuelve este caso de uso de una manera mucho más simple que las soluciones anteriores. Si me equivoco, dame tu implementación más simple :)

Hablaste de código impuro, y en este caso no hay impureza en la implementación de Redux-saga porque los efectos take / put son en realidad datos. Cuando se llama a take () no se ejecuta, devuelve un descriptor del efecto a ejecutar, y en algún momento se activa un intérprete, por lo que no necesitas ningún simulacro para probar las sagas. Si eres un desarrollador funcional que hace Haskell, piensa en mónadas Free / IO.


En este caso permite:

  • Evita complicar actionCreator y haz que dependa de getState
  • Haz lo implícito más explícito
  • Evite acoplar la lógica transversal (como la incorporación anterior) a su dominio empresarial principal (creación de tareas pendientes)

También puede proporcionar una capa de interpretación, lo que permite traducir eventos sin procesar en eventos más significativos / de alto nivel (un poco como lo hace ELM al envolver los eventos a medida que surgen).

Ejemplos:

  • "TIMELINE_SCROLLED_NEAR-BOTTOM" podría conducir a "NEXT_PAGE_LOADED"
  • "REQUEST_FAILED" podría conducir a "USER_DISCONNECTED" si el código de error es 401.
  • "HASHTAG_FILTER_ADDED" podría generar "CONTENT_RELOADED"

Si desea lograr un diseño de aplicación modular con patos, puede evitar que los patos se junten. La saga se convierte en el punto de unión. Los patos solo tienen que saber de sus eventos crudos, y la saga interpreta estos eventos crudos. Esto es mucho mejor que tener duck1 enviando acciones directamente de duck2 porque hace que el proyecto duck1 sea más fácil de reutilizar en otro contexto. Sin embargo, se podría argumentar que el punto de acoplamiento también podría estar en actionCreators y esto es lo que la mayoría de la gente está haciendo hoy.

Todos 106 comentarios

Es curioso si usa Immutable.js u otro. En el puñado de cosas redux que he construido, no podía imaginarme no usar inmutable, pero _ sí_ tengo una estructura bastante profundamente anidada que Immutable ayuda a domar.

Guau. Qué descuido para mí _no_ mencionar eso. ¡Sí! Usamos Immutable! Como dices, es difícil imaginar _no_ usarlo para algo sustancial.

@bvaughn Un área con la que he luchado es dónde trazar la línea entre Immutable y los componentes. Pasar objetos inmutables a los componentes le permite usar decoradores / mixins de renderizado puro muy fácilmente, pero luego termina con un código IMmutable en sus componentes (que no me gusta). Hasta ahora he cedido y hecho eso, pero sospecho que usa selectores en los métodos render () en lugar de acceder directamente a los métodos de Immutable.js.

Para ser honesto, esto es algo sobre lo que aún no hemos definido una política estricta. A menudo usamos selectores en nuestros contenedores "inteligentes" para valores nativos extra de nuestros objetos inmutables y luego pasamos los valores nativos a nuestros componentes como cadenas, booleanos, etc. Ocasionalmente pasamos un objeto inmutable pero cuando lo hacemos, casi siempre pasar un tipo Record para que el componente pueda tratarlo como un objeto nativo (con getters).

Me he movido en la dirección opuesta, haciendo que los creadores de acción sean más triviales. Pero realmente estoy comenzando con redux. Algunas preguntas sobre su enfoque:

1) ¿Cómo pruebas a tus creadores de acciones? Me gusta mover tanta lógica como sea posible a funciones puras y sincrónicas que no dependen de servicios externos porque es más fácil de probar.
2) ¿Utiliza el viaje en el tiempo con recarga en caliente? Una de las cosas interesantes de react redux devtools es que cuando se configura la recarga en caliente, la tienda volverá a ejecutar todas las acciones contra el nuevo reductor. Si tuviera que trasladar mi lógica a los creadores de acción, lo perdería.
3) Si sus creadores de acciones se envían varias veces para producir un efecto, ¿significa eso que su estado está brevemente en un estado inválido? (Estoy pensando aquí en varios envíos sincrónicos, no en los que se envían de forma asincrónica en un momento posterior)

Usa selectores en todas partes

Sí, eso parece decir que sus reductores son un detalle de implementación de su estado y que expone su estado a su componente a través de una API de consulta.
Como cualquier interfaz, permite desacoplar y facilitar la refactorización del estado.

Utilice ImmutableJS

En mi opinión, con la nueva sintaxis JS ya no es tan útil usar ImmutableJS, ya que puede modificar fácilmente listas y objetos con JS normal. A menos que tenga listas y objetos muy grandes con muchas propiedades y necesite compartir estructuras por razones de rendimiento, ImmutableJS no es un requisito estricto.

Haga más en acción

@bvaughn realmente deberías mirar este proyecto: https://github.com/yelouafi/redux-saga
Cuando comencé a discutir sobre sagas (inicialmente concepto de backend) con @yelouafi , fue para resolver este tipo de problema. En mi caso, primero intenté usar sagas mientras conectaba a un usuario que se estaba incorporando a una aplicación existente.

1) ¿Cómo pruebas a tus creadores de acciones? Me gusta mover tanta lógica como sea posible a funciones puras y sincrónicas que no dependen de servicios externos porque es más fácil de probar.

Traté de describir esto arriba, pero básicamente ... creo que tiene más sentido (para mí hasta ahora) probar a tus creadores de acción con un enfoque similar al de los "patos". Comience una prueba enviando el resultado de un creador de acciones y luego verifique el estado usando selectores. De esta manera, con una sola prueba, puede cubrir el creador de la acción, su (s) reductor (es) y todos los selectores relacionados.

2) ¿Utiliza el viaje en el tiempo con recarga en caliente? Una de las cosas interesantes de react redux devtools es que cuando se configura la recarga en caliente, la tienda volverá a ejecutar todas las acciones contra el nuevo reductor. Si tuviera que trasladar mi lógica a los creadores de acción, lo perdería.

No, no usamos viajes en el tiempo. Pero, ¿por qué tendría algún impacto aquí la lógica de su negocio al estar en un creador de acciones? Lo único que actualiza el estado de su aplicación son sus reductores. Y así, volver a ejecutar las acciones creadas lograría el mismo resultado de cualquier manera.

3) Si sus creadores de acciones se envían varias veces para producir un efecto, ¿significa eso que su estado está brevemente en un estado inválido? (Estoy pensando aquí en varios envíos sincrónicos, no en los que se envían de forma asincrónica en un momento posterior)

El estado no válido transitorio es algo que realmente no se puede evitar en algunos casos. Siempre que haya una coherencia eventual, generalmente no es un problema. Y nuevamente, su estado podría ser temporalmente inválido independientemente de que su lógica comercial esté entre los creadores de acciones o los reductores. Tiene más que ver con los efectos secundarios y las características específicas de su tienda.

En mi opinión, con la nueva sintaxis JS ya no es tan útil usar ImmutableJS, ya que puede modificar fácilmente listas y objetos con JS normal. A menos que tenga listas y objetos muy grandes con muchas propiedades y necesite compartir estructuras por razones de rendimiento, ImmutableJS no es un requisito estricto.

Las razones principales para usar Immutable (en mi opinión) no son el rendimiento o el azúcar sintáctico para las actualizaciones. La razón principal es que evita que usted (u otra persona) _accidentalmente_ mute su estado entrante dentro de un reductor. Eso es un no-no y desafortunadamente es fácil de hacer con objetos JS simples.

@bvaughn realmente deberías mirar este proyecto: https://github.com/yelouafi/redux-saga
Cuando comencé a discutir sobre sagas (inicialmente concepto de backend) con @yelouafi , fue para resolver este tipo de problema. En mi caso, primero intenté usar sagas mientras conectaba a un usuario que se estaba incorporando a una aplicación existente.

De hecho, he revisado ese proyecto antes :) Aunque todavía no lo he usado. Se ve bien.

Traté de describir esto arriba, pero básicamente ... creo que tiene más sentido (para mí hasta ahora) probar a tus creadores de acción con un enfoque similar al de los "patos". Comience una prueba enviando el resultado de un creador de acciones y luego verifique el estado usando selectores. De esta manera, con una sola prueba, puede cubrir el creador de la acción, su (s) reductor (es) y todos los selectores relacionados.

Lo siento, tengo esa parte. Lo que me preguntaba es la parte de las pruebas que interactúa con la asincronicidad. Podría escribir una prueba como esta:

var store = createStore();
store.dispatch(actions.startRequest());
store.dispatch(actions.requestResponseReceived({...});
strictEqual(isLoaded(store.getState());

Pero, ¿cómo es tu prueba? ¿Algo como esto?

var mock = mockFetch();
store.dispatch(actions.request());
mock.expect("/api/foo.bar").andRespond("{status: OK}");
strictEqual(isLoaded(store.getState());

No, no usamos viajes en el tiempo. Pero, ¿por qué tendría algún impacto aquí la lógica de su negocio al estar en un creador de acciones? Lo único que actualiza el estado de su aplicación son sus reductores. Y así, volver a ejecutar las acciones creadas lograría el mismo resultado de cualquier manera.

¿Y si se cambia el código? Si cambio el reductor, se repiten las mismas acciones pero con el nuevo reductor. Mientras que si cambio el creador de la acción, las nuevas versiones no se reproducirán. Entonces, para considerar dos escenarios:

Con reductor:

1) Intento una acción en mi aplicación.
2) Hay un error en mi reductor, lo que resulta en un estado incorrecto.
3) Arreglo el error en el reductor y guardo
4) El viaje en el tiempo carga el nuevo reductor y me pone en el estado en el que debería haber estado.

Mientras que con un creador de acción

1) Intento una acción en mi aplicación.
2) Hay un error en el creador de la acción, lo que resulta en la creación de una acción incorrecta.
3) Arreglo el error en el creador de acciones y guardo
4) Todavía estoy en el estado incorrecto, lo que requerirá que al menos intente la acción nuevamente, y posiblemente actualice si me pone en un estado completamente roto.

El estado no válido transitorio es algo que realmente no se puede evitar en algunos casos. Siempre que haya una coherencia eventual, generalmente no es un problema. Y nuevamente, su estado podría ser temporalmente inválido independientemente de que su lógica comercial esté entre los creadores de acciones o los reductores. Tiene más que ver con los efectos secundarios y las características específicas de su tienda.

Supongo que mi forma de pensar en redux insiste en que la tienda siempre está en un estado válido. El reductor siempre toma un estado válido y produce un estado válido. ¿Qué casos cree que obliga a uno a permitir algunos estados inconsistentes?

Disculpe la interrupción, pero ¿qué quiere decir con estados inválidos y válidos?
¿aquí? Los datos que se están cargando o la acción que se está ejecutando, pero aún no han terminado, se ven
como un estado transitorio válido para mí.

¿Qué quieres decir con estado transitorio en Redux @bvaughn y @sompylasar ? O se acaba el despacho o se tira. Si arroja, entonces el estado no cambia.

A menos que su reductor tenga problemas de código, Redux solo tiene estados que son consistentes con la lógica del reductor. De alguna manera, todas las acciones enviadas se manejan en una transacción: o se actualiza todo el árbol o el estado no cambia en absoluto.

Si todo el árbol se actualiza pero no de una manera adecuada (como un estado que React no puede representar), es solo que no ha hecho su trabajo correctamente :)

En Redux, el estado actual es considerar que un solo envío es un límite de transacción.

Sin embargo, entiendo la preocupación de @winstonewert que parece querer enviar 2 acciones sincrónicamente en una misma transacción. Porque a veces, los actionCreators envían múltiples acciones y esperan que todas las acciones se ejecuten correctamente. Si se envían 2 acciones y luego la segunda falla, solo se aplicará la primera, lo que conducirá a un estado que podríamos considerar "inconsistente". Tal vez @winstonewert quiera que si el envío de la segunda acción falla, entonces revertimos las 2 acciones.

@winstonewert He implementado algo así en nuestro marco interno aquí y funciona bien hasta ahora: https://github.com/stample/atom-react/blob/master/src/atom/atom.js
También quería manejar los errores de renderizado: si un estado no se puede renderizar correctamente, quería que mi estado se revirtiera para evitar bloquear la interfaz de usuario. Desafortunadamente, hasta la próxima versión, React hace un muy mal trabajo cuando los métodos de renderización arrojan errores, por lo que no fue muy útil, pero puede serlo en el futuro.

Estoy bastante seguro de que podemos permitir que una tienda acepte múltiples envíos de sincronización en una transacción con un middleware.

Sin embargo, no estoy seguro de que sea posible revertir el estado en caso de error de renderizado, ya que generalmente la tienda redux ya se ha "comprometido" cuando intentamos renderizar su estado. En mi marco hay un gancho "beforeTransactionCommit" que utilizo para activar el renderizado y eventualmente deshacer cualquier error de render.

@gaearon Me pregunto si planea admitir este tipo de funciones y si sería posible con la API actual.

Me parece que redux-batched-subscribe no permite realizar transacciones reales, sino que solo reduce el número de representaciones. Lo que veo es que la tienda se "confirma" después de cada envío, incluso si el oyente de suscripción solo se activa una vez al final

¿Por qué necesitamos un soporte completo para las transacciones? No creo que entiendo el caso de uso.

@gaearon No estoy realmente seguro todavía, pero estaría feliz de saber más sobre el caso de

La idea es que podría hacer dispatch([a1,a2]) y si a2 falla, entonces retrocedemos al estado antes de que se enviara a1.

En el pasado, a menudo he estado enviando múltiples acciones de forma sincrónica (en un solo oyente de onClick, por ejemplo, o en un actionCreator) e implementé principalmente transacciones como una forma de llamar a render solo al final de todas las acciones que se envían, pero esto ha sido resuelto de una manera diferente por el proyecto redux-batched-subscribe.

En mis casos de uso, las acciones que solía disparar en una transacción eran principalmente para evitar representaciones innecesarias, pero las acciones tenían sentido de forma independiente, por lo que incluso si el envío fallaba para la segunda acción, no revertir la primera acción todavía me daría un estado consistente ( pero tal vez no el que estaba planeado ...). Realmente no sé si alguien puede pensar en un caso de uso en el que una reversión completa sería útil

Sin embargo, cuando el renderizado falla, ¿no tiene sentido intentar retroceder al último estado en el que el renderizado no falla en lugar de intentar avanzar en un estado no reproducible?

¿Funcionaría un simple potenciador reductor? p.ej

const enhanceReducerWithTheAbilityToConsumeMultipleActions = (reducer =>
  (state, actions) => (typeof actions.reduce === 'function'
    ? actions.reduce(reducer, state)
    : reducer(state, actions)
  )
)

Con eso, puede enviar una matriz a la tienda. El potenciador desempaqueta la acción individual y la alimenta a cada reductor.

Ohh @gaearon , no sabía eso. No noté que había 2 proyectos distintos que intentan resolver un caso de uso bastante similar de diferentes maneras:

Ambos permitirán evitar renderizados innecesarios, pero el primero revertiría todas las acciones por lotes mientras que el segundo solo no aplicaría la acción fallida.

@gaearon Ay, mi mal por no mirar eso. : enrojecido:


Action Creators representan Impure Code

Probablemente no he tenido tanta experiencia práctica con Redux como la mayoría de las personas, pero a primera vista, tengo que estar en desacuerdo con "hacer más en los creadores de acción y hacer menos en los reductores". Tuve una discusión similar dentro de nuestro empresa.

In Hacker Way: Repensar el desarrollo de aplicaciones web en Facebook, donde se introduce el patrón Flux, el mismo problema que lleva a que se invente Flux es el código imperativo.

En este caso, un Action Creator que hace E / S es ese código imperativo.

No estamos usando Redux en el trabajo, pero en donde yo trabajo solíamos tener acciones detalladas (que, por supuesto, todas tienen sentido por sí mismas) y las activamos en un lote. Por ejemplo, cuando hace clic en un mensaje, se activan tres acciones: OPEN_MESSAGE_VIEW , FETCH_MESSAGE , MARK_NOTIFICATION_AS_READ .

Entonces resulta que estas acciones de "bajo nivel" no son más que un "comando" o "establecedor" o "mensaje" para establecer algún valor dentro de una tienda. También podríamos volver atrás y usar MVC y terminar con un código más simple si seguimos haciéndolo así.

En cierto sentido, los creadores de acciones representan código impuro, mientras que los reductores (y los selectores) representan código puro . La gente de Haskell ha descubierto que es mejor tener un código menos impuro y un código más puro .

Por ejemplo, en mi proyecto paralelo (usando Redux), uso la API de reconocimiento de voz de webkit. Emite evento onresult mientras habla. Hay dos opciones: ¿dónde se procesan estos eventos?

  • Haga que el creador de la acción procese el evento primero y luego lo envíe a la tienda.
  • Simplemente envíe el objeto del evento a la tienda.

Fui con el número dos: simplemente envíe el objeto de evento sin formato a la tienda.

Sin embargo, parece que a las herramientas de desarrollo de Redux no les gusta cuando se envían objetos que no son simples a la tienda, así que agregué una pequeña lógica en el creador de acciones para transformar estos objetos de eventos en objetos simples. (El código del creador de la acción es tan trivial que no puede fallar).

Entonces, el reductor puede combinar estos eventos muy primitivos y construir la transcripción de lo que se habla. Debido a que esa lógica vive dentro del código puro, puedo modificarla en vivo muy fácilmente (recargando en caliente el reductor).

Me gustaría apoyar a @dtinth. Las acciones deben representar eventos que ocurrieron en el mundo real, no cómo queremos reaccionar ante estos eventos. En particular, consulte CQRS: queremos registrar la mayor cantidad de detalles sobre eventos de la vida real, y es probable que los reductores mejoren en el futuro y procesen eventos antiguos con nueva lógica.

@dtinth @ denis-sokolov También estoy de acuerdo contigo en eso. Por cierto, cuando estaba haciendo referencia al proyecto redux-saga , tal vez no dejé en claro que estoy en contra de la idea de hacer que los creadores de acción crezcan y sean cada vez más complejos con el tiempo.

El proyecto Redux-saga también es un intento de hacer lo que estás describiendo @dtinth, pero hay una sutil diferencia con lo que ambos dicen. Parece que quiere decir que si escribe todos los eventos sin procesar que sucedieron en el registro de acciones, puede calcular fácilmente cualquier estado a partir de los reductores de este registro de acciones. Esto es absolutamente cierto, y he seguido ese camino durante un tiempo hasta que mi aplicación se volvió muy difícil de mantener porque el registro de acciones no se volvió explícito y los reductores fueron demasiado complejos con el tiempo.

Tal vez pueda mirar este punto de la discusión original que condujo a la discusión de la saga Redux: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Caso de uso para resolver

Imagina que tienes una aplicación Todo, con el evento TodoCreated obvio. Luego le pedimos que codifique la incorporación de una aplicación. Una vez que el usuario crea una tarea, debemos felicitarlo con una ventana emergente.

El camino "impuro":

Esto es lo que parece preferir @bvaughn

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

No me gusta este enfoque porque hace que el creador de la acción esté muy acoplado al diseño de la vista de la aplicación. Se asume que actionCreator debe conocer la estructura del árbol de estado de la interfaz de usuario para tomar su decisión.

La forma de "calcular todo desde eventos sin procesar":

Esto es lo que parece preferir @ denis-sokolov @dtinth :

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

Sí, puede crear un reductor que sepa si se debe mostrar la felicitación. Pero luego tiene una ventana emergente que se mostrará sin siquiera una acción que diga que la ventana emergente se ha mostrado. En mi propia experiencia haciendo eso (y todavía tengo código heredado haciendo eso) siempre es mejor hacerlo muy explícito: NUNCA muestre la ventana emergente de felicitación si no se activa ninguna acción DISPLAY_CONGRATULATION. Explícito es mucho más fácil de mantener que implícito.

La forma simplificada de la saga.

La saga redux usa generadores y puede parecer un poco complicado si no está acostumbrado, pero básicamente con una implementación simplificada escribiría algo como:

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

La saga es un actor con estado que recibe eventos y puede producir efectos. Aquí se implementa como un reductor impuro para darte una idea de lo que es, pero en realidad no está en el proyecto redux-saga.

Complicando un poco las reglas:

Si cuidas la regla inicial, no es muy explícito en todo.
Si observa las implementaciones anteriores, notará que la ventana emergente de felicitación se abre cada vez que creamos una tarea durante la incorporación. Lo más probable es que queramos que se abra solo para el primer todo creado que ocurre durante la incorporación y no para todos. Además, queremos permitir que el usuario eventualmente rehaga la incorporación desde el principio.

¿Puedes ver cómo el código se volvería complicado en las 3 implementaciones con el tiempo a medida que la incorporación se vuelve cada vez más complicada?

El camino de redux-saga

Con redux-saga y las reglas de incorporación anteriores, escribirías algo como

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

Creo que resuelve este caso de uso de una manera mucho más simple que las soluciones anteriores. Si me equivoco, dame tu implementación más simple :)

Hablaste de código impuro, y en este caso no hay impureza en la implementación de Redux-saga porque los efectos take / put son en realidad datos. Cuando se llama a take () no se ejecuta, devuelve un descriptor del efecto a ejecutar, y en algún momento se activa un intérprete, por lo que no necesitas ningún simulacro para probar las sagas. Si eres un desarrollador funcional que hace Haskell, piensa en mónadas Free / IO.


En este caso permite:

  • Evita complicar actionCreator y haz que dependa de getState
  • Haz lo implícito más explícito
  • Evite acoplar la lógica transversal (como la incorporación anterior) a su dominio empresarial principal (creación de tareas pendientes)

También puede proporcionar una capa de interpretación, lo que permite traducir eventos sin procesar en eventos más significativos / de alto nivel (un poco como lo hace ELM al envolver los eventos a medida que surgen).

Ejemplos:

  • "TIMELINE_SCROLLED_NEAR-BOTTOM" podría conducir a "NEXT_PAGE_LOADED"
  • "REQUEST_FAILED" podría conducir a "USER_DISCONNECTED" si el código de error es 401.
  • "HASHTAG_FILTER_ADDED" podría generar "CONTENT_RELOADED"

Si desea lograr un diseño de aplicación modular con patos, puede evitar que los patos se junten. La saga se convierte en el punto de unión. Los patos solo tienen que saber de sus eventos crudos, y la saga interpreta estos eventos crudos. Esto es mucho mejor que tener duck1 enviando acciones directamente de duck2 porque hace que el proyecto duck1 sea más fácil de reutilizar en otro contexto. Sin embargo, se podría argumentar que el punto de acoplamiento también podría estar en actionCreators y esto es lo que la mayoría de la gente está haciendo hoy.

@slorber ¡ Este es un excelente ejemplo! Gracias por tomarse el tiempo para explicar claramente los beneficios y los inconvenientes de cada enfoque. (Incluso creo que debería ir en los documentos).

Solía ​​explorar una idea similar (que llamé "componentes de trabajo"). Básicamente, es un componente de React que no representa nada ( render: () => null ), pero escucha los eventos (por ejemplo, de las tiendas) y desencadena otros efectos secundarios. Luego, ese componente de trabajo se coloca dentro del componente raíz de la aplicación. Solo otra forma loca de manejar efectos secundarios complejos. : Lengua_salida_atascada:

Mucha discusión aquí mientras dormía.

@winstonewert , plantea un buen punto sobre el viaje en el tiempo y la reproducción del código defectuoso. Creo que ciertos tipos de errores / cambios no funcionarán con el viaje en el tiempo de ninguna manera, pero creo que, en general, tienes razón.

@dtinth , lo siento, pero no sigo la mayor parte de tu comentario. Alguna parte de su código "pato" creador / reductor de acciones tiene que ser impuro, en el sentido de que alguna parte tiene que buscar datos. Más allá de eso, me perdiste. Uno de los propósitos principales de mi publicación inicial fue solo uno de pragmatismo.


@winstonewert dijo: "Creo que mi forma de pensar en redux insiste en que la tienda siempre está en un estado válido".
@slorber preguntó: "¿Qué quieres decir con estado transitorio en Redux @bvaughn y @sompylasar ? O el envío finaliza o arroja. Si arroja, entonces el estado no cambia".

Estoy bastante seguro de que estamos pensando en cosas diferentes. Cuando dije "estado no válido transitorio", me refería a un caso de uso como el siguiente. Por ejemplo, un colega y yo lanzamos recientemente

Imagine que su tienda de aplicaciones contiene algunos objetos que se pueden buscar: [{id: 1, name: "Alex"}, {id: 2, name: "Brian"}, {id: 3, name: "Charles"}] . El usuario ingresó el texto de filtro "e" y, por lo tanto, el middleware de búsqueda contiene una matriz de ID 1 y 3. Ahora imagine que el usuario 1 (Alex) se elimina, ya sea en respuesta a una acción del usuario localmente o una actualización de datos remotos que ya no contiene ese registro de usuario. En el momento en que su reductor actualice la colección de usuarios, su tienda será temporalmente inválida, porque redux-search hará referencia a una identificación que ya no existe en la colección. Una vez que el middleware se haya ejecutado nuevamente, corregirá el estado no válido. Este tipo de cosas puede suceder en cualquier momento en que un nodo de su árbol esté relacionado con otro nodo.


@slorber dijo: "No me gusta este enfoque porque hace que el creador de la acción esté muy acoplado al diseño de la vista de la aplicación. Se asume que actionCreator debe conocer la estructura del árbol de estado de la interfaz de usuario para tomar una decisión".

No entiendo lo que quiere decir con el enfoque que combina el creador de acciones "con el diseño de la vista de la aplicación". El árbol de estado _ impulsa_ (o informa) la interfaz de usuario. Ese es uno de los principales propósitos de Flux. Y sus creadores de acciones y reductores están, por definición, acoplados con ese estado (pero no con la interfaz de usuario).

Por lo que vale el código de ejemplo que escribiste como algo que prefiero, no es el tipo de cosas que tenía en mente. Tal vez hice un mal trabajo al explicarme. Creo que una dificultad para discutir algo como esto es que normalmente no se manifiesta en ejemplos simples o comunes. (Por ejemplo, la aplicación TODO MVC estándar no es lo suficientemente compleja para discusiones matizadas como esta).

Editado para mayor claridad sobre el último punto.

Por cierto @slorber, aquí hay un ejemplo de lo que tenía en mente. Es un poco artificial.

Digamos que su estado tiene muchos nodos. Uno de esos nodos almacena recursos compartidos. (Por "compartidos" me refiero a los recursos que se almacenan en caché localmente y a los que se accede a través de varias páginas dentro de su aplicación). Estos recursos compartidos tienen sus propios creadores de acciones y reductores ("patos"). Otro nodo almacena información para una página de aplicación en particular. Tu página también tiene su propio pato.

Supongamos que su página necesita cargar la última y mejor cosa y luego permitir que un usuario la edite. Aquí hay un ejemplo de enfoque de creador de acciones que podría usar para tal situación:

import { fetchThing, thingSelector } from 'resources/thing/duck'
import { showError } from 'messages/duck'

export function fetchAndProcessThing ({ params }): Object {
  const { id } = params
  return async ({ dispatch, getState }) => {
    try {
      await dispatch(fetchThing({ id }))

      const thing = thingSelector(getState())

      dispatch({ type: 'PROCESS_THING', thing })
    } catch (err) {
      dispatch(showError(`Invalid thing id="${id}".`))
    }
  }
}

Tal vez @winstonewert quiera que si el envío de la segunda acción falla, entonces revertimos las 2 acciones.

No. No escribiría un creador de acciones que distribuya dos acciones. Definiría una sola acción que hizo dos cosas. El OP parece preferir los creadores de acciones que envían acciones más pequeñas que permiten los estados transitorios inválidos que no me gustan.

En el momento en que su reductor actualice la colección de usuarios, su tienda será temporalmente inválida, porque redux-search hará referencia a una identificación que ya no existe en la colección. Una vez que el middleware se haya ejecutado nuevamente, corregirá el estado no válido. Este tipo de cosas puede suceder en cualquier momento en que un nodo de su árbol esté relacionado con otro nodo.

En realidad, este es el tipo de caso que me molesta. En mi opinión, el índice sería idealmente algo manejado completamente por el reductor o un selector. Tener que enviar acciones adicionales para mantener la búsqueda actualizada parece un uso menos puro de redux.

El OP parece preferir los creadores de acciones que envían acciones más pequeñas que permiten los estados transitorios inválidos que no me gustan.

No exactamente. Yo favorecería las acciones individuales en lo que respecta al nodo del árbol de estado de su creador de acciones. Pero si una única "acción" conceptual del usuario afecta a varios nodos del árbol de estado, entonces deberá enviar varias acciones. Puede invocar por separado cada acción (que creo que es _bad_) o puede hacer que un único creador de acciones envíe las acciones (la forma redux-thunk, que creo que es _mejor_ porque oculta esa información de su capa de vista).

En realidad, este es el tipo de caso que me molesta. En mi opinión, el índice sería idealmente algo manejado completamente por el reductor o un selector. Tener que enviar acciones adicionales para mantener la búsqueda actualizada parece un uso menos puro de redux.

No estás enviando acciones adicionales. La búsqueda es un software intermedio. Es automático. Pero existe un estado transitorio cuando los dos nodos de su árbol no están de acuerdo.

@bvaughn ¡Oh, perdón por ser tan purista!

Bueno, el código impuro tiene que ver con la búsqueda de datos y otros efectos secundarios / IO, mientras que el código puro no puede desencadenar ningún efecto secundario. Consulte esta tabla para comparar entre código puro e impuro.

Las mejores prácticas de Flux dicen que una acción debe "describir la acción de un usuario, no son determinantes". Los documentos de Flux también insinuaron de dónde se supone que provienen estas acciones:

Cuando ingresan nuevos datos al sistema, ya sea a través de una persona que interactúa con la aplicación o mediante una llamada a la API web , esos datos se empaquetan en una acción: un objeto literal que contiene los nuevos campos de datos y un tipo de acción específico.

Básicamente, las acciones son hechos / datos que describen "lo que sucedió", no lo que debería suceder. Las tiendas solo pueden reaccionar a estas acciones de forma sincrónica, predecible y sin ningún otro efecto secundario. Todos los demás efectos secundarios deben manejarse en creadores de acción (o sagas: wink :).

Ejemplo

No digo que esta sea la mejor manera o mejor que cualquier otra manera, ni siquiera una buena manera. Pero esto es lo que actualmente considero una mejor práctica.

Por ejemplo, digamos que el usuario quiere ver el marcador que requiere conexión a un servidor remoto. Esto es lo que debería suceder:

  • El usuario hace clic en el botón Ver marcador.
  • La vista del marcador se muestra con un indicador de carga.
  • Se envía una solicitud al servidor para obtener el marcador.
  • Espere la respuesta.
  • Si tiene éxito, muestre el marcador.
  • Si falla, el marcador se cierra y aparece un cuadro de mensaje con un mensaje de error. El usuario puede cerrarlo.
  • El usuario puede cerrar el marcador.

Suponiendo que las acciones solo pueden llegar a la tienda como resultado de la acción del usuario o la respuesta del servidor, podemos crear 5 acciones.

  • SCOREBOARD_VIEW (como resultado de que el usuario haga clic en el botón Ver marcador)
  • SCOREBOARD_FETCH_SUCCESS (como resultado de una respuesta exitosa del servidor)
  • SCOREBOARD_FETCH_FAILURE (como resultado de una respuesta de error del servidor)
  • SCOREBOARD_CLOSE (como resultado de que el usuario haga clic en el botón cerrar)
  • MESSAGE_BOX_CLOSE (como resultado de que el usuario haga clic en el botón cerrar en el cuadro de mensaje)

Estas 5 acciones son suficientes para manejar todos los requisitos anteriores. Puede ver que las primeras 4 acciones no tienen nada que ver con ningún "pato". Cada acción solo describe lo que sucedió en el mundo exterior (el usuario quiere hacer esto, el servidor dijo eso) y puede ser consumida por cualquier reductor. Tampoco tenemos la acción MESSAGE_BOX_OPEN , porque eso no es “lo que sucedió” (aunque eso es lo que debería suceder).

La única forma de cambiar el árbol de estado es emitir una acción, un objeto que describe lo que sucedió. - README de Redux

Están pegados con estos creadores de acción:

function viewScoreboard () {
  return async function (dispatch) {
    dispatch({ type: 'SCOREBOARD_VIEW' })
    try {
      const result = fetchScoreboardFromServer()
      dispatch({ type: 'SCOREBOARD_FETCH_SUCCESS', result })
    } catch (e) {
      dispatch({ type: 'SCOREBOARD_FETCH_FAILURE', error: String(e) })
    }
  }
}
function closeScoreboard () {
  return { type: 'SCOREBOARD_CLOSE' }
}

Luego, cada parte de la tienda (gobernada por reductores) puede reaccionar a estas acciones:

| Parte de la tienda / reductor | Comportamiento |
| --- | --- |
| scoreboardView | Actualice la visibilidad a verdadero en SCOREBOARD_VIEW , falso en SCOREBOARD_CLOSE y SCOREBOARD_FETCH_FAILURE |
| scoreboardLoadingIndicator | Actualice la visibilidad a verdadero en SCOREBOARD_VIEW , falso en SCOREBOARD_FETCH_* |
| scoreboardData | Actualizar datos dentro de la tienda en SCOREBOARD_FETCH_SUCCESS |
| messageBox | Actualice la visibilidad a verdadero y almacene el mensaje en SCOREBOARD_FETCH_FAILURE , y actualice la visibilidad a falso en MESSAGE_BOX_CLOSE |

Como puede ver, una sola acción puede afectar a muchas partes de la tienda. Las tiendas solo reciben una descripción de alto nivel de una acción (¿qué sucedió?) En lugar de un comando (¿qué hacer?). Como resultado:

  1. Es más fácil identificar errores.

Nada puede afectar el estado del cuadro de mensaje. Nadie puede decirle que se abra por ningún motivo. Solo reacciona a lo que está suscrito (acciones del usuario y respuestas del servidor).

Por ejemplo, si el servidor no puede obtener el marcador y no aparece un cuadro de mensaje, no es necesario que averigüe por qué no se envía la acción SHOW_MESSAGE_BOX . Resulta obvio que el cuadro de mensaje no manejó la acción SCOREBOARD_FETCH_FAILURE correctamente.

Una solución es trivial y se puede recargar en caliente y viajar en el tiempo.

  1. Los creadores y reductores de acciones se pueden probar por separado.

Puede probar si los creadores de acciones describieron correctamente lo que sucede en el mundo exterior, sin tener en cuenta cómo reaccionan las tiendas.

De la misma manera, los reductores simplemente pueden probarse si reaccionan adecuadamente a las acciones del mundo exterior.

(Una prueba de integración aún sería muy útil).

No hay problema. :) Agradezco la aclaración adicional. De hecho, parece que estamos de acuerdo aquí. Mirando su ejemplo de creador de acciones, viewScoreboard , se parece mucho a mi ejemplo de creador de acciones fetchAndProcessThing , justo encima de él.

Los creadores y reductores de acciones se pueden probar por separado.

Si bien estoy de acuerdo con esto, creo que a menudo tiene un sentido más pragmático probarlos juntos. Es probable que su acción _o_ o su reductor (tal vez ambos) sean súper simples y, por lo tanto, el valor de retorno del esfuerzo de probar el simple de forma aislada es algo bajo. Por eso propuse probar el creador de acción, el reductor y los selectores relacionados juntos (como un "pato").

Pero si una única "acción" conceptual del usuario afecta a varios nodos del árbol de estado, entonces deberá enviar varias acciones.

Ahí es precisamente donde creo que lo que está haciendo difiere de lo que se considera las mejores prácticas para redux. Creo que la forma estándar es tener una acción a la que respondan varios nodos del árbol de estado.

Ah, observación interesante @winstonewert. Hemos estado siguiendo un patrón de uso de constantes de tipo únicas para cada paquete de "patos" y, por extensión, una única respuesta reductora a las acciones enviadas por sus hermanos creadores de acciones. Honestamente, no estoy seguro de cómo me siento, inicialmente, acerca de los reductores arbitrarios que responden a una acción. Se siente un poco como una mala encapsulación.

Hemos seguido un patrón de uso de constantes de tipo únicas para cada paquete de "patos".

Tenga en cuenta que no lo respaldamos en ninguna parte de los documentos ;-) No digo que sea malo, pero le da a la gente ciertas ideas a veces erróneas sobre Redux.

Entonces, por extensión, un reductor solo responde a las acciones enviadas por sus hermanos creadores de acciones.

No existe el emparejamiento de reductor / creador de acción en Redux. Eso es puramente una cosa de los patos. A algunas personas les gusta, pero oscurece las fortalezas fundamentales del modelo Redux / Flux: las mutaciones de estado se desacoplan entre sí y del código que las causa.

Honestamente, no estoy seguro de cómo me siento, inicialmente, acerca de los reductores arbitrarios que responden a una acción. Se siente un poco como una mala encapsulación.

Dependiendo de lo que considere los límites de encapsulación. Las acciones son globales en la aplicación y creo que eso está bien. Es posible que una parte de la aplicación desee reaccionar a las acciones de otra parte debido a los requisitos complejos del producto, y creemos que esto está bien. El acoplamiento es mínimo: todo de lo que depende es una cuerda y la forma del objeto de acción. El beneficio es que es fácil introducir nuevas derivaciones de las acciones en diferentes partes de la aplicación sin crear toneladas de cableado con los creadores de acciones. Sus componentes ignoran lo que sucede exactamente cuando se envía una acción; esto se decide en el extremo del reductor.

Por lo tanto, nuestra recomendación oficial es que primero debe intentar que diferentes reductores respondan a las mismas acciones. Si se vuelve incómodo, entonces claro, cree creadores de acciones separados. Pero no empieces con este enfoque.

Recomendamos usar selectores; de hecho, recomendamos exportar funciones de conservación que leen del estado ("selectores") junto con reductores, y usarlas siempre en mapStateToProps lugar de codificar la estructura de estado en el componente. De esta forma, es fácil cambiar la forma del estado interno. Puede (pero no tiene que hacerlo) usar reselect para el rendimiento, pero también puede implementar selectores ingenuamente como en el ejemplo shopping-cart .

Quizás, todo se reduce a si programa en estilo imperativo o en estilo reactivo. El uso de patos puede hacer que las acciones y los reductores se acoplen mucho, lo que fomenta acciones más imperativas.

  • En un estilo imperativo, a la tienda se le da "qué hacer", por ejemplo, SHOW_MESSAGE_BOX o SHOW_ERROR
  • En estilo reactivo, la tienda recibe "un hecho de lo que sucedió", por ejemplo, DATA_FETCHING_FAILED o USER_ENTERED_INVALID_THING_ID . La tienda reacciona en consecuencia.

En el ejemplo anterior, no tengo SHOW_MESSAGE_BOX action o showError('Invalid thing id="'+id+'"') action creator, porque eso no es un hecho. Eso es un comando.

Una vez que ese hecho ingresa a la tienda, puede traducir ese hecho en comandos, dentro de sus reductores puros, por ejemplo

// type Command = State → State
// :: Action → Command
function interpretAction (action) {
  switch (action.type) {
  case 'DATA_FETCHING_FAILED':
    return showErrorMessage('Data fetching failed')
    break
  case 'USER_ENTERED_INVALID_THING_ID':
    return showErrorMessage('User entered invalid thing ID')
    break
  case 'CLOSE_ERROR_MESSAGE':
    return hideErrorMessage()
    break
  default:
    return doNothing()
  }
}

// :: (State, Action) → State
function errorMessageReducer (state, action) {
  return interpretAction(action)(state)
}

const showErrorMessage = message => state => ({ visible: true, message })
const hideErrorMessage = () => state => ({ visible: false })
const doNothing = () => state => state

Cuando una acción entra en la tienda como "un hecho" en lugar de "una orden", hay menos posibilidades de que salga mal, porque, bueno, es un hecho.

Ahora, si sus reductores malinterpretaron ese hecho, se puede arreglar fácilmente y la solución puede viajar en el tiempo. Sin embargo, si sus creadores de acciones malinterpretan ese hecho, debe volver a ejecutar sus creadores de acciones.

También puede cambiar su reductor para que cuando se dispare USER_ENTERED_INVALID_THING_ID , se restablezca el campo de texto de ID de objeto. Y ese cambio también viaja a través del tiempo. También puede localizar su mensaje de error y sin actualizar la página. Eso estrecha el ciclo de retroalimentación y hace que la depuración y los ajustes sean mucho más fáciles.

(Solo estoy hablando de los pros, por supuesto que hay contras. Tienes que pensar mucho más sobre cómo representar ese hecho, dado que tu tienda solo puede responder a estos hechos de forma sincrónica y sin efectos secundarios. Ver discusión sobre alternativas asincrónicas / modelos de efectos secundarios y esta pregunta que publiqué en StackOverflow . Supongo que aún no hemos clavado esa parte).


Honestamente, no estoy seguro de cómo me siento, inicialmente, acerca de los reductores arbitrarios que responden a una acción. Se siente un poco como una mala encapsulación.

También es muy común que varios componentes tomen datos de la misma tienda. También es bastante común que un solo componente dependa de datos de varias partes de la tienda. ¿No suena eso también un poco a una mala encapsulación? Para volverse verdaderamente modular, ¿no debería un componente React estar también dentro del paquete "pato"? (La arquitectura de Elm hace eso ).

React hace que su interfaz de usuario sea reactiva (de ahí su nombre) al tratar los datos de su tienda como un hecho. Por lo tanto, no tiene que decirle a su vista "cómo actualizar la interfaz de usuario".

De la misma manera, también creo que Redux / Flux hace que su modelo de datos sea reactivo , al tratar las acciones como un hecho, por lo que no tiene que decirle a su modelo de datos cómo actualizarse.

Gracias por tomarse el tiempo para escribir y compartir sus pensamientos, @dtinth. También gracias a @gaearon por

También es muy común que varios componentes tomen datos de la misma tienda. También es bastante común que un solo componente dependa de datos de varias partes de la tienda. ¿No suena eso también un poco a una mala encapsulación?

Eh ... algo de esto es subjetivo, pero no. Pienso en los selectores y creadores de acciones exportados como la API del módulo.

De todos modos, creo que esta ha sido una buena discusión. Como Thai mencionó en su respuesta anterior, existen pros y contras de estos enfoques que estamos discutiendo. Ha sido agradable conocer los enfoques de otros. :)

Por cierto, el cuadro de mensaje es un buen ejemplo de dónde preferiría tener un creador de acciones separado para mostrar. Principalmente porque querré pasar un tiempo cuando se creó para que pueda descartarse automáticamente (y el creador de la acción es donde llamas impuro Date.now() ), porque quiero configurar un temporizador para descartarlo, yo quiero eliminar el rebote de ese temporizador, etc. Por lo tanto, consideraría un cuadro de mensaje el caso en el que su "flujo de acción" es lo suficientemente importante como para justificar sus acciones personales. Dicho esto, quizás lo que describí se pueda resolver de manera más elegante con https://github.com/yelouafi/redux-saga.

Escribí esto en el chat de Discord Reactiflux inicialmente, pero me pidieron que lo pegara aquí.

He estado pensando mucho en las mismas cosas recientemente. Siento que las actualizaciones de estado se dividen en tres partes.

  1. Al creador de la acción se le pasa la cantidad mínima de información necesaria para ejecutar la actualización. Es decir, cualquier cosa que se pueda calcular a partir del estado actual no debería estar en él.
  2. Se consulta el estado para cualquier información que necesite para cumplir con las actualizaciones (por ejemplo, cuando desea copiar Todo con id X, obtiene los atributos de Todo con id X para poder hacer una copia). Esto se puede hacer en el creador de acciones, y esa información luego se incluye en el objeto de acción. Esto da como resultado objetos de acción gordos . O podría calcularse en el reductor - objetos de acción fina .
  3. Con base en esa información, se aplica la lógica reductora pura para obtener el siguiente estado.

Ahora, el problema es qué poner en el creador de acción y qué en el reductor, la elección entre objetos de acción gruesos y delgados. Si pones toda la lógica en el creador de acciones, terminas con objetos de acción gruesos que básicamente declaran las actualizaciones del estado. Los reductores se vuelven puros, tontos, agregan esto, eliminan aquello, actualizan estas funciones. Serán fáciles de componer. Pero no gran parte de su lógica empresarial estará ahí.

Si pones más lógica en el reductor, terminas con objetos de acción delgados y agradables, la mayor parte de tu lógica de datos en un solo lugar, pero tus reductores son más difíciles de componer ya que es posible que necesites información de otras ramas. Terminas con reductores grandes o reductores que toman argumentos adicionales de más arriba en el estado.

No sé cuál es la respuesta a estos problemas, no estoy seguro de si existe todavía.

Gracias por compartir estos pensamientos @tommikaikkonen. Yo mismo todavía estoy indeciso acerca de cuál es la "respuesta" aquí. Estoy de acuerdo con tu resumen. Agregaría una pequeña nota a la sección "poner toda la lógica en el creador de acciones ...", que es que te permite usar selectores (compartidos) para leer datos, lo que en algunos casos puede ser bueno.

¡Este es un hilo interesante! Averiguar dónde colocar el código en una aplicación redux es un problema que supongo que todos enfrentamos. Me gusta la idea de CQRS de simplemente registrar las cosas que han sucedido.
Pero puedo ver un desajuste de ideas aquí porque en CQRS, AFAIK, la mejor práctica es construir un estado desnormalizado a partir de la acción / eventos que es directamente consumible por las vistas. Pero en redux, la mejor práctica es construir un estado completamente normalizado y obtener los datos de sus vistas a través de selectores.
Si construimos un estado desnormalizado que es directamente consumible por la vista, entonces creo que el problema de que un reductor quiere datos en otro reductor desaparece (porque cada reductor puede almacenar todos los datos que necesita sin preocuparse por la normalización). Pero luego tenemos otros problemas al actualizar los datos. ¿Quizás este sea el núcleo de la discusión?

Viniendo de muchos años de desarrollo orientado a objetos. Redux se siente como un gran paso hacia atrás. Me encuentro rogando por crear clases que encapsulen eventos (creadores de acciones) y la lógica empresarial. Todavía estoy tratando de encontrar un compromiso significativo, pero hasta ahora no he podido hacerlo. Alguien más se siente igual?

La programación orientada a objetos fomenta la combinación de lecturas con escrituras. Esto hace que un montón de cosas sean problemáticas: instantáneas y reversiones, registro centralizado, depuración de mutaciones de estado incorrectas, actualizaciones eficientes granulares. Si no cree que esos son los problemas para usted, si sabe cómo evitarlos mientras escribe código MVC tradicional orientado a objetos, y si Redux presenta más problemas de los que resuelve en su aplicación, no use Redux: wink: .

@jayesbe Viniendo de un fondo de programación orientada a objetos, puede encontrar que choca con las ideas emergentes en programación. Este es uno de los muchos artículos sobre el tema: https://www.leaseweb.com/labs/2015/08/object-oriented-programming-is-exceptionally-bad/.

Al separar las acciones de la transformación de datos, la prueba de la aplicación de las reglas comerciales es más sencilla. Las transformaciones se vuelven menos dependientes del contexto de la aplicación.

Eso no significa abandonar objetos o clases en Javascript. Por ejemplo, los componentes de React se implementan como objetos y ahora como clases. Pero los componentes de React están diseñados simplemente para crear una proyección de los datos proporcionados. Se le anima a utilizar componentes puros que no almacenan el estado.

Ahí es donde entra Redux: para organizar el estado de la aplicación y unir las acciones y la correspondiente transformación del estado de la aplicación.

@johnsoftek gracias por el enlace. Sin embargo, basado en mi experiencia en los últimos 10 años ... no estoy de acuerdo con eso, pero no necesitamos entrar en el debate entre OO y no OO aquí. El problema que tengo es con la organización del código y la abstracción.

Mi objetivo es crear una sola aplicación / arquitectura única que pueda (usando solo los valores de configuración) usarse para crear cientos de aplicaciones. El caso de uso con el que tengo que lidiar es el manejo de una solución de software de marca blanca que está siendo utilizada por muchos clientes ... cada uno de los cuales llama a la aplicación como suya.

He llegado a lo que creo que es un compromiso interesante ... y creo que lo maneja lo suficientemente bien, pero puede que no cumpla con los estándares funcionales de multitudes de programación. Todavía me gustaría publicarlo.

Tengo una sola clase de aplicación que es autónoma con toda la lógica empresarial, envoltorios de API, etc. que necesito para interactuar con mi aplicación del lado del servidor.

ejemplo..

export default Application {
    constructor(config) {
        this.config = config;
    } 

    config() {
        return this.config;
    }

    login(data, cb) {
        const url = [
            this.config.url,
            '?client=' + this.config.client,
            '&username=' + data.username,
            ....
        ].join('');

        fetch(url).then((responseText) => {
            cb(responseText);
        })
    }

    ... more business logic 
}

Creé una instancia única de este objeto y la coloqué en el contexto ... extendiendo el proveedor de Redux

import { Provider } from 'react-redux';

export default class MyProvider extends Provider {
    getChildContext() {
        return Object.assign({}, Provider.prototype.getChildContext.call(this), {
            app: this.props.app
        });
    }

    render() {
        return this.props.children;
    }
}
MyProvider.childContextTypes = {
    store: React.PropTypes.object,
    app: React.PropTypes.object
}

Entonces usé este proveedor como tal

import Application from './application';
import config from './config';

class MyApp extends Component {
  render() {
    return (
      <MyProvider store={store} app={new Application(config)}>
        <Router />
      </MyProvider>
    );
  }
}

AppRegistry.registerComponent('MyApp', () => MyApp);

finalmente en mi Componente utilicé mi aplicación ..

class Login extends React.Component {
    render() {
        const { app } = this.context;
        const { state, actions } = this.props;
        return (
              <View style={style.transparentContainer}>
                <Form ref="form" type={User} options={options} />
                <Button 
                  onPress={() => {
                    value = this.refs.form.getValue();
                    if (value) {
                      app.login(value, actions.login);
                    }
                  }}
                >
                  Login
                </Button>
              </View>
        );
    }
};
Login.contextTypes = {
  app: React.PropTypes.object,
};

function mapStateToProps(state) {
  return {
      state: state.default.auth
  };
};

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(authActions, dispatch),
    dispatch
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Login);

Por lo tanto, Action Creator es solo una función de devolución de llamada para mi lógica comercial.

                      app.login(value, actions.login);

Esta solución parece estar funcionando bien en este momento, aunque solo comencé con la autenticación.

Creo que también podría pasar la tienda a mi instancia de aplicación, pero no quiero hacerlo porque no quiero que la tienda sufra una mutación fortuita. Aunque acceder a la tienda puede ser útil. Pensaré más en eso si es necesario.

He llegado a lo que creo que es un compromiso interesante ... y creo que lo maneja lo suficientemente bien, pero puede que no cumpla con los estándares funcionales de multitudes de programación.

Aquí no encontrará "la multitud funcional": wink:. La razón por la que elegimos soluciones funcionales en Redux no es porque seamos dogmáticos, sino porque resuelven algunos problemas que la gente suele hacer debido a las clases. Por ejemplo, separar los reductores de los creadores de acciones nos permite separar lecturas y escrituras, lo cual es importante para registrar y reproducir errores. Las acciones que son objetos simples hacen posible la grabación y reproducción porque son serializables. De manera similar, el estado es un objeto simple en lugar de una instancia de MyAppState hace que sea muy fácil serializarlo en el servidor y deserializarlo en el cliente para la representación del servidor, o conservar partes de él en localStorage . Expresar los reductores como funciones nos permite implementar el viaje en el tiempo y la recarga en caliente, y expresar los selectores como funciones hace que la memorización sea fácil de agregar. Todos estos beneficios no tienen nada que ver con que seamos una “multitud funcional” y todo con la resolución de tareas específicas para las que se creó esta biblioteca.

Creé una instancia única de este objeto y la coloqué en el contexto ... extendiendo el proveedor de Redux

Esto me parece totalmente sensato. No tenemos un odio irracional por las clases. El punto es que preferiríamos no usarlos en los casos en que sean muy limitantes (como para reductores u objetos de acción), pero está bien usarlos para generar objetos de acción, por ejemplo.

Sin embargo, evitaría extender Provider ya que es frágil. No es necesario: React fusiona el contexto de los componentes, por lo que puede ajustarlo en su lugar.

import { Component } from 'react';
import { Provider } from 'react-redux';

export default class MyProvider extends Component {
    getChildContext() {
        return {
            app: this.props.app
        };
    }

    render() {
        return (
            <Provider store={this.props.store}>
                {this.props.children}
            </Provider>
        );
    }
}
MyProvider.childContextTypes = {
    app: React.PropTypes.object
}
MyProvider.propTypes = {
    app: React.PropTypes.object,
    store: React.PropTypes.object
}

En realidad, en mi opinión, se lee más fácilmente y es menos frágil.

Entonces, en general, su enfoque tiene total sentido. El uso de una clase en este caso no es realmente diferente de algo como createActions(config) que es un patrón que también recomendamos si necesita parametrizar los creadores de acciones. No hay absolutamente nada de malo en ello.

Solo le recomendamos que no use instancias de clase para objetos de estado y acción porque las instancias de clase hacen que la serialización sea muy complicada. Para los reductores, tampoco recomendamos usar clases porque será más difícil usar la composición reductora, es decir, reductores que llaman a otros reductores. Para todo lo demás, puede utilizar cualquier medio de organización del código, incluidas las clases.

Si su aplicación y configuración son inmutables (y creo que deberían serlo, pero tal vez he bebido demasiada ayuda funcional para enfriar), entonces podría considerar el siguiente enfoque:

const appSelector = createSelector(
   (state) => state.config,
   (config) => new Application(config)
)

Y luego en mapStateToProps:

function mapStateToProps(state) {
  return {
      app: appSelector(state)
  };
};

Entonces no necesita la técnica de proveedor que ha adoptado, solo obtiene el objeto de aplicación del estado. Gracias a la reselección, el objeto Aplicación solo se construirá cuando cambie la configuración, que probablemente sea solo una vez.

Donde creo que este enfoque puede tener una ventaja es que fácilmente le permite extender la idea a tener múltiples objetos de este tipo y también hacer que esos objetos dependan de otras partes del estado. Por ejemplo, podría tener una clase UserControl con métodos de inicio de sesión / cierre de sesión / etc que tenga acceso tanto a la configuración como a parte de su estado.

Entonces, en general, su enfoque tiene total sentido.

: +1: Gracias por la ayuda. Estoy de acuerdo con la mejora en MyProvider. Actualizaré mi código para seguir. Uno de los mayores problemas que tuve cuando aprendí Redux por primera vez fue la noción semántica de "Creadores de acción" ... no encajaba hasta que los equiparé con Eventos. Para mí fue una especie de realización que fue como ... estos son eventos que se están enviando.

@winstonewert, ¿createSelector está disponible en react-native? No creo que lo sea. Al mismo tiempo, parece que está creando la nueva aplicación cada vez que la adjunta en mapStateToProps en algún componente. Mi objetivo es tener un único objeto instanciado que proporcione toda la lógica empresarial a la aplicación y que ese objeto sea accesible globalmente. No estoy seguro si tu sugerencia funciona. Aunque me gusta la idea de tener objetos adicionales disponibles si es necesario ... técnicamente también puedo crear instancias según sea necesario a través de la instancia de la Aplicación.

Uno de los mayores problemas que tuve cuando aprendí Redux por primera vez fue la noción semántica de "Creadores de acción" ... no encajaba hasta que los equiparé con Eventos. Para mí fue una especie de realización que fue como ... estos son eventos que se están enviando.

Yo diría que no existe una noción semántica de Action Creators en Redux. Existe una noción semántica de Acciones (que describen lo que sucedió y son aproximadamente equivalentes a los eventos, pero no del todo, por ejemplo, ver la discusión en el n. ° 351). Los creadores de acciones son solo un patrón para organizar el código. Es conveniente tener fábricas de acciones porque desea asegurarse de que las acciones del mismo tipo tengan una estructura consistente, tengan el mismo efecto secundario antes de ser enviadas, etc. Pero desde el punto de vista de Redux, los creadores de acciones no existen — Redux solo ve acciones.

¿Está createSelector disponible en react-native?

Está disponible en Reselect, que es JavaScript simple sin dependencias y puede funcionar en la web, en el nativo, en el servidor, etc.

Ah. Ok lo tengo. Las cosas están mucho más claras. Salud.

No anidar objetos en mapStateToProps y mapDispatchToProps

Recientemente me encontré con un problema en el que los objetos anidados se perdían cuando Redux fusiona mapStateToProps y mapDispatchToProps (ver reactjs / react-redux # 324). Aunque @gaearon proporciona una solución que me permite usar objetos anidados, continúa diciendo que esto es un anti-patrón:

Tenga en cuenta que agrupar objetos como este provocará asignaciones innecesarias y también dificultará las optimizaciones de rendimiento porque ya no podemos confiar en la igualdad superficial de los accesorios de resultado como una forma de saber si cambiaron. Por lo tanto, verá más representaciones que con un enfoque simple sin espacio de nombres que recomendamos en los documentos.

@bvaughn dijo

los reductores deben ser estúpidos y simples

y deberíamos poner la mayor parte de la lógica empresarial en acción, los creadores, estoy absolutamente de acuerdo con eso. Pero si todo se ha convertido en acciones, ¿por qué todavía tenemos que crear archivos y funciones reductores manualmente? ¿Por qué no almacenar directamente los datos que se han operado en acciones?

Me ha confundido un período de tiempo ...

¿Por qué todavía tenemos que crear archivos y funciones reductores manualmente?

Debido a que los reductores son funciones puras, si hay un error en la lógica de actualización de estado, puede volver a cargar el reductor en caliente. La herramienta de desarrollo puede entonces rebobinar la aplicación a su estado inicial y luego reproducir todas las acciones, usando el nuevo reductor. Esto significa que puede corregir errores de actualización de estado sin tener que revertir manualmente y volver a realizar la acción. Este es el beneficio de mantener la mayor parte de la lógica de actualización del estado en el reductor.

Este es el beneficio de mantener la mayor parte de la lógica de actualización del estado en el reductor.

@dtinth Solo para aclarar, al decir "lógica de actualización de estado", ¿te refieres a "lógica de negocios"?

No estoy tan seguro de que poner la mayor parte de su lógica en acción de los creadores sea una buena idea. Si todos sus reductores son solo funciones triviales que aceptan acciones similares a ADD_X , entonces es poco probable que tengan errores, ¡genial! Pero luego todos tus errores se han enviado a tus creadores de acciones y pierdes la gran experiencia de depuración a la que alude @dtinth .

Pero también como mencionó @tommikaikkonen , no es tan simple escribir reductores complejos. Mi intuición es que ahí es donde querrías presionar si quisieras cosechar los beneficios de Redux; de lo contrario, en lugar de llevar los efectos secundarios al límite, estás presionando tus funciones puras para manejar solo las tareas más triviales, dejando la mayoría de su aplicación en un infierno dominado por el estado. :)

@sompylasar "lógica de negocios" y "lógica de actualización de estado" son, en mi opinión, algo parecido.

Sin embargo, para intervenir con mis propios detalles de implementación ... mis acciones son principalmente búsquedas de entradas en la acción. En realidad, todas mis acciones son puras, ya que he trasladado toda mi "lógica empresarial" a un contexto de aplicación.

como ejemplo ... este es mi típico reductor

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
    default:
      return state;
  }
}

Mis acciones típicas:

export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

export function fooResponse( res ) {
  return {
    type: 'FOO_RESPONSE',
    data: {
        isFooing: false,
        isFooed: true,
        fooData: res.data
    }
  };
}

export function fooError( res ) {
  return {
    type: 'FOO_ERROR',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: res.error
    }
  };
}

export function fooReset( res ) {
  return {
    type: 'FOO_RESET',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: null,
        toFoo: true
    }
  };
}

Mi lógica empresarial se define en un objeto almacenado en el contexto, es decir.

export default class FooBar
{
    constructor(store)
    {
        this.actions = bindActionCreators({
            ...fooActions
        }, store.dispatch);
    }

    async getFooData()
    {
        this.actions.fooRequest({
            saidToFoo: true
        });

        fetch(url)
        .then((response) => {
            this.actions.fooResponse(response);
        })
    }
}

Si ve mi comentario anterior, también estaba luchando con el mejor enfoque. Finalmente refactoré y me conformé con pasar la tienda al constructor de mi objeto de aplicación y conectar todas las acciones al despachador en este punto central. Todas las acciones que conoce mi aplicación se asignan aquí.

Ya no uso mapDispatchToProps () en ningún lugar. Para Redux, ahora solo mapStateToProps al crear un componente conectado. Si necesito desencadenar alguna acción ... puedo desencadenarlas a través de mi objeto de aplicación a través del contexto.

class SomeComponent extends React.Component {
    componentWillReceiveProps(nextProps) {
        if (nextProps.someFoo != this.props.someFoo) {
            const { app } = this.context;
            app.actions.getFooData();
        }
    }
}
SomeComponent.contextTypes = {
    app: React.PropTypes.object
};

No es necesario conectar el componente anterior. Todavía puede enviar acciones. Por supuesto, si necesita actualizar el estado dentro del componente, lo convertiría en un componente conectado para asegurarse de que se propague el cambio de estado.

Así es como organicé mi "lógica empresarial" central. Dado que mi estado se mantiene realmente en un servidor backend ... esto funciona muy bien para mi caso de uso.

El lugar donde almacena su "lógica empresarial" depende realmente de usted y de cómo se adapta a su caso de uso.

@jayesbe La siguiente parte significa que no tiene "lógica comercial" en los reductores y, además, la estructura de estado se ha trasladado a los creadores de acciones que crean la carga útil que se transfiere a la tienda a través del reductor:

    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

@jayesbe Mis acciones y reductores son muy similares a los suyos, algunas acciones reciben un objeto de respuesta de red simple como argumento, y encapsulé la lógica de cómo manejar los datos de respuesta en la acción y finalmente devolver un objeto muy simple como valor devuelto y luego pasar a reductor mediante el envío de llamadas (). Como lo hiciste tú. El problema es que si su acción escrita de esta manera, su acción ha hecho casi todo y la responsabilidad de su reductor será muy liviana, ¿por qué tenemos que transferir los datos para almacenarlos manualmente si el reductor simplemente extiende el objeto de acción simplemente? Redux dosificarlo automáticamente para nosotros no es nada difícil.

No necesariamente. Pero la mayor parte del tiempo, parte del proceso empresarial
implica actualizar el estado de la aplicación de acuerdo con las reglas comerciales,
por lo que es posible que tenga que incorporar algo de lógica empresarial.

Para un caso extremo, mira esto:
“Motores síncronos RTS y una historia de desincronización” @ForrestTheWoods
https://blog.forrestthewoods.com/synchronous-rts-engines-and-a-tale-of-desyncs-9d8c3e48b2be

El 5 de abril de 2016 a las 5:54 p.m., "John Babak" [email protected] escribió:

Este es el beneficio de mantener la mayor parte de la lógica de actualización del estado en el
reductor.

@dtinth https://github.com/dtinth Solo para aclarar, diciendo
"lógica de actualización de estado" ¿quiere decir "lógica de negocios"?

-
Recibes esto porque te mencionaron.
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/reactjs/redux/issues/1171#issuecomment -205754910

@LumiaSaki El consejo de mantener sus reductores simples mientras mantiene la lógica compleja en los creadores de acción va en contra de la forma recomendada de usar Redux. El patrón recomendado por Redux es el opuesto: mantenga los creadores de acciones simples mientras mantiene la lógica compleja en los reductores. Por supuesto, eres libre de poner toda tu lógica en acción de todos modos, pero al hacerlo no estás siguiendo el paradigma de Redux, incluso si estás usando Redux.

Por eso, Redux no transferirá automáticamente los datos de las acciones a la tienda. Porque esa no es la forma en que se supone que debes usar Redux. Redux no se cambiará para facilitar su uso de una manera diferente a la prevista. Por supuesto, eres absolutamente libre de hacer lo que funcione para ti, pero no esperes que Redux cambie.

Por lo que vale, produzco mis reductores usando algo como:

let {reducer, actions} = defineActions({
   fooRequest: (state, res) => ({...state, isFooing: true, toFoo: res.saidToFoo}),
   fooResponse: (state, res) => ({...state, isFooing: false, isFooed: true, fooData: res.data}),
   fooError: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: res.error})
   fooReset: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: null, toFoo: false})
})

defineActions devuelve tanto el reductor como los creadores de acciones. De esta manera, me resulta muy fácil mantener mi lógica de actualización dentro del reductor sin tener que pasar mucho tiempo escribiendo creadores de acciones triviales.

Si insiste en mantener su lógica en sus creadores de acciones, entonces no debería tener ningún problema para automatizar los datos usted mismo. Su reductor es una función y puede hacer lo que quiera. Entonces su reductor podría ser tan simple como:

function reducer(state, action) {
    if (action.data) {
        return {...state, ...action.data}
   } else {
        return state;
   }
}

Ampliando los puntos anteriores con respecto a la lógica empresarial, creo que puede separar su lógica empresarial en dos partes:

  • La parte no determinista. Esta parte hace uso de servicios externos, código asincrónico, hora del sistema o un generador de números aleatorios. En mi opinión, esta parte se maneja mejor con una función de E / S (o un creador de acciones). Yo los llamo el proceso empresarial.

Considere un software similar a IDE donde el usuario puede hacer clic en el botón Ejecutar, y compilaría y ejecutaría la aplicación. (Aquí uso una función asíncrona que toma una tienda, pero puedes usar redux-thunk lugar).

js export async function runApp (store) { try { store.dispatch({ type: 'startCompiling' }) const compiledApp = await compile(store) store.dispatch({ type: 'startRunning', app: compiledApp }) } catch (e) { store.dispatch({ type: 'errorCompiling', error: e }) } }

  • La parte determinista. Esta parte tiene un resultado totalmente predecible. Dado el mismo estado y el mismo evento, el resultado siempre es predecible. En mi opinión, esta parte se maneja mejor con un reductor. Yo llamo a esto las reglas comerciales.

`` js
importar u desde 'updeep'

exportar const reducer = createReducer ({
// [nombre de la acción]: action => currentState => nextState
startCompiling: () => u ({compiling: true}),
errorCompiling: ({error}) => u ({compiling: false, compileError: error}),
startRunning: ({app}) => u ({
en ejecución: () => aplicación,
compilación: falso
}),
stopRunning: () => u ({running: false}),
discardCompileError: () => u ({compileError: null}),
// ...
})
''

Intento poner tanto código en esta tierra determinista como sea posible, teniendo en cuenta que la única responsabilidad del reductor es mantener la coherencia del estado de la aplicación, dadas las acciones entrantes. Nada mas. Cualquier cosa además de eso, lo haría fuera de Redux, porque Redux es solo un contenedor de estado.

@dtinth Genial, porque el ejemplo anterior en https://github.com/reactjs/redux/issues/1171#issuecomment -205782740 se ve totalmente diferente a lo que ha escrito en https://github.com/reactjs/redux/ issues / 1171 # issuecomment -205888533 - sugiere construir una parte de los creadores de estado en acción y pasarlos a los reductores para que solo difundan las actualizaciones (este enfoque me parece incorrecto, y estoy de acuerdo con lo mismo señalado en https://github.com/reactjs/redux/issues/1171#issuecomment-205865840).

@winstonewert

El patrón recomendado por Redux es el opuesto: mantenga los creadores de acciones simples mientras mantiene la lógica compleja en los reductores.

¿Cómo puedes poner lógica compleja en los ruducers y aún mantenerlos puros?

Si estoy llamando a fetch () por ejemplo y cargando datos desde el servidor ... entonces los proceso de alguna manera. Todavía tengo que ver un ejemplo de un reductor que tenga "lógica compleja"

@jayesbe : Uh ... "complejo" y "puro" son ortogonales. Puede tener una lógica o manipulación condicional _realmente_ complicada dentro de un reductor, y siempre que sea solo una función de sus entradas sin efectos secundarios, sigue siendo pura.

Si tiene un estado local complejo (piense en un editor de publicaciones, una vista de árbol, etc.) o maneja cosas como actualizaciones optimistas, sus reductores contendrán una lógica compleja. Realmente depende de la aplicación. Algunos tienen solicitudes complejas, otros tienen actualizaciones de estado complejas. Algunos tienen ambos :-)

@markerikson ok Las declaraciones lógicas son una cosa ... pero ¿ejecutar tareas específicas? Por ejemplo, tengo una acción que en un caso desencadena otras tres acciones, o en otro caso desencadena dos acciones distintas y separadas. Esa lógica + ejecución de tareas no parece que deban ir en reductores.

Mi estado de datos / modelo está en el servidor, el estado de vista es distinto del modelo de datos, pero la administración de ese estado está en el cliente. El estado de mi modelo de datos simplemente se transmite a la vista ... que es lo que hace que mis reductores y acciones sean tan esbeltos.

@jayesbe : No creo que nadie haya dicho nunca que la activación de otras acciones debería ir en un reductor. Y de hecho, no debería. El trabajo de un reductor es simplemente (currentState + action) -> newState .

Si necesita unir varias acciones, hágalo en algo como un thunk o una saga y las dispare en secuencia, o tenga algo que escuche los cambios de estado, o use un middleware para interceptar una acción y hacer un trabajo adicional.

Estoy un poco confundido sobre cuál es la discusión en este momento, para ser honesto.

@markerikson, el tema parece ser sobre la "lógica empresarial" y hacia dónde va. El hecho es que ... todo depende de la aplicación. Hay diferentes formas de hacerlo. Algunos más complejos que otros. Si encuentra una buena manera de resolver su problema que le facilite el mantenimiento y la organización. Eso es todo lo que realmente importa. Mi implementación resulta ser muy ajustada para mi caso de uso, incluso si va en contra del paradigma.

Como nota, todo lo que realmente importa es lo que funciona para usted. Pero así es como abordaría los problemas sobre los que me preguntaste.

Si estoy llamando a fetch () por ejemplo y cargando datos desde el servidor ... entonces los proceso de alguna manera. Todavía tengo que ver un ejemplo de un reductor que tenga "lógica compleja"

Mi reductor toma una respuesta sin procesar de mi servidor y actualiza mi estado con ella. De esa manera, la respuesta de procesamiento de la que habla se realiza en mi reductor. Por ejemplo, la solicitud podría ser buscar registros JSON para mi servidor, que el reductor guarda en mi caché de registros local.

k enunciados logicos son una cosa .. pero ejecutan tareas especificas? Por ejemplo, tengo una acción que en un caso desencadena otras tres acciones, o en otro caso desencadena dos acciones distintas y separadas. Esa lógica + ejecución de tareas no parece que deban ir en reductores.

Eso depende de lo que estés haciendo. Obviamente, en el caso de búsqueda del servidor, una acción activará otra. Eso está completamente dentro del procedimiento recomendado de Redux. Sin embargo, también podría estar haciendo algo como esto:

function createFoobar(dispatch, state, updateRegistry) {
   dispatch(createFoobarRecord());
   if (updateRegistry) {
      dispatch(updateFoobarRegistry());
   } else {
       dispatch(makeFoobarUnregistered());
   }
   if (hasFoobarTemps(state)) {
      dispatch(dismissFoobarTemps());
   }
}

Esta no es la forma recomendada de usar Redux. La forma recomendada de Redux es tener una sola acción CREATE_FOOBAR que cause todos estos cambios deseados.

@winstonewert :

Esta no es la forma recomendada de usar Redux. La forma recomendada de Redux es tener una sola acción CREATE_FOOBAR que cause todos estos cambios deseados.

¿Tiene un puntero a algún lugar especificado? Porque cuando estaba investigando para la página de preguntas frecuentes, lo que se me ocurrió fue "depende", directamente de Dan. Vea http://redux.js.org/docs/FAQ.html#actions -multiple-actions y esta respuesta de Dan en SO .

La "lógica empresarial" es un término bastante amplio. Puede cubrir cosas como "¿Ha sucedido algo?", "¿Qué hacemos ahora que esto sucedió?", "¿Es válido?", Etc. Basado en el diseño de Redux, esas preguntas _pueden_ ser respondidas en varios lugares dependiendo de cuál sea la situación, aunque yo vería "¿ha sucedido?" Como una responsabilidad de creador de acción, y "qué ahora" es casi definitivamente una responsabilidad de reducción.

En general, mi opinión sobre esta _ cuestión completa_ de "lógica empresarial" es: _ "depende _". Hay razones por las que es posible que desee realizar el análisis de solicitudes en un creador de acciones y razones por las que es posible que desee hacerlo en un reductor. Hay ocasiones en las que su reductor puede ser simplemente "tomar este objeto y colocarlo en mi estado", y otras ocasiones en las que su reductor puede ser una lógica condicional muy compleja. Hay momentos en los que su creador de acciones puede ser muy simple y otros momentos en los que puede ser complejo. Hay ocasiones en las que tiene sentido enviar varias acciones seguidas para representar los pasos de un proceso, y otras ocasiones en las que solo desea enviar una acción genérica "THING_HAPPENED" para representarlo todo.

La única regla estricta con la que estoy de acuerdo es "el no determinismo en los creadores de acción, el determinismo puro en los reductores". Eso es un hecho.

¿Aparte de eso? Encuentre algo que funcione para usted. Se consistente. Sepa por qué lo está haciendo de cierta manera. Vaya con eso.

aunque yo vería "¿ha sucedido?" más como una responsabilidad del creador de la acción, y "lo que ahora" es casi definitivamente una responsabilidad reductora.

Es por eso que hay una discusión paralela sobre cómo poner los efectos secundarios, es decir, la parte no pura del "qué ahora", en reductores: # 1528 y convertirlos en meras descripciones de lo que debería suceder, como las próximas acciones a enviar.

El patrón que he estado usando es:

  • Qué hacer : Acción / Creador de acciones
  • Cómo hacerlo : Middleware (por ejemplo, middleware que escucha acciones 'asíncronas' y realiza llamadas a mi objeto API).
  • Qué hacer con el resultado : reductor

Desde antes en este hilo, la declaración de Dan fue:

Por lo tanto, nuestra recomendación oficial es que primero debe intentar que diferentes reductores respondan a las mismas acciones. Si se vuelve incómodo, entonces claro, cree creadores de acciones separados. Pero no empieces con este enfoque.

Por eso, entiendo que el enfoque recomendado es enviar una acción por evento. Pero, pragmáticamente, haga lo que funcione.

@winstonewert : Dan se refiere al patrón de "composición reductora", es decir, "es una acción que solo un reductor escucha" frente a "muchos reductores pueden responder a la misma acción". Dan es muy aficionado a los reductores arbitrarios que responden a una sola acción. Otros prefieren cosas como el enfoque de "patos", donde los reductores y las acciones están MUY estrechamente agrupados, y solo un reductor maneja una acción determinada. Entonces, ese ejemplo no se trata de "enviar múltiples acciones en secuencia", sino de "cuántas partes de mi estructura reductora esperan responder a esto".

Pero, pragmáticamente, haga lo que funcione.

: +1:

@sompylasar Veo el error de mis caminos al tener la estructura de estado en mis Acciones. Puedo cambiar fácilmente la estructura del estado a mis reductores y simplificar mis acciones. Salud.

Me parece que es lo mismo.

O tiene una sola acción que activa múltiples reductores que causan múltiples cambios de estado, o tiene múltiples acciones, cada una de las cuales activa un solo reductor que causa un solo cambio de estado. Tener múltiples reductores respondiendo a una acción y hacer que un evento envíe múltiples acciones son soluciones alternativas al mismo problema.

En la pregunta de StackOverflow que menciona, afirma:

Mantenga el registro de acciones lo más cerca posible del historial de interacciones del usuario. Sin embargo, si hace que los reductores sean difíciles de implementar, considere dividir algunas acciones en varias, si una actualización de la interfaz de usuario se puede pensar en dos operaciones separadas que simplemente están juntas.

Como yo lo veo, Dan respalda mantener una acción por interacción de usuario como la forma ideal. Pero es pragmático, cuando hace que el reductor sea difícil de implementar, respalda la división de la acción.

Estoy visualizando un par de casos de uso similares pero algo diferentes aquí:

1) Una acción requiere actualizaciones en múltiples áreas de su estado, particularmente si está usando combineReducers para tener funciones de reducción separadas que manejen cada subdominio. Vos si:

  • hacer que tanto el Reductor A como el Reductor B respondan a la misma acción y actualicen sus bits de estado de forma independiente
  • haga que su procesador incluya TODOS los datos relevantes en la acción para que cualquier reductor pueda acceder a otros bits de estado fuera de su propio fragmento
  • agregar otro reductor de nivel superior que agarre trozos del estado del Reductor A y, en un caso especial, entregárselo al Reductor B?
  • enviar una acción destinada al Reductor A con los bits de estado que necesita, y una segunda acción al Reductor B con lo que necesita?

2) Tiene un conjunto de pasos que suceden en una secuencia específica, cada paso requiere alguna actualización de estado o acción de middleware. Vos si:

  • ¿Desea enviar cada paso individual como una acción separada para representar el progreso y dejar que los reductores individuales respondan a acciones específicas?
  • ¿Despachar un gran evento y confiar en que los reductores lo manejen adecuadamente?

Así que sí, definitivamente algo de superposición, pero creo que parte de la diferencia en la imagen mental aquí son los diversos casos de uso.

@markerikson Entonces, su consejo es 'depende de la situación en la que se encuentre', y cómo equilibrar la 'lógica empresarial' en las acciones o reductores solo depende de su consideración, también deberíamos aprovechar los beneficios de la función pura tanto como sea posible.

Sí. Los reductores _deben_ ser puros, como requisito de Redux (excepto en el 0,00001% de casos especiales). Los creadores de acción absolutamente _no _ tienen que ser puros, y de hecho es donde vivirá la mayoría de sus "impurezas". Sin embargo, dado que las funciones puras son obviamente más fáciles de entender y probar que las funciones impuras, _si_ puedes hacer que parte de tu lógica de creación de acciones sea pura, ¡genial! Si no, está bien.

Y sí, desde mi punto de vista, depende de usted, como desarrollador, determinar cuál es el equilibrio adecuado para la lógica de su propia aplicación y dónde vive. No existe una única regla estricta para determinar de qué lado de la división de creador / reductor de acción debería vivir. (Erm, excepto por el "determinismo / no-determinismo" que mencioné anteriormente. A lo que claramente quise hacer referencia en este comentario. Obviamente).

@cpsubrian

Qué hacer con el resultado: reductor

En realidad, esta es la razón por la que las sagas son para: lidiar con efectos como "si esto sucedió, entonces eso también debería suceder".


@markerikson @LumiaSaki

Los creadores de acción no tienen que ser puros en absoluto, y de hecho es donde vivirán la mayoría de tus "impurezas".

En realidad, los creadores de acciones ni siquiera están obligados a ser impuros o incluso a existir.
Ver http://stackoverflow.com/a/34623840/82609

Y sí, desde mi punto de vista, depende de usted, como desarrollador, determinar cuál es el equilibrio adecuado para la lógica de su propia aplicación y dónde vive. No existe una única regla estricta para determinar de qué lado de la división de creador / reductor de acción debería vivir.

Sí, pero no es tan obvio notar los inconvenientes de cada enfoque sin experiencia :) Vea también mi comentario aquí: https://github.com/reactjs/redux/issues/1171#issuecomment -167585575

Ninguna regla estricta funciona bien para la mayoría de las aplicaciones simples, pero si desea crear componentes reutilizables, estos componentes no deben ser conscientes de algo fuera de su propio alcance.

Entonces, en lugar de definir una lista de acciones global para toda su aplicación, puede comenzar a dividir su aplicación en componentes reutilizables y cada componente tiene su propia lista de acciones, y solo puede enviarlas / reducirlas. El problema es entonces, ¿cómo se expresa "cuando se selecciona la fecha en mi selector de fechas, entonces deberíamos guardar un recordatorio en ese elemento de tareas pendientes, mostrar un brindis de comentarios y luego navegar por la aplicación a todos con recordatorios": aquí es donde la saga entra en acción: orquestando los componentes

Consulte también https://github.com/slorber/scalable-frontend-with-elm-or-redux

Y sí, desde mi punto de vista, depende de usted, como desarrollador, determinar cuál es el equilibrio adecuado para la lógica de su propia aplicación y dónde vive. No existe una única regla estricta para determinar de qué lado de la división de creador / reductor de acción debería vivir.

Sí, Redux no exige que pongas tu lógica en los reductores o en los creadores de acciones. Redux no se romperá de ninguna manera. No existe una regla estricta y rápida que requiera que lo hagas de una forma u otra. Pero la recomendación de Dan fue "Mantenga el registro de acciones lo más cerca posible del historial de interacciones de los usuarios". No es necesario enviar una sola acción por evento de usuario, pero se recomienda.

En mi caso tengo 2 reductores interesados ​​en 1 acción. La action.data en bruto no es suficiente. Necesitan manejar datos transformados. No quería realizar la transformación en los 2 reductores. Entonces moví la función para realizar la transformación a un procesador. De esta forma mis reductores reciben un dato listo para el consumo. Esto es lo mejor que pude pensar en mi corta experiencia de 1 mes con redux.

¿Qué hay de desacoplar los componentes / vistas de la estructura de la tienda? mi objetivo es que todo lo que se vea afectado por la estructura de la tienda se administre en los reductores, por eso me gusta colocar selectores con reductores, por lo que los componentes realmente no necesitan saber cómo obtener un nodo en particular de la tienda.

Eso es genial para pasar datos a los componentes, ¿y al revés, cuando los componentes envían acciones?

Digamos que, por ejemplo, en una aplicación Todo, estoy actualizando el nombre de un elemento Todo, por lo que envío una acción que pasa la parte del elemento que quiero actualizar, es decir:

dispatch(updateItem({name: <text variable>}));

, y la definición de acción es:

const updateItem = (updatedData) => {type: "UPDATE_ITEM", updatedData}

que a su vez es manejado por el reductor que simplemente podría hacer:

Object.assign({}, item, action.updatedData)

para actualizar el artículo.

Esto funciona muy bien ya que puedo reutilizar la misma acción y reductor para actualizar cualquier accesorio del elemento Todo, es decir:

updateItem({description: <text variable>})

cuando se cambia la descripción.

Pero aquí el componente necesita saber cómo se define un ítem Todo en la tienda y si esa definición cambia debo recordar cambiarlo en todos los componentes que dependen de él, lo cual obviamente es una mala idea, ¿alguna sugerencia para este escenario?

@dcoellarb

Mi solución en este tipo de situación es aprovechar la flexibilidad de Javascript para generar lo que sería repetitivo.

Entonces podría tener:

const {reducer, actions, selector} = makeRecord({
    name: TextField,
    description: TextField,
    completed: BooleanField
})

Donde makeRecord es una función para construir automáticamente reductores, creadores de acciones y selectores a partir de mi descripción. Eso elimina el texto estándar, pero si necesito hacer algo que no se ajuste a este patrón ordenado más adelante, puedo agregar reductor / acciones / selector personalizados al resultado de makeRecord.

tks @winstonewert me gusta el enfoque para evitar la repetición, puedo ver que se ahorra mucho tiempo en aplicaciones con muchos modelos; pero todavía no veo cómo esto desacoplará el componente de la estructura de la tienda, quiero decir, incluso si se genera la acción, el componente aún necesitará pasarle los campos actualizados, lo que significa que el componente aún necesita conocer la estructura de la tienda, ¿verdad?

@winstonewert @dcoellarb En mi opinión, la estructura de carga útil de la acción debería pertenecer a las acciones, no a los reductores, y traducirse explícitamente en la estructura del estado en un reductor. Debería ser una coincidencia afortunada que estas estructuras se reflejen entre sí por su simplicidad inicial. No es necesario que estas dos estructuras se reflejen siempre entre sí porque no son la misma entidad.

@sompylasar correcto, hago eso, estructura de la

@dcoellarb Puede pensar en sus vistas como entradas de datos de cierto tipo (como una cadena o un número, pero un objeto estructurado con campos). Este objeto de datos no refleja necesariamente la estructura de la tienda. Pones este objeto de datos en una acción. Esto no se acopla con la estructura de la tienda. Tanto el almacenamiento como la vista deben estar acoplados con la estructura de carga útil de la acción.

@sompylasar tiene sentido, lo intentaré, muchas gracias !!!

Probablemente también debería agregar que puede hacer que las acciones sean más puras usando redux-saga . Sin embargo, redux-saga tiene dificultades para manejar eventos asíncronos, por lo que puede llevar esta idea un paso más allá usando RxJS (o cualquier biblioteca FRP) en lugar de redux-saga. Aquí hay un ejemplo usando KefirJS: https://github.com/awesome-editor/awesome-editor/blob/saga-demo/src/stores/app/AppSagaHandlers.js

Hola @frankandrobot ,

redux-saga tiene problemas para manejar eventos asíncronos

¿Qué quiere decir con esto? ¿No está redux-saga hecho para manejar eventos asíncronos y efectos secundarios de una manera elegante? Eche un vistazo a https://github.com/reactjs/redux/issues/1171#issuecomment -167715393

No @IceOnFire . La última vez que leí los documentos redux-saga , manejar flujos de trabajo asíncronos complejos es difícil. Ver, por ejemplo: http://yelouafi.github.io/redux-saga/docs/advanced/NonBlockingCalls.html
Dijo (¿todavía dice?) Algo en el sentido

dejaremos el resto de los detalles al lector porque se está empezando a complicar

Compare eso con la forma FRP: https://github.com/frankandrobot/rflux/blob/master/doc/06-sideeffects.md#a -more-complex-workflow
Todo ese flujo de trabajo se maneja por completo. (En solo unas pocas líneas podría agregar). Además de eso, todavía obtienes la mayor parte de las bondades de redux-saga (todo es una función pura, en su mayoría pruebas unitarias fáciles).

La última vez que pensé en esto, llegué a la conclusión de que el problema es que redux-saga hace que todo parezca sincrónico. Es excelente para flujos de trabajo simples, pero para flujos de trabajo asíncronos complejos, es más fácil si maneja el asíncrono explícitamente ... que es en lo que se destaca FRP.

Hola @frankandrobot ,

Gracias por tu explicación. No veo la cita que mencionaste, tal vez la biblioteca evolucionó (por ejemplo, ahora veo un efecto cancel que nunca vi antes).

Si los dos ejemplos (saga y FRP) se comportan exactamente igual, entonces no veo mucha diferencia: uno es una secuencia de instrucciones dentro de bloques try / catch, mientras que el otro es una cadena de métodos en streams. Debido a mi falta de experiencia en las transmisiones, incluso encuentro más legible el ejemplo de la saga, y más comprobable, ya que puedes probar cada yield uno por uno. Pero estoy bastante seguro de que esto se debe a mi forma de pensar más que a las tecnologías.

De todos modos me encantaría conocer la opinión de @yelouafi al respecto.

@bvaughn, ¿ puede señalar algún ejemplo decente de acción de prueba, reductor, selector en la misma prueba que describe aquí?

La forma más eficiente de probar acciones, reductores y selectores es seguir el enfoque de "patos" al escribir pruebas. Esto significa que debe escribir un conjunto de pruebas que cubra un conjunto determinado de acciones, reductores y selectores en lugar de 3 conjuntos de pruebas que se centren en cada uno de forma individual. Esto simula con mayor precisión lo que sucede en su aplicación real y proporciona el máximo rendimiento.

Hola @ morgs32 😄

Este problema es un poco antiguo y no he usado Redux por un tiempo. Hay una sección en el sitio de Redux sobre pruebas de escritura que puede que desee consultar.

Básicamente, solo estaba señalando que, en lugar de escribir pruebas para acciones y reductores de forma aislada, puede ser más eficiente escribirlos juntos, así:

import configureMockStore from 'redux-mock-store'
import { actions, selectors, reducer } from 'your-redux-module';

it('should support adding new todo items', () => {
  const mockStore = configureMockStore()
  const store = mockStore({
    todos: []
  })

  // Start with a known state
  expect(selectors.todos(store.getState())).toEqual([])

  // Dispatch an action to modify the state
  store.dispatch(actions.addTodo('foo'))

  // Verify the action & reducer worked successfully using your selector
  // This has the added benefit of testing the selector also
  expect(selectors.todos(store.getState())).toEqual(['foo'])
})

Esta fue solo mi propia observación después de usar Redux durante unos meses en un proyecto. No es una recomendación oficial. YMMV. 👍

"Haga más en los creadores de acciones y menos en los reductores"

¿Qué pasa si la aplicación es servidor y cliente y el servidor debe contener lógica empresarial y validadores?
Así que envío la acción como está, y la mayor parte del trabajo se realizará en el lado del servidor mediante el reductor ...

Primero, lo siento por mi inglés. Pero tengo opiniones diferentes.

Mi elección es fat reducer, thin creadores de acciones.

Mis creadores de acciones solo __dispatch__ acciones (async, sync, serial-async, paralelo-async, paralelo-async en for-loop) basadas en algún __promise middleware__.

Mis reductores se dividen en muchos sectores pequeños para manejar la lógica empresarial. use combineReduers combínelos. reducer es __función pura__, por lo que es fácil de reutilizar. Tal vez algún día use angularJS, creo que puedo reutilizar mi reducer en mi servicio para la misma lógica comercial. Si su reducer tiene muchos códigos de líneas, tal vez pueda dividirse en un reductor más pequeño o abstraer algunas funciones.

Sí, hay algunos casos entre estados cuyos significados A dependen de B, C .. y B, C son datos asíncronos. Debemos usar B,C para completar o inicializar A. Por eso uso crossSliceReducer .

Acerca de __Haga más en creadores de acción y menos en reductores__.

  1. Si usa redux-thunk o etc. Sí. Puede acceder al estado completo dentro de los creadores de acciones por getState() . Ésta es una elección. O bien, puede crear algunos __crossSliceReducer__, para que también pueda acceder al estado completo, puede usar algún segmento de estado para calcular su otro estado.

Acerca de __Unit testing__

reducer es __ función pura__. Así que es fácil de probar. Por ahora, solo pruebo mis reductores, porque es más importante que otra parte.

Para probar action creators ? Creo que si son "gordos", tal vez no sea fácil probarlos. Especialmente los __creadores de acciones asincrónicas__.

Estoy de acuerdo contigo @mrdulin, así es como yo también he ido.

@mrdulin Sí. Parece que el middleware es el lugar adecuado para colocar su lógica impura.
Pero para la lógica empresarial, el reductor no parece el lugar adecuado. Terminará con múltiples acciones "sintéticas" que no representan lo que el usuario ha preguntado, sino lo que requiere su lógica comercial.

Una opción mucho más simple es simplemente llamar a algunas funciones puras / métodos de clase desde el middleware:

middleware = (...) => {
  // if(action.type == 'HIGH_LEVEL') 
  handlers[action.name]({ dispatch, params: action.payload })
}
const handlers = {
  async highLevelAction({ dispatch, params }) {
    dispatch({ loading: true });
    const data = await api.getData(params.someId);
    const processed = myPureLogic(data);
    dispatch({ loading: false, data: processed });
  }
}

@bvaughn

Esto reduce la probabilidad de variables mal escritas que conducen a valores sutiles indefinidos, simplifica los cambios en la estructura de su tienda, etc.

Mi caso en contra del uso de selectores en todas partes, incluso para piezas de estado triviales que no requieren memorización o ningún tipo de transformación de datos:

  • ¿No deberían las pruebas unitarias para reductores detectar propiedades estatales mal escritas?
  • Según mi experiencia, los state.stuff.item1 a state.stuff.item2 , busca en el código y lo cambia en todas partes, como si realmente cambiara el nombre de cualquier otra cosa. Es una tarea común y una obviedad para las personas que usan IDE especialmente.
  • Usar selectores en todas partes es una especie de abstracción vacía. simples pedazos de Estado. Claro, gana consistencia al tener esta API para acceder al estado, pero renuncia a la simplicidad.

Obviamente, los selectores son necesarios, pero me gustaría escuchar algunos otros argumentos para convertirlos en una API obligatoria.

Desde un punto de vista práctico, una buena razón en mi experiencia es que las bibliotecas como reselect o redux-saga aprovechan los selectores para acceder a partes del estado. Esto es suficiente para que me quede con los selectores.

Hablando filosóficamente, siempre veo a los selectores como los "métodos captadores" del mundo funcional. Y por la misma razón por la que nunca accedería a los atributos públicos de un objeto Java, nunca accedería a subestados directamente en una aplicación Redux.

@IceOnFire No hay nada que aprovechar si el cálculo no es caro o no se requiere la transformación de datos.

Los métodos getter pueden ser una práctica común en Java, pero también lo es acceder a los POJO directamente en JS.

@timotgl

¿Por qué hay una API entre la tienda y otro código redux?

Los selectores son una API pública de consulta (lectura) de un reductor, las acciones son una API pública de comando (escritura) de un reductor. La estructura del reductor es su detalle de implementación.

Los selectores y las acciones se utilizan en la capa de interfaz de usuario y en la capa de saga (si usa redux-saga), no en el reductor en sí.

@sompylasar No estoy seguro de seguir su punto de vista aquí. No hay alternativa a las acciones, debo usarlas para interactuar con redux. Sin embargo, no tengo que usar selectores, solo puedo elegir algo directamente del estado cuando está expuesto, que es por diseño.

Estás describiendo una forma de pensar en los selectores como la API de "lectura" de un reductor, pero mi pregunta era qué justifica hacer que los selectores sean una API obligatoria en primer lugar (obligatorio como para hacer cumplir eso como una mejor práctica en un proyecto, no por biblioteca diseño).

@timotgl Sí, los selectores no son obligatorios. Pero hacen una buena práctica para prepararse para futuros cambios en el código de la aplicación, lo que hace posible refactorizarlos por separado:

  • cómo se estructura y se escribe cierta parte del estado (la preocupación del reductor, un lugar)
  • cómo se usa esa parte del estado (la interfaz de usuario y las preocupaciones sobre los efectos secundarios, muchos lugares desde donde se consulta la misma parte del estado)

Cuando esté a punto de cambiar la estructura de la tienda, sin selectores, tendrá que buscar y refactorizar todos los lugares donde se accede a las partes de estado afectadas, y esto podría ser una tarea no trivial, no simplemente buscar y ... reemplace, especialmente si pasa los fragmentos de estado obtenidos directamente de la tienda, no a través de un selector.

@sompylasar Gracias por tu aporte.

esto podría ser una tarea potencialmente no trivial

Esa es una preocupación válida, simplemente me parece una compensación costosa. Supongo que no me he encontrado con una refactorización estatal que haya causado tales problemas. Sin embargo, me he encontrado con "selectores espaguetis", donde los selectores anidados para cada subparte trivial de estado causaron bastante confusión. Después de todo, esta contramedida en sí misma también debe mantenerse. Pero ahora entiendo mejor la razón detrás de esto.

@timotgl Un ejemplo simple que puedo compartir públicamente:

export const PROMISE_REDUCER_STATE_IDLE = 'idle';
export const PROMISE_REDUCER_STATE_PENDING = 'pending';
export const PROMISE_REDUCER_STATE_SUCCESS = 'success';
export const PROMISE_REDUCER_STATE_ERROR = 'error';

export const PROMISE_REDUCER_STATES = [
  PROMISE_REDUCER_STATE_IDLE,
  PROMISE_REDUCER_STATE_PENDING,
  PROMISE_REDUCER_STATE_SUCCESS,
  PROMISE_REDUCER_STATE_ERROR,
];

export const PROMISE_REDUCER_ACTION_START = 'start';
export const PROMISE_REDUCER_ACTION_RESOLVE = 'resolve';
export const PROMISE_REDUCER_ACTION_REJECT = 'reject';
export const PROMISE_REDUCER_ACTION_RESET = 'reset';

const promiseInitialState = { state: PROMISE_REDUCER_STATE_IDLE, valueOrError: null };
export function promiseReducer(state = promiseInitialState, actionType, valueOrError) {
  switch (actionType) {
    case PROMISE_REDUCER_ACTION_START:
      return { state: PROMISE_REDUCER_STATE_PENDING, valueOrError: null };
    case PROMISE_REDUCER_ACTION_RESOLVE:
      return { state: PROMISE_REDUCER_STATE_SUCCESS, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_REJECT:
      return { state: PROMISE_REDUCER_STATE_ERROR, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_RESET:
      return { ...promiseInitialState };
    default:
      return state;
  }
}

export function extractPromiseStateEnum(promiseState = promiseInitialState) {
  return promiseState.state;
}
export function extractPromiseStarted(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_PENDING);
}
export function extractPromiseSuccess(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS);
}
export function extractPromiseSuccessValue(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS ? promiseState.valueOrError || null : null);
}
export function extractPromiseError(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_ERROR ? promiseState.valueOrError || true : null);
}

No es la preocupación de este usuario reductor si lo que se almacena es state, valueOrError o algo más. Se exponen la cadena de estado (enumeración), un par de comprobaciones de uso frecuente sobre ese estado, el valor y el error.

Sin embargo, me he encontrado con "selectores espaguetis", donde los selectores anidados para cada subparte trivial de estado causaron bastante confusión.

Si este anidamiento fue causado por reflejar el anidamiento del reductor (composición), eso no es lo que recomendaría, ese es el detalle de implementación del reductor. Usando el ejemplo anterior, los reductores que usan promiseReducer para algunas partes de su estado exportan sus propios selectores nombrados de acuerdo con estas partes. Además, no todas las funciones que se parecen a un selector deben exportarse y ser parte de la API reductora.

function extractTransactionLogPromiseById(globalState, transactionId) {
  return extractState(globalState).transactionLogPromisesById[transactionId] || undefined;
}

export function extractTransactionLogPromiseStateEnumByTransactionId(globalState, transactionId) {
  return extractPromiseStateEnum(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogPromiseErrorTransactionId(globalState, transactionId) {
  return extractPromiseError(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogByTransactionId(globalState, transactionId) {
  return extractPromiseSuccessValue(extractTransactionLogPromiseById(globalState, transactionId));
}

Oh, una cosa más que casi me olvido. Los nombres de funciones y las exportaciones / importaciones se pueden minimizar de forma correcta y segura. Claves de objetos anidados: no tanto, necesita un rastreador de flujo de datos adecuado en el minificador para no estropear el código.

@timotgl : muchas de nuestras mejores prácticas recomendadas con Redux tratan de encapsular la lógica y el comportamiento relacionados con Redux.

Por ejemplo, puede enviar acciones directamente desde un componente conectado, haciendo this.props.dispatch({type : "INCREMENT"}) . Sin embargo, lo desaconsejamos, porque obliga al componente a "saber" que está hablando con Redux. Una forma más idiomática de React de hacer las cosas es pasar los creadores de acciones vinculados, de modo que el componente pueda simplemente llamar a this.props.increment() , y no importa si esa función es un creador de acciones de Redux vinculado, se pasa una devolución de llamada por un padre, o una función simulada en una prueba.

También puede escribir a mano tipos de acciones en todas partes, pero recomendamos definir variables constantes para que se puedan importar, rastrear y disminuir la posibilidad de errores tipográficos.

Del mismo modo, no hay nada que le impida acceder a state.some.deeply.nested.field en sus funciones o procesadores mapState . Pero, como ya se ha descrito en este hilo, esto aumenta las posibilidades de errores tipográficos, hace que sea más difícil rastrear los lugares en los que se está utilizando una pieza de estado en particular, hace que la refactorización sea más difícil y significa que cualquier lógica de transformación costosa probablemente sea reestructurada. -corriendo cada vez, incluso si no es necesario.

Así que no, no tienes que usar selectores, pero son una buena práctica de arquitectura.

Es posible que desee leer mi publicación Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance .

@markerikson No estoy discutiendo contra los selectores en general, o los selectores para cálculos costosos. Y nunca tuve la intención de pasar el envío a un componente :)

Mi punto fue que no estoy de acuerdo con esta creencia:

Idealmente, solo sus funciones reductoras y selectores deberían conocer la estructura de estado exacta, por lo que si cambia el lugar donde vive algún estado, solo necesitaría actualizar esas dos piezas de lógica.

Acerca de su ejemplo con state.some.deeply.nested.field , puedo ver el valor de tener un selector para acortar esto. selectSomeDeeplyNestedField() es un nombre de función frente a 5 propiedades que podría equivocarme.

Por otro lado, si sigue esta guía al pie de la letra, también está haciendo const selectSomeField = state => state.some.field; o incluso const selectSomething = state => state.something; , y en algún momento los gastos generales (importación, exportación, prueba) de hacer esto de manera constante ya no justifica la (discutible) seguridad y pureza en mi opinión. Tiene buenas intenciones, pero no puedo deshacerme del espíritu dogmático de la directriz. Confiaría en que los desarrolladores de mi proyecto usen los selectores de manera inteligente y cuando corresponda, porque mi experiencia hasta ahora ha sido que lo hacen.

Sin embargo, bajo ciertas condiciones, pude ver por qué querrías errar por el lado de la seguridad y las convenciones. Gracias por su tiempo y compromiso, en general estoy muy feliz de que tengamos redux.

Seguro. FWIW, hay otras bibliotecas de selectores y también envoltorios alrededor de Reselect . Por ejemplo, https://github.com/planttheidea/selectorator le permite definir rutas de clave de notación de puntos, y realiza los selectores intermedios por usted internamente.

¿Fue útil esta página
0 / 5 - 0 calificaciones