Redux: Intentando poner las llamadas a la API en el lugar correcto

Creado en 20 jul. 2015  ·  115Comentarios  ·  Fuente: reduxjs/redux

Estoy tratando de hacer que el inicio de sesión sea un éxito / error, pero mi principal preocupación es dónde puedo poner esta lógica.

Actualmente estoy usando actions -> reducer (switch case with action calling API) -> success/error on response triggering another action .

El problema con este enfoque es que el reductor no funciona cuando llamo a la acción desde la llamada API.

¿Me estoy perdiendo de algo?

Reductor

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';
import Immutable from 'immutable';
import LoginApiCall from '../utils/login-request';

const initialState = new Immutable.Map({
  email: '',
  password: '',
}).asMutable();

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      console.log(action.user);
      LoginApiCall.login(action.user);
      return state;
    case LOGGED_FAILED:
      console.log('failed from reducer');
      return state;
    case LOGGED_SUCCESSFULLY:
      console.log('success', action);
      console.log('success from reducer');
      break;
    default:
      return state;
  }
}

comportamiento

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return dispatch => {
    dispatch({ error, type: LOGGED_FAILED });
  };
}

/*
 * Should add the route like parameter in this method
*/
export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard'); // will fire CHANGE_ROUTE in its change handler
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return dispatch => {
    dispatch({ user, type: LOGIN_ATTEMPT });
  };
}

Llamadas a la API

 // Use there fetch polyfill
 // The main idea is create a helper in order to handle success/error status
import * as LoginActions from '../actions/LoginActions';

const LoginApiCall = {
  login(userData) {
    fetch('http://localhost/login', {
      method: 'post',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userData.email,
        password: userData.password,
      }),
    })
    .then(response => {
      if (response.status >= 200 && response.status < 300) {
        console.log(response);
        LoginActions.loginSuccess(response);
      } else {
        const error = new Error(response.statusText);
        error.response = response;
        LoginActions.loginError();
        throw error;
      }
    })
    .catch(error => { console.log('request failed', error); });
  },
};

export default LoginApiCall;
docs question

Comentario más útil

Finalmente, ¿dónde coloca la llamada API de inicio de sesión?

Esto es precisamente para lo que sirve el creador de acciones dispatch => {} . ¡Efectos secundarios!

Es solo otro creador de acción. Combínalo con otras acciones:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}

export function login(userData) {
  return dispatch =>
    fetch('http://localhost/login', {
      method: 'post',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userData.email,
        password: userData.password,
      }),
    })
    .then(response => {
      if (response.status >= 200 && response.status < 300) {
        console.log(response);
        dispatch(loginSuccess(response));
      } else {
        const error = new Error(response.statusText);
        error.response = response;
        dispatch(loginError(error));
        throw error;
      }
    })
    .catch(error => { console.log('request failed', error); });
}

En sus componentes, solo llame

this.props.login(); // assuming it was bound with bindActionCreators before

// --or--

this.props.dispatch(login()); // assuming you only have dispatch from Connector

Todos 115 comentarios

Esto es casi correcto, pero el problema es que no puedes simplemente llamar a los creadores de acción pura y esperar que sucedan cosas. No olvide que sus creadores de acciones son solo funciones que especifican _lo_ que debe enviarse.

// CounterActions
export function increment() {
  return { type: INCREMENT }
}


// Some other file
import { increment } from './CounterActions';
store.dispatch(increment()); <--- This will work assuming you have a reference to the Store
increment(); <---- THIS DOESN'T DO ANYTHING! You're just calling your function and ignoring result.


// SomeComponent
import { increment } from './CounterActions';

@connect(state => state.counter) // will inject store's dispatch into props
class SomeComponent {
  render() {
    return <OtherComponent {...bindActionCreators(CounterActions, this.props.dispatch)} />
  }
}


// OtherComponent
class OtherComponent {
  handleClick() {
    // this is correct:
    this.props.increment(); // <---- it was bound to dispatch in SomeComponent

    // THIS DOESN'T DO ANYTHING:
    CounterActions.increment(); // <---- it's just your functions as is! it's not bound to the Store.
  }
}

Ahora, vayamos a tu ejemplo. Lo primero que quiero aclarar es que no necesita el formulario asíncrono dispatch => {} si solo envía una acción sincrónicamente y no tiene efectos secundarios (cierto para loginError y loginRequest ).

Esta:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return dispatch => {
    dispatch({ error, type: LOGGED_FAILED });
  };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return dispatch => {
    dispatch({ user, type: LOGIN_ATTEMPT });
  };
}

se puede simplificar como

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

// You'll have a side effect here so (dispatch) => {} form is a good idea
export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}

En segundo lugar, se supone que sus reductores son _ funciones puras que no tienen efectos secundarios_. No intente llamar a su API desde un reductor.

Esta:

const initialState = new Immutable.Map({
  email: '',
  password: '',
  isLoggingIn: false,
  isLoggedIn: false,
  error: null
}).asMutable(); // <---------------------- why asMutable?

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      console.log(action.user);
      LoginApiCall.login(action.user); // <------------------------ no side effects in reducers! :-(
      return state;
    case LOGGED_FAILED:
      console.log('failed from reducer');
      return state;
    case LOGGED_SUCCESSFULLY:
      console.log('success', action);
      console.log('success from reducer');
      break;
    default:
      return state;
  }

probablemente debería verse más como

const initialState = new Immutable.Map({
  email: '',
  password: '',
  isLoggingIn: false,
  isLoggedIn: false,
  error: null
});

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      return state.merge({
        isLoggingIn: true,
        isLoggedIn: false,
        email: action.email,
        password: action.password // Note you shouldn't store user's password in real apps
      });
    case LOGGED_FAILED:
      return state.merge({
        error: action.error,
        isLoggingIn: false,
        isLoggedIn: false
      });
    case LOGGED_SUCCESSFULLY:
      return state.merge({
        error: null,
        isLoggingIn: false,
        isLoggedIn: true
      });
      break;
    default:
      return state;
  }

Finalmente, ¿dónde coloca la llamada API de inicio de sesión?

Esto es precisamente para lo que sirve el creador de acciones dispatch => {} . ¡Efectos secundarios!

Es solo otro creador de acción. Combínalo con otras acciones:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}

export function login(userData) {
  return dispatch =>
    fetch('http://localhost/login', {
      method: 'post',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userData.email,
        password: userData.password,
      }),
    })
    .then(response => {
      if (response.status >= 200 && response.status < 300) {
        console.log(response);
        dispatch(loginSuccess(response));
      } else {
        const error = new Error(response.statusText);
        error.response = response;
        dispatch(loginError(error));
        throw error;
      }
    })
    .catch(error => { console.log('request failed', error); });
}

En sus componentes, solo llame

this.props.login(); // assuming it was bound with bindActionCreators before

// --or--

this.props.dispatch(login()); // assuming you only have dispatch from Connector

Finalmente, si a menudo escribe grandes creadores de acciones como este, es una buena idea escribir un middleware personalizado para llamadas asíncronas compatible con las promesas, sea lo que sea que use para asíncronos.

La técnica que describí anteriormente (creadores de acciones con dispatch => {} firma) ahora está incluida en Redux, pero en 1.0 estará disponible solo como un paquete separado llamado redux-thunk . Mientras lo hace, también puede consultar redux-promise-middleware o redux-promise .

@gaearon : aplaudir: ¡fue una explicación increíble! ;) Definitivamente debería aparecer en los documentos.

@gaearon increíble explicación! :trofeo:

@gaearon ¿Qué comprobable en proyectos a gran escala.

Descubrimos que hacer llamadas a API en Reductores (flujo atómico) es muy útil y comprobable en proyectos a gran escala.

Esto rompe récord / repetición. Las funciones con efectos secundarios son más difíciles de probar que las funciones puras por definición. Puedes usar Redux así, pero está completamente en contra de su diseño. :-)

Mantener eso en acción a los creadores me suena a romper una única fuente de verdad.

“Fuente única de la verdad” significa que los datos se encuentran en un solo lugar y no tienen copias independientes. No significa que "toda la lógica del dominio debe estar en un solo lugar".

¿Qué sucede si necesita realizar alguna lógica para determinar qué llamada de API debe llamarse? ¿No es la lógica del dominio la que debería estar en un solo lugar (reductores)?

Los reductores especifican cómo las acciones transforman el estado. No deberían preocuparse por dónde se originan estas acciones. Pueden provenir de componentes, creadores de acciones, sesiones grabadas serializadas, etc. Ésta es la belleza del concepto de usar acciones.

Cualquier llamada a la API (o lógica que determina a qué API se llama) ocurre antes que los reductores. Es por eso que Redux admite middleware. El middleware de procesador descrito anteriormente le permite usar condicionales e incluso leer desde el estado:

// Simple pure action creator
function loginFailure(error) {
  return { type: LOGIN_FAILURE, error };
}

// Another simple pure action creator
function loginSuccess(userId) {
  return { type: LOGIN_SUCCESS, userId };
}

// Another simple pure action creator
function logout() {
  return { type: LOGOUT };  
}


// Side effect: uses thunk middleware
function login() {
  return dispatch => {
    MyAwesomeAPI.performLogin().then(
      json => dispatch(loginSuccess(json.userId)),
      error => dispatch(loginFailure(error))
    )
  };
}


// Side effect *and* reads state
function toggleLoginState() {
  return (dispatch, getState) => {
    const { isLoggedIn } = getState().loginState;
    if (isLoggedIn) {
      dispatch(login());
    } else {
      dispatch(logout());
    }
  };
}

// Component
this.props.toggleLoginState(); // Doesn't care how it happens

Los creadores de acciones y el middleware están _ diseñados_ para realizar efectos secundarios y complementarse entre sí.
Los reductores son solo máquinas de estado y no tienen nada que ver con async.

Los reductores especifican cómo las acciones transforman el estado.

Este es un punto muy válido, gracias.

Volveré a abrir para la posteridad hasta que haya una versión de esto en los documentos.

: +1:

Yo diría que si bien los reductores son máquinas de estados, también lo son los creadores de acciones en general (pero implícitamente).

Digamos que el usuario hizo clic en el botón "enviar" dos veces. La primera vez, enviará una solicitud HTTP al servidor (en el creador de acciones) y cambiará el estado a "enviar" (en el reductor). La segunda vez no quieres hacer eso.

En el modelo actual, su creador de acciones tendrá que elegir qué acciones enviar según el estado actual. Si actualmente no se está "enviando", envíe una solicitud HTTP y envíe una acción; de lo contrario, simplemente no haga nada o incluso envíe una acción diferente (como una advertencia).

Por lo tanto, su flujo de control se divide efectivamente en dos máquinas de estado y una de ellas es implícita y está llena de efectos secundarios.

Yo diría que este enfoque Flux maneja muy bien la mayoría de los casos de uso de una aplicación web típica. Pero cuando tiene una lógica de control compleja, puede resultar problemático. Todos los ejemplos existentes simplemente no tienen una lógica de control compleja, por lo que este problema está algo oculto.

https://github.com/Day8/re-frame es un ejemplo de enfoque diferente en el que tienen un controlador de eventos único que actúa como el único FSM.

Pero los reductores tienen efectos secundarios. Por lo tanto, es interesante cómo tratan la función de "reproducción" en tales casos, se les preguntó en https://github.com/Day8/re-frame/issues/86

En general, creo que se trata de un problema real y no es de extrañar que aparezca una y otra vez. Es interesante ver qué soluciones surgirán eventualmente.

¡Manténganos informados sobre cambios de marco y efectos secundarios!

En mi libro, hay 3 tipos de estados en cualquier aplicación web Redux decente.

1) Ver componentes (React this.state). Esto debería evitarse, pero a veces solo quiero implementar algún comportamiento que se pueda reutilizar simplemente reutilizando un componente independientemente de Redux.

2) Estado compartido de la aplicación, es decir, Redux.state. Es el estado que usa una capa de vista para presentar una aplicación a un usuario y los componentes de vista la usan para "comunicarse" entre sí. Y este estado se puede utilizar para "comunicarse" entre ActionCreators, Middlewares, puntos de vista, ya que las decisiones pueden depender de este estado. De ahí que lo "compartido" sea importante. Sin embargo, no tiene que ser un estado completo de la aplicación. O creo que no es práctico implementar todo como este tipo de estado (en términos de acciones).

3) Estado que se encuentra en otros módulos / bibliotecas / servicios complejos con efectos secundarios. Los escribiría para manejar los escenarios que describe vladar. O react-router es otro ejemplo. Puede tratarlo como una caja negra que tiene su propio estado o puede decidir elevar parte del estado del enrutador de reacción al estado compartido de la aplicación (Redux.state). Si escribiera un módulo HTTP complejo que manejara inteligentemente todas mis solicitudes y sus tiempos, generalmente no estoy interesado en los detalles del tiempo de solicitud en Redux.state. Solo usaría este módulo de ActionCreators / Middlewares, posiblemente junto con Redux.state para obtener un nuevo Redux.state.

Si quisiera escribir el componente de vista que muestre los tiempos de mi solicitud, tendría que obtener este estado en Redux.state.

@vladar ¿Es lo que quiere decir con que AC / MW son máquinas de estado implícitas? ¿Eso se debe a que no mantienen el estado por sí mismo, pero aún dependen del estado que se mantiene en otro lugar y que pueden definir la lógica de control de cómo estos estados evolucionan en el tiempo? Para algunos casos, creo que todavía podrían implementarse como cierres y mantener su propio estado, convirtiéndose en máquinas de estado explícitas.

Alternativamente, podría llamar a Redux.state un "estado público", mientras que otros estados son privados. Cómo diseñar mi aplicación es un acto para decidir qué mantener como estado privado y qué como estado público. Me parece una buena oportunidad para la encapsulación, por lo que no veo problemático tener estados divididos en diferentes lugares en la medida en que no se convierta en un infierno cómo se afectan entre sí.

@vladar

Pero los reductores tienen efectos secundarios. Así que es interesante cómo tratan la función de "reproducción" en tales casos.

Para lograr una reproducción sencilla, el código debe ser determinista. Es lo que logra Redux al requerir reductores puros. En efecto, Redux.state divide la aplicación en partes deterministas y no deterministas (asíncronas). Puede reproducir bahavior por encima de Redux.state asumiendo que no hace locuras en los componentes de vista. El primer objetivo importante para lograr un código determinista es alejar el código asíncrono y traducirlo en código síncrono a través del registro de acciones. Es lo que hace la arquitectura Flux en general. Pero en general no es suficiente y otros códigos con efectos secundarios o mutantes pueden romper el procesamiento determinista.

Lograr la capacidad de reproducción con reductores de efectos secundarios es en mi humilde opinión o imprácticamente difícil, incluso imposible o funcionará solo parcialmente con muchos casos de esquina con probablemente un pequeño efecto práctico.

Para lograr un viaje en el tiempo fácil, almacenamos instantáneas del estado de la aplicación en lugar de reproducir acciones (que siempre es difícil) como se hace en Redux.

Para lograr un viaje en el tiempo fácil, almacenamos instantáneas del estado de la aplicación en lugar de reproducir acciones (que siempre es difícil) como se hace en Redux.

Redux DevTools almacena tanto instantáneas como acciones. Si no tiene acciones, no puede recargar los reductores en caliente. Es la característica más poderosa del flujo de trabajo que habilita DevTools.

El viaje en el tiempo funciona bien con los creadores de acciones impuras porque _ ocurre en el nivel de las acciones crudas (finales) enviadas_. Estos son objetos simples, por lo que son completamente deterministas. Sí, no puede "deshacer" una llamada a la API, pero no veo ningún problema con eso. Puede revertir las acciones sin procesar emitidas por él.

¿Eso se debe a que no mantienen el estado por sí mismo, pero aún dependen del estado que se mantiene en otro lugar y que pueden definir la lógica de control de cómo estos estados evolucionan en el tiempo?

Sí, eso es exactamente lo que quiero decir. Pero también puede terminar duplicando su lógica de control. Algún código para demostrar el problema (de mi ejemplo anterior con doble clic en enviar).

function submit(data) {
  return (dispatch, getState) => {
    const { isSubmitting } = getState().isSubmitting;
    if (!isSubmitting) {
      dispatch(started(data));

      MyAPI.submit(data).then(
        json => dispatch(success(json)),
        error => dispatch(failure(error))
      )
    }
  }
}

function started(data) {
   return { type: SUBMIT_STARTED, data };
}

function success(result) {
  return { type: SUBMIT_SUCCESS, result };
}

function failure(error) {
  return { type: SUBMIT_FAILURE, error };
}

Y luego tiene su reductor para verificar el estado "isSubmitting" nuevamente

const initialState = new Immutable.Map({
  isSubmitting: false,
  pendingData: null,
  error: null
});

export default function form(state = initialState, action) {
  switch (action.type) {
    case SUBMIT_STARTED:
      if (!state.isSubmitting) {
        return state.merge({
          isSubmitting: true,
          pendingData: action.data
        });
      } else {
        return state;
      }
  }
}

Entonces terminas teniendo las mismas verificaciones lógicas en dos lugares. Obviamente, la duplicación en este ejemplo es mínima, pero para escenarios más complejos puede que no sea bonita.

Creo que en el último ejemplo el cheque _no_ debería estar dentro del reductor. De lo contrario, puede descender rápidamente a la inconsistencia (por ejemplo, la acción se envía por error, pero se ignora, pero luego la acción "exitosa" también se envía pero fue inesperada y el estado podría fusionarse incorrectamente).

(Perdón si ese era el punto que estabas diciendo ;-)

Estoy de acuerdo con gaeron porque! IsSubmitting ya está codificado en el hecho de que se envió la acción SUBMIT_STARTED. Cuando una acción es el resultado de alguna lógica, esa lógica no debe replicarse en el reductor. Entonces la responsabilidad del reductor es solo que cuando en cualquier momento recibe SUBMIT_STARTED no piensa y solo actualiza el estado con la carga útil de acción porque alguien más había asumido la responsabilidad de decidir que SUBMIT_STARTED.

Siempre se debe apuntar a que haya una sola cosa que asuma la responsabilidad de una sola decisión.

Reducer puede luego seguir construyendo sobre el hecho SUBMIT_STARTED y extenderlo con lógica adicional, pero esta lógica debería ser diferente. Fe:

case SUBMIT_STARTED:
  if (goodMood) loading...
  else nothing_happens

Puedo imaginar que a veces puede ser complicado decidir quién debe ser responsable de qué. ActionCreator, Middleware, módulo complejo independiente, reductor?

Mi objetivo es tomar tantas decisiones como sea posible en los reductores mientras los mantengo puros, ya que pueden recargarse en caliente. Si se requieren decisiones para efectos secundarios / asíncronos, entonces van a AC / MW / anywhere_else. También puede depender de cómo evolucionen las mejores prácticas con respecto a la reutilización de código, es decir, reductores frente a middleware.

AC no puede mutar state.isSubmitting directamente. Por lo tanto, si su lógica depende de ello, AC necesita garantías de que state.isSubmitting estará sincronizado con su lógica. Si la lógica de CA quiere establecer state.isSubmitted en verdadero con nuevos datos y las lecturas están de vuelta, espera que se establezca como tal. Otra lógica no debería cambiar potencialmente esta premisa. ACCIÓN es la propia primitiva de sincronización. Básicamente, las acciones definen el protocolo y los protocolos deben estar bien y claramente definidos.

Pero creo que sé lo que intentas decir. Redux.state es un estado compartido. El estado compartido siempre es complicado. Es fácil escribir código en reductores que cambia de estado de una manera inesperada para la lógica de CA. Por lo tanto, puedo imaginar que puede ser difícil mantener la lógica entre el CA y los reductores sincronizados en algunos escenarios muy complejos. Esto puede provocar errores que pueden ser difíciles de seguir y depurar. Creo que para tales escenarios siempre es posible colapsar la lógica a AC / Middleware y hacer que los reductores actúen estúpidamente basándose en acciones bien definidas con poca o ninguna lógica.

Alguien siempre puede agregar un nuevo reductor que romperá algún viejo AC dependiente. Uno debería pensarlo dos veces antes de hacer que AC dependa de Redux.state.

Tengo un creador de acciones que requiere un token de autenticación devuelto por una solicitud de API con credenciales de usuario. Hay un límite de tiempo para este token y algo debe actualizarlo y administrarlo. Lo que propones parece indicar que este estado no pertenece a Redux.state?

¿A dónde debería ir? Conceptualmente, ¿se pasa al constructor de la tienda Redux como estado externo inmutable?

Por lo tanto, puedo imaginar que puede ser difícil mantener la lógica entre el CA y los reductores sincronizados en algunos escenarios muy complejos.

Sí. No estoy diciendo que sea un obstáculo, pero puede volverse inconsistente. El objetivo de la máquina de estados es reaccionar a los eventos, independientemente del orden de su aparición, y mantener la coherencia.

Si confía en el orden adecuado de los eventos de CA, simplemente mueve implícitamente partes de su motor de estado a CA y lo acopla con el reductor.

Tal vez sea solo una cuestión de preferencia, pero me siento mucho mejor cuando la tienda / reductor siempre actúa de manera consistente y no depende de algo externo para mantener la coherencia en otro lugar.

Uno debería pensarlo dos veces antes de hacer que AC dependa de Redux.state.

Creo que no se puede evitar en casos complejos (sin mover algunos efectos secundarios a reductores / controladores de eventos).

Parece que es una compensación: "eliminar la función de recarga en caliente o de reproducción" frente a "administrar el estado en un solo lugar". En un caso posterior, puede escribir FSM reales o Statecharts como propone re-frame; en el primer caso, es más lógica de control ad-hoc como la que tenemos ahora en Flux.

He leído (demasiado rápido) Re-frame y es el mismo tipo de cosas que Redux con algunas diferencias en los detalles de implementación. Los componentes de la vista de reencuadre se basan en observables, los eventos son acciones, el reencuadre distribuye eventos (acciones) directamente como objetos (se construyen en un componente de vista) sin usar creadores, los manejadores de eventos son puros y son lo mismo que Redux reductores.

El manejo de los efectos secundarios no se discute mucho o me lo perdí, pero lo que supongo es que se manejan en Middlewares. Y ahí está la principal diferencia. Los Middlewares envuelven los controladores de eventos (reductores), componen con ellos desde el exterior, mientras que Redux Middlewares no envuelve los reductores. En otras palabras, Re-frame compone los efectos secundarios directamente en reductores puros, mientras que Redux los mantiene separados.

Significa que no se pueden grabar eventos Re-frame y reproducirlos fácilmente.

Cuando estaba hablando de posibles inconsistencias, consideré el estado compartido como la causa raíz y creo que Re-frame tiene el mismo riesgo.

La diferencia está en acoplar Middlewares con reductores. Cuando los Middlewares terminan en Re-frame, pasan directamente el resultado a los controladores de eventos (reductores), este resultado no se registra como evento / acción, por lo que no se puede reproducir. Yo diría que Redux solo pone ganchos en el medio en forma de acciones, por lo tanto, creo que técnicamente puedo lograr lo mismo que Re-frame, con más flexibilidad a expensas de más escritura (creación de acciones) y probablemente más espacio para hacerlo. incorrecto.

Al final, no hay nada en Redux que me impida implementar exactamente el mismo enfoque que en Re-frame. Puedo crear cualquier función que tome reductor como parámetro, hacer algún procesamiento en él y luego llamar directamente a la función reducer con el resultado y llamarlo Reducer Middlewares o lo que sea. Se trata solo de si quiero hacerlo un poco más fácil a expensas de perder DevTools y más acoplamiento.

Todavía tengo que pensar más si hay una ventaja significativa en el enfoque de Reencuadre por encima de cierta simplificación (cantidad decreciente de acciones). Estoy abierto a demostrar que estoy equivocado, como he dicho, lo leí rápidamente.

De hecho, estaba un poco equivocado. La diferencia no está en las implementaciones de Redux vs Re-frame, sino realmente en "dónde pones tus efectos secundarios".

Si hace esto en controladores de eventos (reductores) y no permite que los creadores de acciones afecten su flujo de control, entonces técnicamente no hay diferencia: también puede usar fsm / statecharts en Redux y tener su flujo de control en un solo lugar.

Pero en realidad, hay una diferencia. @gaearon lo explicó muy bien: Redux espera que sus reductores sean puros, de lo contrario, la función de reproducción se rompe. Entonces, se supone que no debes hacer eso (poner efectos secundarios en los reductores), ya que va en contra del diseño de Redux.

Re-frame también indica que los controladores de eventos son puros, pero incluso en Readme mencionan que las solicitudes HTTP se inician en los controladores de eventos. Así que esto no está claro para mí, pero parece que sus expectativas son diferentes: puedes poner tus efectos secundarios en los controladores de eventos.

Entonces es interesante cómo abordan la reproducción (y eso es lo que pregunté en https://github.com/Day8/re-frame/issues/86).

Supongo que la única forma de hacerlo cuando sus controladores de eventos pueden tener efectos secundarios es lo que mencionó @ tomkis1 : estado de grabación devuelto por cada controlador de eventos (no volver a ejecutar reductores en cada evento).

La recarga en caliente aún puede funcionar, excepto que no puede afectar eventos "pasados", solo producir una nueva rama de estado en el tiempo.

Entonces, la diferencia en el diseño probablemente sea sutil, pero para mí es importante.

Creo que hacen solicitudes HTTP componiendo controladores de eventos puros con middlewares de efectos secundarios. Esta composición devuelve un nuevo controlador de eventos que ya no es puro, pero el interno permanece puro. No creo que sugieran construir controladores de eventos que mezclen la mutación app-db (estado) y los efectos secundarios. Hace que el código sea más comprobable.

Si registra el resultado de un controlador de eventos, no puede hacerlo recargable en caliente, lo que significa que puede cambiar su código sobre la marcha. Tendría que registrar el resultado del middleware y eso sería exactamente lo mismo que hace Redux: grabe este resultado como una acción y páselo a reducer (escúchalo).

Creo que podría decir que los middlewares Re-frame son proactivos mientras que Redux permite la reactividad (cualquier reductor ad-hoc puede escuchar un resultado de middleware / ac). Una cosa de la que estoy empezando a darme cuenta es que la CA, como se usa principalmente en Flux y middlewares, es lo mismo que los controladores.

Como entendí, @ tomkis1 sugiere un

Bien, al darse cuenta de que los reductores / controladores de eventos devuelven un estado completamente nuevo, por lo tanto, dijiste lo mismo.

@merk Intentaré escribir mi pensamiento al respecto probablemente el lunes, ya que probablemente no llegue durante un fin de semana.

Re-frame docs recomienda hablar con el servidor en los controladores de eventos: https://github.com/Day8/re-frame#talking -to-a-server

Entonces aquí es donde diverge de Redux. Y esta diferencia es más profunda de lo que parece a primera vista.

Hm, no veo la diferencia con Redux:

Los controladores de eventos de iniciación deben organizar que los controladores en caso

Reemplace el evento con Acción. Redux solo tiene un nombre especial para un controlador de eventos puro: un reductor. Creo que me estoy perdiendo algo o mi cerebro no funciona este fin de semana.

Pude oler una diferencia más significativa en la forma en que se ejecutan los eventos: en cola y asíncronos. Creo que Redux ejecuta acciones sincronizadas y bloqueadas acción por acción para garantizar la coherencia del estado. Puede afectar aún más la capacidad de reproducción, ya que no estoy seguro de si solo guardar eventos y reproducirlos una y otra vez dará como resultado el mismo estado final. Sin tener en cuenta que no puedo hacer eso porque no sé qué eventos desencadenan efectos secundarios a diferencia de Redux, donde las acciones siempre desencadenan código puro.

@vladap Para resumir la diferencia como yo la veo:

1.

  • en Redux, inicia la solicitud del servidor en el creador de acciones (consulte la respuesta de @gaearon a la pregunta original de este problema); tus reductores deben ser puros
  • en Re-frame lo haces en event handler (reductor); sus controladores de eventos (reductores) pueden tener efectos secundarios

2.

  • En el primer caso, su flujo de control se divide entre AC y reductor.
  • En segundo caso, todo su flujo de control está en un solo lugar: manejador de eventos (reductor).

3.

  • Si su flujo de control está dividido, necesita coordinación entre AC y reductor (incluso si está implícito): AC lee el estado producido por el reductor; reducer asume el orden correcto de eventos de AC. Técnicamente, introduce el acoplamiento de CA y reductor, porque los cambios en el reductor también pueden afectar a CA y viceversa.
  • Si su flujo de control está en un solo lugar, no necesita coordinación adicional + puede usar herramientas como FSM o Statecharts en sus controladores / reductores de eventos


    1. La forma de Redux de aplicar reductores puros y hacer que los efectos secundarios se trasladen a los AC permite la recarga en caliente y la reproducción

  • Reencuadrar la forma de tener efectos secundarios en los controladores de eventos (reductores) cierra la puerta a la reproducción como se hace en Redux (reevaluando los reductores), pero otras opciones (probablemente menos poderosas) aún son posibles, como mencionó @ tomkis1

En cuanto a la diferencia en la experiencia de desarrollo, solo aparece cuando tiene muchas cosas asíncronas (temporizadores, solicitudes http paralelas, sockets web, etc.). Para las cosas de sincronización, todo es igual (ya que no hay flujo de control en los AC).

Supongo que necesito preparar algún ejemplo del mundo real de un escenario tan complejo para aclarar mi punto.

Puede que te vuelva loco, lo siento pero no estamos de acuerdo en una cosa básica. Tal vez debería leer el código de ejemplos de reencuadre, aprenderé Clojure algún día. Tal vez lo hizo, por lo tanto, sabe más. Gracias por tu esfuerzo.

1.

en Redux, inicia la solicitud del servidor en el creador de acciones (consulte la respuesta de @gaearon a la pregunta original de este problema); tus reductores deben ser puros
en Re-frame lo haces en event handler (reductor); sus controladores de eventos (reductores) pueden tener efectos secundarios

El documento que hice en negrita dice explícitamente que debería iniciar la llamada de webapi en un controlador de eventos y luego enviar el evento en su éxito. No entiendo cómo puedo manejar este evento enviado en el mismo controlador de eventos. Este evento de éxito se envía al enrutador (eq. Del despachador, creo) y necesito otro controlador de eventos para manejarlo. Este segundo controlador de eventos es puro y es equivalente a Redux reducer, el primero es equivalente a ActionCreator. Para mí es lo mismo o claramente extraño algo importante.

En cuanto a la diferencia en la experiencia de desarrollo, solo aparece cuando tiene muchas cosas asíncronas (temporizadores, solicitudes http paralelas, sockets web, etc.). Para las cosas de sincronización, todo es igual (ya que no hay flujo de control en los AC).

No estamos de acuerdo con este. Las cosas asincrónicas en realidad no importan, no puedes reproducirlas en Redux, por lo que no tienes experiencia de desarrollo aquí. La diferencia aparece cuando tienes muchos reductores puros complejos. En Redux puedes tomar el estado A, alguna secuencia de acción, reductor, reproducirlo y obtener el estado B. Luego mantienes todo igual pero haces un cambio de código en el reductor y lo recargas en caliente, repites la misma secuencia de acción desde el estado A y tú obtenga el estado C, viendo inmediatamente el impacto de su cambio de código. No puedes hacerlo con el enfoque @ tomkis1 .

Incluso podría crear escenarios de prueba sobre él. Guarde algún estado inicial, guarde alguna secuencia de acción, reduzca la secuencia de acción al estado A, obtenga el estado B y afirme. Luego, realice cambios en el código que espera que resulten en el mismo estado B, repita el escenario de prueba para afirmar que es cierto y que sus cambios no han roto su aplicación. No digo que sea el mejor enfoque de prueba, pero se puede hacer.

En realidad, me sorprendería bastante si los Clojuristas, a quienes considero casi tan duros practicantes de la programación funcional como Haskellers, recomendaran mezclar efectos secundarios con código que podría ser puro al menos sin cierto nivel de composición.

La evaluación / reactividad perezosa es la técnica básica de cómo mover el código de efectos secundarios lo más lejos posible del código puro. La evaluación perezosa es cómo Haskell finalmente aplicó el concepto de pureza funcional impráctico para obtener un programa que hace algo práctico, porque un programa completamente puro no puede hacer mucho. Usando la composición monádica y otros, todo el programa se crea como flujo de datos y para mantener puro el último paso en la canalización y nunca llamar a efectos secundarios en su código, el programa devuelve una descripción de lo que se debe hacer. Se pasa al tiempo de ejecución, que lo ejecuta de forma perezosa; no activa la ejecución mediante una llamada imperativa. Al menos así es como entiendo la programación funcional desde el punto de vista del pájaro. No tenemos un tiempo de ejecución así, lo simulamos con ActionCreators y reactividad. Descargo de responsabilidad, no lo dé por sentado, es lo que he entendido de no haber leído mucho sobre FP como algún tipo de autoridad aquí. Puede que me haya equivocado, pero creo que entendí el punto.

No entiendo cómo puedo manejar este evento enviado en el mismo controlador de eventos.

No estoy diciendo eso. Por "efectos secundarios" me refiero a que se inicie una solicitud HTTP en el controlador de eventos (reductor). Realizar IO también es un efecto secundario, incluso si no afecta el retorno de su función. No me refiero a "efectos secundarios" en términos de modificar el estado directamente en los controladores de éxito / error.

Este segundo controlador de eventos es puro y es equivalente a Redux reducer, el primero es equivalente a ActionCreator.

No creo que el primero sea equivalente a Action Creator. De lo contrario, ¿por qué necesita Action Creators para hacer esto? ¿Si puedes hacer lo mismo en reductor?

No puedes hacerlo con el enfoque @ tomkis1 .

De acuerdo. Y eso es lo que quise decir cuando escribí "otras opciones (probablemente menos poderosas)".

No creo que el primero sea equivalente a Action Creator. De lo contrario, ¿por qué necesita Action Creators para hacer esto? ¿Si puedes hacer lo mismo en reductor?

Los ActionCreators se utilizan para aislar los efectos secundarios de la aplicación pura (que se puede reproducir). Como Redux está diseñado, no puede (no debería) realizar efectos secundarios en reductores. Necesita otra construcción donde pueda colocar código de efectos secundarios. En Redux, estas construcciones son AC o middlewares. La única diferencia que veo en el reencuadre es lo que hacen los middlewares y ese reencuadre no tiene middlewares para transformar eventos (acciones) antes de que se activen los controladores.

En lugar de AC / middlewares, en realidad se puede usar cualquier cosa: módulo util, servicios (como servicios angulares), puede construirlo como una biblioteca separada que luego interactuará con AC o middleware y traducir su API en acciones. Si me preguntas, AC no es el lugar adecuado para colocar este código. AC debería ser simplemente creadores / fábricas que construyen objetos de acción a partir de argumentos. Si quisiera ser un purista, sería mejor colocar el código de efectos secundarios estrictamente en middlewares. ActionCreator es un mal término para una especie de responsabilidad del controlador. Pero si este código es solo una llamada de webapi de una sola línea, entonces hay una tendencia a simplemente ponerlo dentro de AC y para una aplicación simple puede estar bien.

AC, middlewares, bibliotecas de terceros forman una capa que me gusta llamar capa de servicio. Los WebApiUtils originales de Facebook serían parte de esta capa, al igual que si usara react-router y escuchara sus cambios y los tradujera a acciones para actualizar Redux.state, Relay se puede usar como capa de servicio (que usar Redux para aplicación / vista estado). Como la mayoría de las bibliotecas de terceros están programadas actualmente, no funcionan bien con la reproducción. Obviamente, no quiero reescribir todo desde cero, así que lo haría. Lo que me gusta pensar al respecto es que estas bibliotecas, servicios, utilidades, etc. extienden el entorno del navegador formando una plataforma sobre la cual puedo construir mi aplicación reproducible. Esta plataforma está llena de efectos secundarios, de hecho es el lugar al que intencionalmente muevo los efectos secundarios. Traduzco esta API de plataforma a acciones y mi aplicación se conecta indirectamente a esta plataforma escuchando estos objetos de acción (tratando de simular el enfoque de Haskell que traté de describir en una publicación anterior). Esta es una especie de truco en la programación funcional sobre cómo lograr la pureza (posiblemente no en un sentido académico absoluto) pero desencadenar efectos secundarios.

Hay otra cosa que me confunde, pero podría malinterpretarlo. Creo que dijiste que para una lógica compleja es beneficioso tener todo en un solo lugar. Creo que todo lo contrario.

Todo este asunto de acciones es similar en concepto a los eventos DOM. Realmente no me gustaría mezclar mi código comercial con un código de cómo el navegador fe detecta el evento mousemove y estoy seguro de que a usted tampoco. Afortunadamente, estoy acostumbrado a trabajar además de addListener ('mousemove', ...) porque estas responsabilidades están bien separadas. El punto es lograr esta buena separación de preocupaciones también para mi lógica empresarial. ActionCreators, middlewares son herramientas para ello.

Imagínese que estaría escribiendo una aplicación contra un webapi obsoleto que no se adapta bien a las necesidades de mi negocio. Para obtener los datos requeridos, tendría que llamar a un par de puntos finales, usar su resultado para crear las próximas llamadas y luego fusionar todos los resultados en una forma canónica que usaría en Redux.state. Esta lógica no se filtraría en mis reductores para que no tenga que transformar datos en mis reductores una y otra vez. Estaría en middlewares. Efectivamente, aislaría una api obsoleta y desordenada y desarrollaría mi aplicación empresarial, ya que estaría escrita en una buena api. Una vez hecho esto, tal vez obtendría un cambio para reconstruir nuestro webapi y luego simplemente volvería a escribir mi middleware.

Por lo tanto, tener todo en un solo lugar parece fácil para una aplicación fácil. Pero muy al contrario por lo complejo veo una buena separación de preocupaciones beneficiosas. Pero tal vez no he entendido su punto o caso de uso al que se refiere.

En realidad, probablemente movería la mayor parte de la transformación de datos a su forma canónica a reductores y usaría la composición de reductores en su lugar, ya que me daría reproducción y capacidad de prueba para este código que generalmente es puro.

No entiendo cómo puedo manejar este evento enviado en el mismo controlador de eventos.
No estoy diciendo eso.

Pensé que querías mezclar las llamadas de webapi con la lógica de reductores para tenerlo en un solo lugar. Lo que estoy diciendo es que tampoco lo haces en un reencuadre porque sugieren enviar el evento en caso de éxito.

Puede haber situaciones en las que webapi -> mucha lógica -> webapi -> mucha lógica -> ... -> resultado para reductor, toda la cadena se activa con un solo clic. En tales casos, la mayor parte de la lógica probablemente estaría en AC, no la dividiría hasta que todos los efectos secundarios terminen. ¿Es esto a lo que te refieres?

Lo mejor que puedo hacer aquí es mover la mayor parte de la lógica a funciones puras para la capacidad de prueba, pero se llamarían en el alcance de CA.

No creo que me guste la posibilidad de hacerlo como una serie de controladores de eventos conectados indirectamente. Sería una cadena prometedora u observable.

Reduxers más experimentados podrían tener una opinión diferente sobre ese pensamiento ( @gaearon , @johanneslumpe , @acdlite , @emmenko ...)

@vladap Creo que esta conversación se alejó demasiado del tema de este problema. Ya mencionaste el punto principal de mis comentarios aquí:

Por lo tanto, puedo imaginar que puede ser difícil mantener la lógica entre el CA y los reductores sincronizados en algunos escenarios muy complejos.

Ejemplo rápido:

Situación 1: tienes una lista de los 10 autores de blogs principales en algún lugar de la página. Ahora eliminó una categoría de publicaciones en el blog. Una vez que se haya eliminado correctamente, debe actualizar esta lista de autores del servidor para que esté actualizada.

Situación 2: estás en una página diferente. No tiene una lista de autores, pero tiene una lista de los 10 comentarios principales. También puede eliminar alguna categoría de publicaciones de blog y tendrá que actualizar la lista de comentarios sobre la eliminación exitosa.

Entonces, ¿cómo manejamos esto a nivel de motor de estado? (También podríamos usar los ganchos de React para obtener ayuda, pero un buen motor de estado debería ser capaz de mantenerse consistente por sí solo)

Opciones:
A) ponemos esta lógica en AC. Entonces AC tocará CategoryApi (para eliminar), leerá desde userList estado y commentList estado (para verificar qué listas están presentes en el estado ahora), hablar con UserListApi y / o CommentListApi (para actualizar listas) + enviar TOP_AUTHORS_UPDATED y / o TOP_COMMENTS_UPDATED . Así que básicamente tocará 3 dominios diferentes.

B) lo ponemos en los controladores userList eventos commentList . Estos controladores escucharían el evento DELETE_CATEGORY_SUCCESS , luego llamarían a su servicio API y, a su vez, enviarían el evento TOP_AUTHORS_UPDATED / TOP_COMMENTS_UPDATED . Entonces, cada controlador solo toca los servicios / estado de su propio dominio.

Este ejemplo es probablemente demasiado simplista, pero incluso en este nivel las cosas se vuelven menos bonitas con las llamadas a API en AC.

La diferencia proviene del hecho de que, a diferencia de los controladores de eventos en el reencuadre, los ActionCreators son proactivos en el manejo de la lógica empresarial. Y solo se puede ejecutar una CA en función del evento DOM. Sin embargo, este AC de nivel superior puede llamar a otros AC. Puede haber AC separados para UserListApi y CommentListApi para que los dominios estén mejor separados, pero siempre tiene que haber AC (como controlador) que los conecte. Esta parte es un código imperativo bastante clásico, mientras que el reencuadre está totalmente basado en eventos. Con un poco de trabajo, se puede reemplazar con un enfoque preferido, basado en eventos, observables, cps, etc.

@vladap Sería interesante ver si otro enfoque es viable: cuando los reductores pueden tener efectos secundarios, pero también pueden aislarlos para que se puedan ignorar en la reproducción.

Digamos que la firma de los reductores cambiaría a: (state, action) => (state, sideEffects?) donde sideEffects es un cierre. Luego, dependiendo del marco de contexto, podría evaluar estos efectos secundarios o ignorarlos (en caso de repetición).

Entonces, el ejemplo de la descripción del problema original se vería así:

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      return [state, () => {LoginApiCall.login(action.user)}];
    case LOGGED_FAILED:
      // do something
      return state;
    case LOGGED_SUCCESSFULLY:
      // do something else
     return state;
    default:
      return state;
  }
}

(todo lo demás permanece prácticamente igual)

Al menos su flujo de control está en un lugar, sus efectos secundarios siempre son simples (porque simplemente crean otra acción / evento en los controladores asíncronos y la lógica permanece en los reductores, por lo que se reproducirá más de su código).

También puede escribir código FSM regular o tipo Statechart (con composición de reductores / controladores de eventos)

Además, no es necesario devolver el cierre con firmas predefinidas en creadores de acciones.

No estoy seguro de qué problemas podría causar este enfoque, pero en mi opinión, vale la pena jugar con él.

Digamos que la firma de los reductores cambiaría a: (estado, acción) => (estado, efectos secundarios?) Donde efectos secundarios es un cierre. Luego, dependiendo del marco de contexto, podría evaluar estos efectos secundarios o ignorarlos (en caso de repetición).

Esto es realmente similar a lo que escuché hacer a Elm con (State, Action) => (State, Request?) . No he visto ningún ejemplo todavía, pero si alguien quiere explorar esto, no dude en hacerlo.

Otro enfoque más del que posiblemente inspirarse es una implementación de subcontratación de eventos sobre el modelo Actor: Akka Persistance , PersistentFSM . No digo que sea mejor pero no está mal conocer otros intentos. Los actores pueden usar efectos secundarios, pero si mal no recuerdo, a veces es necesario escribir un código explícito sobre cómo reproducir.

Tal vez la función de reproducción pueda estar sesgando nuestras decisiones a dónde pertenece el código. Parece que estamos empezando a pensar como ... "Quiero que esto se pueda volver a jugar, por lo tanto, tiene que ir a un reductor". Puede conducir a un código con límites poco claros porque no lo estamos dividiendo por sus responsabilidades / roles y tendemos a dividir artificialmente nuestra lógica empresarial para que sea reproducible.

Si dejara de lado la función de reproducción, ¿cómo escribiría mi código comercial? Mi objetivo es tener mi lógica empresarial frente a un registro de acciones y tenerla en un solo lugar. Las máquinas de estado de lógica empresarial, o lo que sea que esté usando, harían todas las decisiones complejas y difíciles que resultan en un flujo ya resuelto de HECHOS (acciones). Los reductores serían generalmente actualizadores simples y su principal responsabilidad sería cómo aplicar la carga útil de acción a Redux.state. Podrían tener algo de lógica, pero en cierto sentido interpretar los hechos de manera diferente para obtener otro tipo de vista sobre el flujo de hechos (acción) y presentárselo a un usuario.

Para dividir las responsabilidades, me gusta pensarlo de esta manera también. ¿Qué código podría solicitarse potencialmente que se vuelva a implementar en un servidor? Fe para soportar mejor los dispositivos lentos moviendo la lógica de negocios costosa de computación, tal vez por razones de seguridad, la propiedad de inteligencia oculta lo que sea. No puedo mover la lógica de los reductores al servidor tan fácilmente porque está vinculada a una capa de vista. Si se escribe una lógica de negocios compleja delante del registro de acciones, puedo reemplazarla por llamadas REST que se traducen al mismo flujo de acción y mi capa de vista debería funcionar (probablemente).

La desventaja es que, a diferencia de Akka Persistance, no tengo facilidad para reproducir este código.

Algún día tengo que echar un vistazo más profundo a Elm y entender cómo funcionaría su propuesta.

@gaearon Gracias por mencionar a Elm. Encontré esta conversación, https://gist.github.com/evancz/44c90ac34f46c63a27ae , con ideas similares (pero mucho más avanzadas).

Introducen el concepto de Tareas (http, db, etc.) y Efectos (solo una cola de tareas). Entonces, su "reductor" puede devolver un nuevo estado y efectos.

Lo bueno de esto (y no lo había pensado de esta manera), es que si puede recopilar tareas mientras encadena sus reductores, y luego aplicar alguna función a esta lista. Supongamos que puede realizar solicitudes http por lotes, envolver consultas de base de datos con transacciones, etc.

O podrías decir "administrador de reproducción", que es solo un error para los efectos.

@merk Creo que la mayoría ya se ha escrito. Solo que este tipo de código autónomo de efectos secundarios es probablemente el más complicado. Probablemente causará estragos en la reproducción porque los intervalos no se sincronizarán con timetravel asumiendo que timetravel se ejecuta en el mismo código y los intervalos también comenzarán en modo de reproducción.

La aplicación habitual no necesita token ni cuenta atrás de caducidad presentada a un usuario, por lo que técnicamente no tiene que estar en Redux.state. Es posible que la aplicación de administración necesite tenerla. Por lo tanto, puede implementarlo en ambos sentidos, lo que más le convenga.

Una opción es iniciar la cuenta atrás de caducidad en el AC de inicio de sesión y supongo que se ejecuta sin fin y muere con la aplicación o que el cierre de sesión tiene que limpiarla. Cuando el intervalo se activa, hace lo necesario para confirmar el token y, si expira, envía la acción LOGIN_EXPIRED y el reductor de escucha borra la sesión del usuario y cambia la ubicación, lo que a su vez activa la transición del enrutador a / login.

Otra es usar lib de terceros y delegarle las preocupaciones y traducir su api a la acción LOGIN_EXPIRED. O escriba el suyo y mantenga el estado del token y la cuenta regresiva aquí si no lo necesita en la capa de vista.

Puede mantenerlo en Redux.state pero este estado es volátil. Con un estado grande podemos llegar a los mismos problemas que la programación con variables globales. Y es difícil, probablemente imposible, probarlo. Es fácil probar reductores, pero luego se demuestra que funciona cuando solo un reductor está actualizando esa clave en particular en el objeto de estado. Esto puede empeorar en proyectos grandes con muchos desarrolladores de diferentes calidades. Cualquiera puede decidir escribir reductor y pensar que algún estado es bueno para actualizar y no puedo imaginar cómo probar todos los reductores juntos en todas las posibles secuencias de actualización.

Dependiendo de su caso de uso, podría tener sentido proteger este estado, porque es inicio de sesión, una funcionalidad bastante importante, y tenerlo separado. Si este estado es necesario en una capa de vista, entonces repítalo en Redux.state de una manera similar, como el estado de la ubicación del enrutador de reacción se replica en Redux.state en algunos ejemplos. Si su estado y su equipo son pequeños y se comportan bien, puede estar bien tenerlos en un solo lugar.

@vladar @vladap

¡Vaya, esa esencia de Elm es increíblemente genial! También me recuerda la arquitectura de "controladores" de

@merk En realidad, LOGIN_EXPIRED autónomo no afectaría mucho la reproducción porque se ubicaría al final del registro de acciones y no se procesaría inmediatamente mediante una reproducción y tal vez no llegaría al registro de acciones en absoluto, no lo sé cómo se implementa exactamente la reproducción.

@gaeron Parece que en el nivel alto Elm and Cycle implementa el patrón que estaba tratando de describir si lo entiendo correctamente. Tengo un navegador que me da una plataforma base, extiendo esta plataforma con mi capa de servicio (http, db ...) para construir una plataforma para mi aplicación. Luego, necesito algún tipo de pegamento que interactúe con la capa de servicio y permita que mi aplicación pura se comunique indirectamente con ella para que mi aplicación pueda describir lo que quiere hacer pero no ejecutarlo en sí (mensajes enviados a Cycle drivers, Elm Effects tener una lista de tareas). Podría decir que mi aplicación está construyendo un "programa" usando estructuras de datos (me recuerda el código como mantra de datos) que luego se pasa a la capa de servicio para su ejecución y mi aplicación solo está interesada en un resultado de este "programa" que luego aplica a su estado.

@merk : Más. Si el token y su vencimiento se colocarían en Redux.state / o en cualquier otro servicio js, ​​no se transfiere cuando el usuario abre la aplicación en una nueva pestaña. Yo diría que el usuario espera que permanezca registrado. Así que supongo que este estado debería estar en SessionStore.

Incluso sin eso, los estados pueden separarse cuando la expiración del token no significa la limpieza inmediata de la información del usuario y la transición a / login. Hasta que el usuario no toque el servidor con sus interacciones (tiene suficientes datos en caché), puede continuar trabajando y la acción LOGIN_EXPIRED se activa solo una vez que hace algo que requiere un servidor. El estado isLogged basado en Redux.state no tiene por qué significar que existe un token válido. El estado isLogged en Redux.state se usa para decidir qué renderizar, el estado alrededor de un token puede ocultarse en una capa de vista y mantenerse únicamente en el nivel de creadores de acciones sin tener que escribir acciones y reductores para él, excepto los que afectan a una capa de vista.

@vladar Creo que podría entender su punto general. Creo que no me había dado cuenta antes.

No hay una manera fácil de devolver el control del reductor al creador de la acción y pasar algunos valores. Supongamos que estos valores no son necesarios para la renderización, incluso son temporales, pero son necesarios para iniciar una operación asincrónica.

actionCreator() {
  ... getState
  ... some logic L1
  ... dispatch({ACTION1})
}

reducer() {
  case ACTION1
    ... some logic L2
    ... x, y result from L2
    ... some logic L3
    ... resulting in new state
    ... return new state
}

Hay 3 opciones para iniciar la operación asíncrona usando x, y calculado en reductor. Ninguno de ellos es perfecto.

1) Mueva la lógica del reductor al creador de acciones y disminuya el poder de recarga en caliente. Reducer es solo un actualizador de estado tonto o tiene lógica L3 en adelante.

2) Guarde x, y en Redux.state, contaminándolo con valores temporales / transitorios y básicamente usándolo como un canal de comunicación global entre reductores y creadores de acciones que no estoy convencido de que me guste. Creo que está bien si es un estado real pero no para este tipo de valores. El creador de acciones cambia a:

actionCreator() {
  ... getState
  ... some logic L1
  ... dispatch({ACTION1})
  // I assume I get updated state if previous dispatch is sync.
  // If previous dispatch is async it has to be chained properly.
  // If updated state can't be received here after a dispatch
  // then I would think there is a flaw.
  ... getState
  ... asyncOperation(state.x, state.y)
}

3) Guarde x, y en el estado, úselo como accesorios en algún componente y haga un túnel a través de todo el bucle de la capa de vista utilizando componentWillReceiveProps que activa el nuevo creador de acciones y la operación asincrónica. La peor opción si me preguntas, difundiendo la lógica empresarial por todos lados.

@vladar
En general, no importa dónde se inicie async, ya que el resultado de esta operación se empaqueta como una acción, se envía y aplica mediante un reductor. Funcionará igual. Es la misma discusión que con vanilla flux si async debe iniciarse en creadores de acciones o en tiendas.

Requeriría que Redux sea capaz de identificar e ignorar estas llamadas asíncronas en la reproducción, que es exactamente lo que sugiere.

No hay una manera fácil de devolver el control del reductor al creador de la acción y pasar algunos valores.

Diría que este es uno de los síntomas. Su segundo ejemplo ilustra muy bien mi punto de vista (ver más abajo).

Considere el flujo generalizado de transición de estado (en el mundo FSM):

  1. Llega el evento
  2. Dado el estado actual, FSM calcula el nuevo estado objetivo
  3. FSM realiza operaciones para la transición del estado actual al nuevo estado

¡Tenga en cuenta que el estado actual es importante! Un mismo evento puede conducir a diferentes resultados según el estado actual. Y como resultado a diferentes secuencias de operaciones (o argumentos) en el paso 3.

Así que así es como funciona la transición de estado en Flux cuando tu acción está sincronizada. Sus reductores / tiendas son de hecho FSM.

Pero, ¿qué pasa con las acciones asíncronas? La mayoría de los ejemplos de Flux con acciones asíncronas te obligan a pensar que está invertido:

  1. AC realiza una operación asíncrona (independientemente del estado actual)
  2. AC despacha acción como resultado de esta operación
  3. Reducer / Store lo maneja y cambia de estado

Por lo tanto, se ignora el estado actual, se asume que el estado de destino es siempre el mismo. Si bien, en realidad, el flujo de transición de estado consistente sigue siendo el mismo. Y tiene que verse así (con AC):

  1. AC obtiene el estado actual antes de que se envíe la acción
  2. AC despacha acción
  3. AC lee nuevo estado después de la acción
  4. Estado dado antes de la acción y después: decide qué operaciones realizar (o qué valores pasar como argumentos)

Exactamente tu segundo ejemplo. No estoy diciendo que todos estos pasos sean necesarios todo el tiempo. A menudo, puede omitir algunos de ellos. Pero en casos complejos, así es como tendrá que actuar. Es el flujo generalizado de cualquier transición de estado. Y también significa que los límites de su FSM se han trasladado a AC vs reductor / tienda.

Pero mover los límites causa otros problemas:

  1. Si las operaciones asincrónicas estuvieran en la tienda / reductor, podría tener dos FSM independientes reaccionando al mismo evento. Entonces, para agregar una nueva función, simplemente podría agregar una tienda / reductor que reaccione a algún evento existente y eso es todo.
    Pero con las operaciones en CA, tendría que agregar una tienda / reductor y también ir y editar la CA existente agregando una parte asíncrona de la lógica allí también. Si ya contenía lógica asíncrona para algún otro reductor ... no se pone genial.
    Mantener dicho código es obviamente más difícil.
  2. El flujo de control de su aplicación es diferente en el caso de acciones sincronizadas o asíncronas. Para las acciones de sincronización, el reductor tiene el control de la transición, en el caso de async, AC tiene el control efectivo de todas las transiciones que podrían ser causadas por este evento / acción.

Tenga en cuenta que la mayoría de los problemas están ocultos por los requisitos de estado simples de la mayoría de las aplicaciones web. Pero si tiene una aplicación con estado compleja, definitivamente se enfrentará a estas situaciones.

En la mayoría de los casos, probablemente encontrará todo tipo de soluciones. Pero podría evitarlos en primer lugar si su lógica de transición de estado estuviera mejor encapsulada y sus FSM no estuvieran separados entre AC y reductores.

Desafortunadamente, la parte de "efectos secundarios" de la transición de estado sigue siendo una parte de la transición de estado, y no una pieza lógica independiente separada. Es por eso que para mí (state, action) => (state, sideEffects) parece más natural. Sí, eso ya no es una firma de "reducción"%) Pero el dominio del marco no son las transformaciones de datos, sino las transiciones de estado.

Es la misma discusión que con vanilla flux si async debe iniciarse en creadores de acciones o en tiendas.

Sí, pero Flux no le prohíbe tener cosas asíncronas en las tiendas siempre que envíe () en devoluciones de llamada asíncronas frente al estado mutante directamente. No tienen opiniones incluso si la mayoría de las implementaciones recomiendan el uso de AC para async. Personalmente, creo que es una reminiscencia de MVC, porque mentalmente es conveniente tratar a los AC como controladores en lugar de hacer un cambio mental a los FSM.

@vladar

(state, action) => (state, sideEffects)

Sólo de pensar. Esto significa que tendríamos que cambiar el tipo de retorno de la función combineReducers a [state, SideEffects] o [state, [SideEffects]] y cambiar su código para combinar SideEffects de reductores. Original (estado, acción) => el tipo de reductor de estado podría seguir siendo compatible porque combineReducers forzaría (estado) el tipo de retorno a [estado] o [estado, []].

Luego, debemos ejecutar y enviar SideEffects en algún lugar de Redux. Se podría hacer en Store.dispatch () que aplicaría un nuevo estado y ejecutaría la lista de efectos secundarios y la enviaría. Estoy pensando si podemos entrar en una recursión interminable agradable.

Alternativamente, podría ejecutarse en algún lugar del nivel de middleware, pero luego creo que Store.dispatch debería devolver la lista SideEffects junto con una acción.

@vladar

Sí, pero Flux no le prohíbe tener cosas asíncronas en las tiendas siempre que envíe () en devoluciones de llamada asíncronas frente al estado mutante directamente. No tienen opiniones incluso si la mayoría de las implementaciones recomiendan el uso de AC para async. Personalmente, creo que es una reminiscencia de MVC, porque mentalmente es conveniente tratar a los AC como controladores en lugar de hacer un cambio mental a los FSM.

Estoy de acuerdo en que tratar a ActionCreators como controladores no es una buena forma de pensar. Es la causa de la ruta por la que termino pensando que para una lógica compleja necesito devolver el control del reductor a AC. No debería necesitar eso. Si AC maneja async, deberían ser como mucho como manejadores de solicitudes: ejecutar en base a la decisión tomada en otro lugar y esta solicitud no debe cruzar dominios.

Para evitar su papel como controladores, la única opción ahora es enrutar la lógica a través de componentes inteligentes y usar ActionCreators solo para ejecutar el paso inicial de una lógica en una respuesta directa a la interfaz de usuario. La primera decisión real se toma en un componente inteligente basado en Redux.state y / o el estado local del componente.

Este enfoque en su ejemplo anterior de eliminar categoría y actualizar / eliminar algo más en respuesta usando webapi no sería bueno. Supongo que conducirá a una mezcla de diferentes técnicas basadas en las preferencias de los desarrolladores: ¿debería estar en AC, en reductor, en un componente inteligente? Veamos cómo evolucionarán los patrones.

Estaba comprando el punto para mantener las llamadas asíncronas lejos de las tiendas. Pero no creo que sea válido porque, en mi humilde opinión, aumenta significativamente la complejidad en otro lugar. Por otro lado, no veo mucha complejidad en tener una función asíncrona en una tienda / reductor en la medida en que solo se ejecute allí y se envíe, o se acerque y maneje estas funciones como datos y las active en algún lugar lejano.

Los actores hacen lo mismo, pueden ejecutar async y enviar un mensaje (objeto de acción) a otro actor, o enviárselo a usted mismo para continuar en su FSM.

Estoy cerrando porque no parece haber nada procesable aquí. El documento de

@gaearon ¿ puede agregar código real que implemente la forma correcta de manejar las llamadas a la API a uno de los ejemplos con redux (fuera del ejemplo del "mundo real" que usa middleware)?

Todavía tengo preguntas sobre la ubicación real después de leer todos los documentos y este problema algunas veces. Tengo problemas para entender en qué archivos colocar estos efectos secundarios https://github.com/rackt/redux/issues/291#issuecomment -123010379 . Creo que este es el ejemplo "correcto" de este tema.

Gracias de antemano por cualquier aclaración en esta área.

Me gustaría señalar un pensamiento importante sobre los creadores de acciones impuras:

Poner llamadas asíncronas en los creadores de acciones, o tener creadores de acciones impuros en general, tiene una gran desventaja para mí: fragmenta la lógica de control de tal manera que cambiar completamente la lógica de control de su aplicación no es tan simple como simplemente cambiar su tienda creador. Tengo la intención de usar siempre middleware para procesos asincrónicos y evitar completamente los creadores de acciones impuras.

La aplicación que estoy desarrollando tendrá al menos dos versiones: una que se ejecuta en una Raspberry Pi en el sitio y otra que ejecuta un portal. Incluso puede haber versiones separadas para un portal / portal público operado por un cliente. Es incierto si tengo que usar diferentes API, pero quiero prepararme para esa posibilidad lo mejor que pueda, y el middleware me permite hacerlo.

Y para mí, el concepto de tener llamadas API distribuidas entre los creadores de acciones va completamente en contra del concepto de Redux de control centralizado sobre las actualizaciones de estado. Claro, el mero hecho de que una solicitud de API se esté ejecutando en segundo plano no es parte del estado de la tienda Redux, pero sigue siendo el estado de la aplicación, algo sobre lo que desea tener un control preciso. Y encuentro que puedo tener el control más preciso sobre él cuando, como con las tiendas / reductores de Redux, ese control está centralizado en lugar de distribuido.

@ jedwards1211 Es posible que le interese https://github.com/redux-effects/redux-effects en caso de que aún no los haya revisado, así como en la discusión en el # 569.

@gaearon cool, ¡gracias por señalar eso! En cualquier caso, usar mi propio middleware no ha sido demasiado difícil hasta ahora :)

Creé un enfoque similar al del olmo: redux-side-effect . El README explica el enfoque y lo compara con alternativas.

@gregwebs Me gusta redux-side-effect , aunque también sería incómodo con la lógica de control intercambiable, porque está la cuestión de cómo obtengo la función sideEffect en mis módulos reductores, cuando hay varios configuradores de tienda diferentes módulos que se utilizarán en diferentes versiones.

Sin embargo, me vendría bien un truco divertido: simplemente almacena sideEffect en el propio state , ¡para que esté disponible automáticamente para el reductor! : pegado_en_la_tongue_winking_eye:

Algo como https://github.com/rackt/redux/pull/569/ está cerca de ser ideal para cómo quiero trabajar, aunque, por supuesto, no quiero usarlo en un proyecto de producción a menos que se convierta en una pieza estándar. de la API.

Aquí tienes una idea: haz que un middleware pegue una función sideEffect en la acción. Ligeramente hacky, pero el cambio más mínimo posible necesario para que los reductores puedan iniciar el código asíncrono:

sideEffectMiddleware.js :

export default store => next => action => {
  let sideEffects = [];
  action.sideEffect = callback => sideEffects.push(callback);
  let result = next(action);
  sideEffects.forEach(sideEffect => sideEffect(store));
  return result;
};

Hay varias formas de aplicar el enfoque simple de efectos secundarios. Pruebe sus variantes e informe

Refactoré mi código para usar el enfoque sideEffectMiddleware anterior, y realmente me gusta la organización del código resultante. Es bueno no tener que importar la función sideEffect desde cualquier lugar en mis reductores, solo viene en action .

@ jedwards1211 Publiqué su código como actionSideEffectMiddleware en la versión 2.1.0 del paquete.

@gregwebs ¡genial! ¿Te importaría incluirme como colaborador?

@ jedwards1211 estás agregado. Tal vez agregue el soporte de reproducción adecuado :) No puedo aprovechar eso todavía donde estoy usando esto.

@gaearon Supongo que las acciones grabadas y reproducidas no pasan por el middleware en absoluto, ¿verdad?

Lo hacen, es solo que ya son objetos simples en el momento en que se registran, por lo que el middleware generalmente no interviene.

@gaearon Hmmm. Así que en realidad no he probado esa función, pero ... ¿Supongo que algunos middlewares de efectos secundarios también romperían el récord / la reproducción? Tome redux-effects-fetch por ejemplo, aún realizaría una solicitud ajax en una acción FETCH de objeto simple:

function fetchMiddleware ({dispatch, getState}) {
  return next => action =>
    action.type === 'FETCH'
      ? fetch(action.payload.url, action.payload.params).then(checkStatus).then(deserialize, deserialize)
      : next(action)
}

Si la grabadora limpia las devoluciones [success, failure] llamada steps de la acción FETCH, entonces el middleware redux-effects no enviaría ninguna acción que altere el estado, pero aún así, uno no querría reproduciendo para activar un montón de solicitudes ajax.

¿Hay alguna forma de separar el middleware "puro" (que nunca envía acciones adicionales) como redux-logger del middleware "impuro" (que puede enviar acciones)?

Desafortunadamente, no he examinado redux-effects cerca, por lo que no puedo responder a su pregunta.

No hay problema, en cualquier caso, ¿agrega la grabadora alguna marca a las acciones que el middleware puede usar para decidir si realizar o no efectos secundarios? Solo estoy tratando de encontrar una forma en mi propio middleware para poder admitir las herramientas de desarrollo correctamente

No es así. Se espera que devTools() se coloque _después_ applyMiddleware en la cadena de composición del potenciador, de modo que cuando se alcance la acción, sea una acción “final” que no necesite más interpretación.

Oh, ya veo, entonces, después de que un middleware realiza un efecto secundario, podría eliminar cualquier campo de la acción / modificarlo para que no active el efecto secundario cuando vuelva a través del middleware al reproducirlo, y luego debería trabajar con las herramientas de desarrollo, ¿verdad?

Sí, parece una buena forma.

Genial, gracias por iluminarme!

@gaearon @vladar @ jedwards1211 Puede que le interese https://github.com/salsita/redux-side-effects. Básicamente, es una implementación de elmish (state, action) => (state, sideEffects) pero en lugar de la tupla de retorno del reductor (que ya notó que no es posible), está produciendo efectos.
Jugué con él en breve, la recarga en caliente y la repetición parecen estar bien. No veo ninguna función de redux rota, hasta ahora, pero tal vez algunos de los reductores de alto nivel podrían encontrar algo. :)
Para mí, esto parece una adición

Otro enfoque es redux-saga, que también utiliza generadores pero mantiene los efectos secundarios separados de los reductores.

@gaearon Creo que @minedeljkovic se refería al

Para mí, esto parece una adición realmente importante a la pila de redux, ya que permite que la lógica esté principalmente en los reductores, lo que los convierte en máquinas de estado efectivas y comprobables, que delegan efectos (acciones de transición cuyo resultado no depende solo del estado actual) a servicios externos ( muy parecido a la arquitectura, los efectos y los servicios de Elm).

como la principal ventaja sobre el enfoque tradicional, porque https://github.com/yelouafi/redux-saga/ es muy similar a cómo funciona redux-thunk , en lugar de algo de azúcar de sintaxis en la parte superior, lo que facilita las transacciones de larga duración .

https://github.com/salsita/redux-side-effects por otro lado se parece más a la arquitectura Elm.

Sí, redux-saga y redux-side-effects usan generadores solo para declarar efectos secundarios, manteniendo las sagas y los reductores puros, respectivamente. Eso es similar.

Dos razones por las que prefiero eso en reductores:

  1. Tiene acceso explícito al estado actual que puede afectar la forma en que se debe declarar el efecto (creo que ese fue uno de los puntos de @vladar durante esta discusión)
  2. No se ha introducido ningún concepto nuevo (Saga en redux-saga)

Mi comentario en https://github.com/rackt/redux/issues/1139#issuecomment -165419770 sigue siendo válido ya que redux-saga no resolverá esto y no hay otra forma de resolverlo excepto aceptando el modelo utilizado en la arquitectura del olmo.

Pongo una esencia simple que intenta enfatizar mis puntos sobre los efectos secundarios de redux: https://gist.github.com/minedeljkovic/7347c0b528110889aa50

Traté de definir el escenario más simple posible que creo que es lo suficientemente del "mundo real", pero aún enfatizo algunos de los puntos de esta discusión.

El escenario es:
La aplicación tiene una parte de "Registro de usuario" donde se deben ingresar los datos personales del usuario. Entre otros datos personales, el país de nacimiento se selecciona de la lista de países. La selección se realiza en el cuadro de diálogo "Selección de país", donde el usuario selecciona un país y tiene la opción de confirmar o cancelar una selección. Si el usuario está intentando confirmar la selección, pero no se ha seleccionado ningún país, se le debe advertir.

Restricciones de la arquitectura:

  1. La funcionalidad CountrySelection debe ser modular (en el espíritu de redux ducks ), de modo que pueda reutilizarse en múltiples partes de la aplicación (por ejemplo, también en la parte de la aplicación "Administración de productos", donde se debe ingresar el país de producción)
  2. La porción de estado de CountrySelection no debe considerarse global (estar en la raíz del estado redux), pero debe ser local al módulo (y controlado por ese módulo) que lo invoca.
  3. La lógica central de esta funcionalidad debe incluir la menor cantidad posible de partes móviles (en mi esencia, esta lógica central se implementa solo en reductores (y solo la parte más importante de la lógica). Los componentes deben ser triviales, ya que todos los componentes impulsados ​​por redux deben ser :). Su única responsabilidad sería mostrar el estado actual y enviar acciones).

La parte más importante de la esencia, con respecto a esta conversación, está en la forma en que countrySelection reducer maneja la acción CONFIRM_SELECTION.

@gaearon , sobre todo agradecería su opinión sobre mi afirmación de que los efectos secundarios de redux como adición a redux estándar proporcionan una superficie para la solución más simple considerando las restricciones.

Una posible idea alternativa para la implementación de este escenario (sin usar redux-side-effects, pero usando redux-saga, redux-thunk o algún otro método), también sería muy apropiada.

@ tomkis1 , me encantaría

(Hay una implementación ligeramente diferente en esta esencia https://gist.github.com/minedeljkovic/9d4495c7ac5203def688, donde se evita globalActions. No es importante para este tema, pero tal vez a alguien le gustaría comentar sobre este patrón)

@minedeljkovic Gracias por la esencia. Veo un problema conceptual con tu ejemplo. redux-side-effects está diseñado para usarse solo para efectos secundarios. Sin embargo, en su ejemplo no hay efectos secundarios, sino transacciones comerciales de larga duración y, por lo tanto, redux-saga es mucho más adecuado. @slorber y @yelouafi pueden arrojar más luz sobre esto.

En otras palabras, el problema más preocupante para mí es sincrónico dispatching de nueva acción dentro del reductor (https://github.com/salsita/redux-side-effects/pull/9#issuecomment-165475991):

yield dispatch => dispatch({type: COUNTRY_SELECTION_SUCCESS, payload: state.selectedCountryId});

Creo que @slorber vino con el término llamado "efecto secundario comercial" y este es exactamente tu caso. redux-saga brilla al resolver este problema en particular.

@mindjuice No estoy completamente seguro de entender tu ejemplo, pero me gusta el ejemplo de incorporación que di aquí: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

El patrón de la saga también permite hacer explícitas algunas cosas implícitas.
Por ejemplo, simplemente describe lo que sucedió activando acciones (prefiero eventos porque para mí deberían estar en tiempo pasado), y luego entran en juego algunas reglas comerciales y actualizan la interfaz de usuario. En su caso, la visualización del error está implícita. Con una saga, al confirmar, si no se selecciona ningún país, probablemente enviaría una acción "NO_COUNTRY_SELECTED_ERROR_DISPLAYED" o algo así. Prefiero que sea totalmente explícito.

También se podría ver la Saga como un punto de unión entre patos.
Por ejemplo, tiene pato1 y pato2, con acciones locales en cada uno. Si no le gusta la idea de acoplar los 2 patos (quiero decir, un pato usaría las acciones Creadores del segundo pato), podría dejar que describan lo que sucedió, y luego crear una Saga que conecte algunas reglas 2 patos.

Así que, es un hilo increíblemente largo y todavía hay alguna solución al problema.

Supongamos que tiene un action() asincrónico y su código debe indicar un error o mostrar el resultado.

El primer enfoque fue hacerlo como

// the action call
action().then(dispatch(SUCCESS)).catch(dispatch(FAILURE))

// the reducer
case SUCCESS:
    state.succeeded = true
    alert('Success')

case FAILURE:
    state.succeeded = false
    alert('Failure')

Pero resulta que no es Redux-way porque ahora el reductor contiene "efectos secundarios" (no sé qué se supone que significa eso).
En pocas palabras, la forma correcta es sacar estos alert() s del reductor en algún lugar.

Ese en algún lugar podría ser el componente React que llama a esto action .
Entonces ahora mi código se ve así:

// the reducer
case SUCCESS:
    state.succeeded = true

case FAILURE:
    state.succeeded = false

// the React component
on_click: function()
{
    action().then
    ({
        dispatch(SUCCESS)

        alert('Success')
        // do something else. maybe call another action
    })
    .catch
    ({
        dispatch(FAILURE)

        alert('Failure')
        // do something else. maybe call another action
    })
}

¿Es esta ahora la forma correcta de hacerlo?
¿O necesito investigar más y aplicar alguna biblioteca de terceros?

Redux no es nada sencillo. No es fácil de entender en el caso de las aplicaciones del mundo real. Mabye, se necesita un repositorio de aplicación de muestra del mundo real como ejemplo canónico que ilustra la forma correcta.

@ halt-hammerzeit Redux en sí es muy simple; la confusa fragmentación proviene de diferentes necesidades u opiniones sobre la integración / separación de los efectos secundarios de los reductores cuando se usa Redux.

Además, la fragmentación proviene del hecho de que no es tan difícil hacer efectos secundarios como quieras con Redux. Solo me tomó unas 10 líneas de código para lanzar mi propio middleware de efectos secundarios básicos. Así que abraza tu libertad y no te preocupes por el camino "correcto".

@ jedwards1211 "Redux en sí" no tiene ningún valor o significado y no tiene ningún sentido porque su propósito principal es resolver los problemas cotidianos de los desarrolladores de Flux. Eso implica AJAX, hermosas animaciones y todas las demás cosas _orden y esperadas_

@ halt-hammerzeit tienes razón. Ciertamente, Redux parece tener la intención de lograr que la mayoría de las personas estén de acuerdo sobre cómo administrar las actualizaciones de estado, por lo que es una pena que no haya logrado que la mayoría de las personas estén de acuerdo sobre cómo realizar los efectos secundarios.

¿Has visto la carpeta "ejemplos" en el repositorio?

Si bien hay varias formas de realizar efectos secundarios, generalmente la recomendación es hacerlo fuera de Redux o dentro de los creadores de acciones, como hacemos en todos los ejemplos.

Sí, lo sé ... todo lo que estoy diciendo es que este hilo ha ilustrado una falta de consenso (al menos entre las personas de aquí) sobre la mejor manera de realizar los efectos secundarios.

Aunque quizás la mayoría de los usuarios usan creadores de acciones thunk y nosotros solo somos valores atípicos.

^ lo que dijo.

Preferiría que todas las mentes más brillantes estén de acuerdo en una única solución justa
y tallarlo en piedra para no tener que leer a través de 9000 pantallas
hilo

El jueves 7 de enero de 2016, Andy Edwards [email protected] escribió:

Sí, lo sé ... todo lo que digo es que este hilo ha ilustrado una falta de
consenso (al menos entre la gente de aquí) sobre la mejor manera de realizar
efectos.

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169751432.

Entonces, supongo, la solución actualmente acordada es hacer todo en acción.
creadores. Voy a intentar usar este enfoque y en caso de que encuentre algún defecto en
lo publicaré aquí

El jueves 7 de enero de 2016, Николай Кучумов [email protected] escribió:

^ lo que dijo.

Preferiría que todas las mentes más brillantes estén de acuerdo en una única solución justa
y tallarlo en piedra para no tener que leer a través de 9000 pantallas
hilo

El jueves 7 de enero de 2016, Andy Edwards < [email protected]
<_e i = "16">

Sí, lo sé ... todo lo que digo es que este hilo ha ilustrado una falta de
consenso (al menos entre la gente de aquí) sobre la mejor manera de realizar
efectos.

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169751432.

Este y otros hilos similares existen porque al 1% de los usuarios de Redux les gusta buscar soluciones más potentes / puras / declarativas para los efectos secundarios. Por favor, no dé la impresión de que nadie puede estar de acuerdo con eso. La mayoría silenciosa usa pensamientos y promesas, y en su mayoría está contenta con este enfoque. Pero como cualquier tecnología, tiene desventajas y compensaciones. Si tiene una lógica asincrónica compleja, es posible que desee eliminar Redux y explorar Rx en su lugar. El patrón Redux es fácilmente expresivo en Rx.

OK gracias

El jueves 7 de enero de 2016, Dan Abramov [email protected] escribió:

Este y otros hilos similares existen porque al 1% de los usuarios de Redux les gusta buscar
Soluciones más potentes / puras / declarativas para efectos secundarios. Por favor no
da la impresión de que nadie puede estar de acuerdo en eso. La mayoría silenciosa usa
piensa y promete, y en su mayoría están contentos con este enfoque. Pero como cualquiera
tecnología tiene desventajas y compensaciones. Si tiene lógica asincrónica compleja
es posible que desee eliminar Redux y explorar Rx en su lugar. El patrón de Redux es
fácilmente expresivo en Rx.

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169761410.

@gaearon sí, tienes razón. ¡Y aprecio que Redux sea lo suficientemente flexible para adaptarse a todos nuestros diferentes enfoques!

@ halt-hammerzeit eche un vistazo a mi respuesta aquí donde explico por qué redux-saga puede ser mejor que redux-thunk: http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for- async-flow-in-redux / 34599594

@gaearon Por cierto, su respuesta en stackoverflow tiene el mismo defecto que la documentación: cubre solo el caso más simple
http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594#34599594
Las aplicaciones del mundo real rápidamente van más allá de solo obtener datos a través de AJAX: también deben manejar errores y realizar algunas acciones dependiendo del resultado de la llamada de acción.
Tu consejo es solo dispatch algún otro evento y eso es todo.
Entonces, ¿qué pasa si mi código quiere alert() un usuario al completar la acción?
Ahí es donde esos thunk s no ayudarán, pero las promesas sí.
Actualmente escribo mi código usando promesas simples y funciona de esa manera.

He leído el comentario adjunto sobre redux-saga .
Sin embargo, no entendí lo que hace, lol.
No me gusta tanto la cosa monads y demás, y todavía no sé qué es thunk , y para empezar, no me gusta esa palabra extraña.

Ok, entonces sigo usando Promises en los componentes de React.

Sugerimos devolver las promesas de los thunks a lo largo de los documentos.

@gaearon Sí, de eso estoy hablando: on_click() { dispatch(load_stuff()).then(show_modal('done')) }
Que hará.

Creo que todavía te falta algo.
Consulte el archivo README de redux-thunk.
Muestra cómo encadenar a los creadores de acciones de procesador en la sección "Composición".

@gaearon Sí, eso es exactamente lo que estoy haciendo:

store.dispatch(
  makeASandwichWithSecretSauce('My wife')
).then(() => {
  console.log('Done!');
});

Es exactamente lo que escribí arriba:

on_click()
{
    dispatch(load_stuff())
        .then(() => show_modal('done'))
        .catch(() => show_error('not done'))
}

Esa sección README está bien escrita, gracias

No, ese no era mi punto. Eche un vistazo a makeSandwichesForEverybody . Es un creador de acciones de procesamiento que llama a otros creadores de acciones de procesamiento. Por eso no es necesario poner todo en componentes. Consulte también el ejemplo de async en este repositorio para obtener más información.

@gaearon Pero creo que no sería apropiado poner, digamos, mi elegante código de animación en el creador de acciones, ¿verdad?
Considere este ejemplo:

button_on_click()
{
    this.props.dispatch(load_stuff())
        .then(() =>
        {
                const modal = ReactDOM.getDOMNode(this.refs.modal)
                jQuery(modal).fancy_animation(1200 /* ms */)
                setTimeout(() => jQuery.animate(modal, { background: 'red' }), 1500 /* ms */)
        })
        .catch(() =>
        {
                alert('Failed to bypass the root mainframe protocol to override the function call on the hyperthread's secondary firewall')
        })
}

¿Cómo lo reescribirías de la manera correcta?

@ halt-hammerzeit puede hacer que actionCreator devuelva las promesas para que el componente pueda mostrar algún spinner o lo que sea usando el estado del componente local (difícil de evitar cuando se usa jquery de todos modos)

De lo contrario, puede administrar temporizadores complejos para impulsar animaciones con redux-saga.

Eche un vistazo a esta publicación de blog: http://jaysoo.ca/2016/01/03/managing-processes-in-redux-using-sagas/

Oh, en este caso está bien mantenerlo en componente.
(Nota: generalmente en React es mejor usar el modelo declarativo para todo en lugar de mostrar modales imperativamente con jQuery. Pero no es gran cosa).

@slorber Sí, ya estoy devolviendo Promises y cosas de mis "thunks" (o como los llames; errr, no me gusta esta palabra extraña), así que puedo manejar hilanderos y tal.

@gaearon Ok, te tengo. En el mundo ideal de la maquinaria, por supuesto, sería posible permanecer dentro del modelo de programación declarativa, pero la realidad tiene sus propias demandas, irracionales, digamos, desde el punto de vista de una máquina. Las personas son seres irracionales que no solo operan con ceros y unos puros y requiere comprometer la belleza y pureza del código a favor de poder hacer algunas cosas irracionales.

Estoy satisfecho con el apoyo en este hilo. Mis dudas parecen estar resueltas ahora.

Nueva discusión relevante: https://github.com/reactjs/redux/issues/1528

@gaearon ¡explicación muy impresionante! : trofeo: Gracias: +1:

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