Redux: Essayer de placer les appels API au bon endroit

Créé le 20 juil. 2015  ·  115Commentaires  ·  Source: reduxjs/redux

J'essaie de créer un flux de réussite / erreur de connexion, mais ma principale préoccupation est de savoir où placer cette logique.

J'utilise actuellement actions -> reducer (switch case with action calling API) -> success/error on response triggering another action .

Le problème avec cette approche est que le réducteur ne fonctionne pas lorsque j'appelle l'action à partir de l'appel API.

est-ce que je manque quelque chose?

Réducteur

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;
  }
}

Actions

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 });
  };
}

Appels 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

Commentaire le plus utile

Enfin, où placez-vous l'appel API de connexion?

C'est précisément à cela que sert le créateur d'action dispatch => {} . Effets secondaires!

C'est juste un autre créateur d'action. Associez-le à d'autres actions:

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); });
}

Dans vos composants, il suffit d'appeler

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

// --or--

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

Tous les 115 commentaires

C'est presque correct, mais le problème est que _vous ne pouvez pas simplement appeler des créateurs d'action purs et vous attendre à ce que les choses se passent_. N'oubliez pas que vos créateurs d'actions ne sont que des fonctions qui spécifient _ce qui_ doit être distribué.

// 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.
  }
}

Maintenant, passons à votre exemple. La première chose que je veux clarifier est que vous n'avez pas besoin du formulaire async dispatch => {} si vous ne distribuez qu'une seule action de manière synchrone et que vous n'avez pas d'effets secondaires (vrai pour loginError et loginRequest ).

Ce:

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 });
  };
}

peut être simplifié comme

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 };
}

Deuxièmement, vos réducteurs sont censés être des _ fonctions pures qui n'ont pas d'effets secondaires_. N'essayez pas d'appeler votre API à partir d'un réducteur.

Ce:

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;
  }

devrait probablement ressembler davantage à

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;
  }

Enfin, où placez-vous l'appel API de connexion?

C'est précisément à cela que sert le créateur d'action dispatch => {} . Effets secondaires!

C'est juste un autre créateur d'action. Associez-le à d'autres actions:

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); });
}

Dans vos composants, il suffit d'appeler

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

// --or--

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

Enfin, si vous vous trouvez souvent en train d'écrire de grands créateurs d'action comme celui-ci, c'est une bonne idée d'écrire un middleware personnalisé pour les appels asynchrones compatibles avec les promesses, tout ce que vous utilisez pour l'async.

La technique que j'ai décrite ci-dessus (les créateurs d'action avec la signature dispatch => {} ) est actuellement incluse dans Redux, mais dans la version 1.0, elle ne sera disponible que sous la forme d'un package séparé appelé redux-thunk . Pendant que vous y êtes, vous pouvez aussi bien consulter redux-promise-middleware ou redux-promise .

@gaearon : clap: c'était une explication incroyable! ;) Cela devrait définitivement figurer dans la documentation.

@gaearon explication géniale! :trophée:

@gaearon Que faire si vous devez exécuter une logique pour déterminer quel appel d'API doit être appelé? N'est-ce pas la logique de domaine qui devrait être en un seul endroit (réducteurs)? Garder cela dans les créateurs d'Action me semble comme briser une source unique de vérité. En outre, vous devez principalement paramétrer l'appel d'API par une valeur de l'état de l'application. Vous voudrez probablement aussi tester la logique d'une manière ou d'une autre. Nous avons trouvé que faire des appels d'API dans Reducers (flux atomique) très utile et testable dans un projet à grande échelle.

Nous avons trouvé que faire des appels d'API dans Reducers (flux atomique) très utile et testable dans un projet à grande échelle.

Cela brise l'enregistrement / la relecture. Les fonctions avec effets secondaires sont par définition plus difficiles à tester que les fonctions pures. Vous pouvez utiliser Redux comme ça, mais c'est complètement contre sa conception. :-)

Garder cela dans les créateurs d'Action me semble comme briser une source unique de vérité.

«Une seule source de vérité» signifie que les données vivent en un seul endroit et n'ont pas de copies indépendantes. Cela ne signifie pas que «toute la logique de domaine doit être au même endroit».

Que faire si vous devez exécuter une logique pour déterminer quel appel d'API doit être appelé? N'est-ce pas la logique de domaine qui devrait être en un seul endroit (réducteurs)?

Les réducteurs spécifient comment l'état est transformé par les actions. Ils ne devraient pas s'inquiéter de l'origine de ces actions. Ils peuvent provenir de composants, de créateurs d'actions, de sessions sérialisées enregistrées, etc. C'est la beauté du concept d'utilisation des actions.

Tous les appels d'API (ou la logique qui détermine quelle API est appelée) se produisent avant les réducteurs. C'est pourquoi Redux prend en charge les intergiciels. Le middleware Thunk décrit ci-dessus vous permet d'utiliser des conditions et même de lire à partir de l'état:

// 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

Les créateurs d'action et les middlewares sont conçus pour effectuer des effets secondaires et se compléter mutuellement.
Les réducteurs ne sont que des machines à états et n'ont rien à voir avec l'async.

Les réducteurs spécifient comment l'état est transformé par les actions.

C'est en effet un point très valable, merci.

Je vais rouvrir pour la postérité jusqu'à ce qu'une version de ceci soit dans la documentation.

: +1:

Je dirais que si les réducteurs sont des machines à états, il en va de même pour les créateurs d'action en général (mais implicitement).

Dites que l'utilisateur a cliqué deux fois sur le bouton «Soumettre». La première fois, vous enverrez une requête HTTP au serveur (dans le créateur d'action) et changer d'état en «soumission» (dans le réducteur). La deuxième fois, vous ne voulez pas faire ça.

Dans le modèle actuel, votre créateur d'action devra choisir les actions à envoyer en fonction de l'état actuel. S'il n'est pas en train de "soumettre" - alors envoyez une requête HTTP et envoyez une action, sinon - ne faites rien ou envoyez une action différente (comme un avertissement).

Ainsi, votre flux de contrôle est effectivement divisé en deux machines à états et l'une d'elles est implicite et pleine d'effets secondaires.

Je dirais que cette approche Flux gère très bien la majorité des cas d'utilisation d'applications Web typiques. Mais lorsque vous avez une logique de contrôle complexe, cela peut devenir problématique. Tous les exemples existants n'ont tout simplement pas de logique de contrôle complexe, ce problème est donc quelque peu caché.

https://github.com/Day8/re-frame est un exemple d'approche différente où ils ont un seul gestionnaire d'événements qui agit comme le seul FSM.

Mais alors les réducteurs ont des effets secondaires. Il est donc intéressant de savoir comment ils gèrent la fonction de «relecture» dans de tels cas - leur a-t-on demandé sur https://github.com/Day8/re-frame/issues/86

En général, je pense que c'est un vrai problème et ce n'est pas une surprise qu'il apparaisse encore et encore. Il est intéressant de voir quelles solutions émergeront éventuellement.

Tenez-nous au courant des recadrages et des effets secondaires!

Dans mon livre, il existe 3 types d'état dans toute application Web Redux décente.

1) Afficher les composants (React this.state). Cela devrait être évité, mais parfois je veux simplement implémenter un comportement qui peut être réutilisé en réutilisant simplement un composant indépendamment de Redux.

2) État partagé de l'application, c'est-à-dire Redux.state. C'est l'état qu'une couche de vue utilise pour présenter une application à un utilisateur et que les composants de vue l'utilisent pour «communiquer» entre eux. Et cet état peut être utilisé pour «communiquer» entre ActionCreators, Middlewares, des vues car leurs décisions peuvent dépendre de cet état. Par conséquent, le «partagé» est important. Il n'est cependant pas nécessaire qu'il s'agisse d'un état d'application complet. Ou je pense qu'il est peu pratique de tout mettre en œuvre comme ce type d'état (en termes d'actions).

3) État qui est détenu par d'autres modules / bibliothèques / services complexes à effet secondaire. J'écrirais ces derniers pour gérer les scénarios que vladar décrit. Ou react-router est un autre exemple. Vous pouvez le traiter comme une boîte noire qui a son propre état ou vous pouvez décider de soulever une partie de l'état react-router en état partagé d'application (Redux.state). Si j'écrirais un module HTTP complexe qui gérerait intelligemment toutes mes demandes et ses horaires, je ne suis généralement pas intéressé par les détails de synchronisation des demandes dans Redux.state. J'utiliserais uniquement ce module d'ActionCreators / Middlewares, éventuellement avec Redux.state pour obtenir le nouveau Redux.state.

Si je souhaite écrire un composant de vue montrant les horaires de ma demande, je devrais obtenir cet état dans Redux.state.

@vladar Est-ce que tu veux dire que AC / MW sont des machines à états implicites? Que parce qu'ils ne détiennent pas l'état par lui-même mais qu'ils dépendent toujours d'un état détenu ailleurs et qu'ils peuvent définir la logique de contrôle comment ces états évoluent dans le temps? Dans certains cas, je pense qu'ils pourraient encore être mis en œuvre comme des fermetures et conserver leur propre état, devenant des machines à états explicites?

Sinon, je pourrais appeler Redux.state un "état public", tandis que les autres états sont privés. Comment concevoir mon application est un acte pour décider ce qu'il faut garder en tant qu'état privé et quoi en tant qu'état public. Cela me semble être une belle opportunité d'encapsulation, donc je ne vois pas de problème d'avoir un état divisé en différents endroits dans la mesure où cela ne devient pas un enfer comment ils se touchent.

@vladar

Mais alors les réducteurs ont des effets secondaires. Il est donc intéressant de savoir comment ils gèrent la fonction de "relecture" dans de tels cas

Pour obtenir une relecture facile, le code doit être déterministe. C'est ce que Redux réalise en exigeant des réducteurs purs. Dans un effet, Redux.state divise l'application en parties non déterministes (async) et déterministes. Vous pouvez rejouer le comportement au-dessus de Redux.state en supposant que vous ne fassiez rien de fou dans les composants de vue. Le premier objectif important pour obtenir un code déterministe est de déplacer le code async et de le traduire en code synchrone via le journal des actions. C'est ce que fait l'architecture Flux en général. Mais en général, cela ne suffit pas et d'autres codes à effets secondaires ou mutants peuvent encore interrompre le traitement déterministe.

Atteindre la capacité de relecture avec des réducteurs à effet secondaire est à mon avis soit difficile, voire impossible, ou cela ne fonctionnera que partiellement avec de nombreux cas d'angle avec probablement un peu d'effet pratique.

Pour réaliser un voyage dans le temps facile, nous stockons des instantanés de l'état de l'application au lieu de rejouer les actions (ce qui est toujours difficile) comme cela est fait dans Redux.

Pour réaliser un voyage dans le temps facile, nous stockons des instantanés de l'état de l'application au lieu de rejouer les actions (ce qui est toujours difficile) comme cela est fait dans Redux.

Redux DevTools stocke à la fois les instantanés et les actions. Si vous n'avez pas d'actions, vous ne pouvez pas recharger à chaud les réducteurs. C'est la fonctionnalité la plus puissante du flux de travail activée par DevTools.

Le voyage dans le temps fonctionne bien avec les créateurs d'actions impurs parce que _il se produit au niveau des actions brutes (finales) envoyées_. Ce sont des objets simples, ils sont donc complètement déterministes. Oui, vous ne pouvez pas «annuler» un appel API, mais je ne vois pas de problème avec cela. Vous pouvez annuler les actions brutes émises par celui-ci.

Que parce qu'ils ne détiennent pas l'état par lui-même mais qu'ils dépendent toujours d'un état détenu ailleurs et qu'ils peuvent définir la logique de contrôle comment ces états évoluent dans le temps?

Ouais, c'est exactement ce que je veux dire. Mais vous pouvez également vous retrouver avec une duplication de votre logique de contrôle. Un peu de code pour illustrer le problème (à partir de mon exemple ci-dessus avec double clic).

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 };
}

Et puis vous avez votre réducteur pour vérifier à nouveau l'état "isSubmitting"

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;
      }
  }
}

Vous finissez donc par avoir les mêmes vérifications logiques à deux endroits. Évidemment, la duplication dans cet exemple est minime, mais pour des scénarios plus complexes, cela peut ne pas être joli.

Je pense que dans le dernier exemple, la vérification ne devrait pas être à l'intérieur du réducteur. Sinon, cela peut rapidement sombrer dans l'incohérence (par exemple, l'action est envoyée par erreur, mais ignorée, mais alors l'action «succès» est également distribuée mais elle était inattendue et l'état pourrait être fusionné de manière incorrecte).

(Désolé si c'était le point que vous disiez ;-)

Je suis d'accord avec gaeron car le! IsSubmitting est déjà codé dans le fait que l'action SUBMIT_STARTED a été distribuée. Lorsqu'une action est le résultat d'une logique, cette logique ne doit pas être répliquée dans le réducteur. Ensuite, la responsabilité du réducteur est simplement que, à chaque fois qu'il reçoit SUBMIT_STARTED, il ne pense pas et met simplement à jour l'état avec la charge utile d'action parce que quelqu'un d'autre a pris la responsabilité de décider que SUBMIT_STARTED.

Il faut toujours viser à ce qu'il y ait une seule chose qui assume la responsabilité d'une seule décision.

Le réducteur peut ensuite s'appuyer davantage sur le fait SUBMIT_STARTED et l'étendre avec une logique supplémentaire, mais cette logique devrait être différente. Fe:

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

J'imagine qu'il peut être parfois difficile de décider qui doit être responsable de quoi. ActionCreator, Middleware, module complexe autonome, réducteur?

Je viserais à avoir autant de décisions que possible dans les réducteurs tout en les gardant purs, car ils peuvent être rechargés à chaud. Si des décisions sont nécessaires pour les effets secondaires / async, alors elles vont à AC / MW / quelque part_else. Cela peut également dépendre de l'évolution des meilleures pratiques concernant la réutilisation du code, c'est-à-dire les réducteurs par rapport au middleware.

AC ne peut pas faire muter l'état.est Soumettre directement. Par conséquent, si sa logique en dépend, AC a besoin de garanties que state.isSubmitting sera synchronisé avec sa logique. Si la logique AC veut définir state.isSubmitted sur true avec de nouvelles données et que la lecture est de retour, elle s'attend à ce qu'elle soit définie comme telle. Il ne devrait pas par une autre logique de changer potentiellement cette prémisse. ACTION est la primitive de synchronisation elle-même. Fondamentalement, les actions définissent le protocole et les protocoles doivent être bien et clairement définis.

Mais je pense que je sais ce que vous essayez de dire. Redux.state est un état partagé. L'état partagé est toujours délicat. Il est facile d'écrire du code dans des réducteurs qui change d'état d'une manière inattendue par la logique AC. Par conséquent, je peux imaginer qu'il peut être difficile de garder la logique entre le courant alternatif et les réducteurs synchronisés dans certains scénarios très complexes. Cela peut conduire à des bogues qui peuvent être difficiles à suivre et à déboguer. Je crois que pour de tels scénarios, il est toujours possible de réduire la logique à AC / Middleware et de ne faire agir les réducteurs que stupidement sur la base d'actions bien définies avec peu ou pas de logique.

Quelqu'un peut toujours ajouter un nouveau réducteur qui cassera certains vieux climatiseurs dépendants. Il faut réfléchir à deux fois avant de rendre AC dépendant de Redux.state.

J'ai un créateur d'action qui nécessite un jeton d'authentification renvoyé par une demande d'API avec les informations d'identification de l'utilisateur. Il y a une limite de temps sur ce jeton et il doit être actualisé et géré par quelque chose. Ce que vous proposez semble indiquer que cet état n'appartient pas à Redux.state?

Où doit-il aller? Conceptuellement, est-il passé dans le constructeur du magasin Redux en tant qu'état extérieur immuable?

Par conséquent, je peux imaginer qu'il peut être difficile de garder la logique entre le courant alternatif et les réducteurs synchronisés dans certains scénarios très complexes.

Oui. Je ne dis pas que c'est un spectacle, mais cela peut devenir incohérent. Le but de la machine à états est de réagir aux événements - quel que soit leur ordre d'apparition - et de rester cohérent.

Si vous comptez sur le bon ordre des événements à partir d'AC, vous déplacez implicitement des parties de votre moteur d'état vers AC et le couplez avec un réducteur.

C'est peut-être juste une question de préférence, mais je me sens beaucoup mieux quand le magasin / réducteur agit toujours de manière cohérente et ne compte pas sur quelque chose d'extérieur pour garder la cohérence ailleurs.

Il faut réfléchir à deux fois avant de rendre AC dépendant de Redux.state.

Je pense que vous ne pouvez pas l'éviter dans des cas complexes (sans déplacer certains effets secondaires vers des réducteurs / gestionnaires d'événements).

On dirait que c'est un compromis: "abandonner le rechargement à chaud ou la fonction de relecture" vs "gérer l'état en un seul endroit". Dans un cas plus tard, vous pouvez écrire de vrais FSM ou Statecharts comme les propositions de re-frame, dans le premier cas, c'est une logique de contrôle plus ad-hoc comme nous l'avons dans Flux maintenant.

J'ai lu (trop rapidement) via Re-frame et c'est le même genre de chose que Redux avec quelques différences de détails d'implémentation. Les composants de vue de recadrage sont basés sur des observables, les événements sont des actions, le recadrage distribue les événements (actions) directement sous forme d'objets (ils sont construits dans un composant de vue) sans utiliser de créateurs, les gestionnaires d'événements sont purs et ils sont la même chose que Redux réducteurs.

La gestion des effets secondaires n'est pas beaucoup discutée ou je l'ai ratée, mais ce que je suppose, c'est qu'ils sont gérés dans les middlewares. Et il y a la principale différence. Les middlewares encapsulent les gestionnaires d'événements (réducteurs), composez avec eux de l'extérieur, tandis que les middlewares Redux n'emballent pas les réducteurs. En d'autres termes, Re-frame compose des effets secondaires directement aux réducteurs purs tandis que Redux les maintient séparés.

Cela signifie que l'on ne peut pas enregistrer les événements Re-frame et les rejouer facilement.

Quand je parlais d'incohérences possibles, j'ai considéré l'état partagé comme la cause première et je crois que Re-frame comporte le même risque.

La différence réside dans le couplage des middlewares avec des réducteurs. Lorsque les middlewares terminent en Re-frame, ils transmettent directement le résultat aux gestionnaires d'événements (réducteurs), ce résultat n'est pas enregistré comme événement / action, donc non rejouable. Je dirais que Redux met simplement des crochets entre les deux à la place sous forme d'actions, donc je pense que je peux techniquement obtenir la même chose que Re-frame, avec plus de flexibilité au détriment de plus de frappe (création d'actions) et probablement plus de place pour le faire faux.

En fin de compte, rien ne m'empêche de Redux d'implémenter exactement la même approche que dans Re-frame. Je peux créer n'importe quelle fonction qui prend le réducteur comme paramètre, y faire un traitement, puis appeler directement la fonction de réducteur avec le résultat et l'appeler Reducer Middlewares ou autre. C'est à peu près si je veux le rendre un peu plus facile au détriment de la perte de DevTools et plus de couplage.

Je dois encore réfléchir davantage s'il y a un avantage significatif dans l'approche Re-frame par rapport à une certaine simplification (diminution du nombre d'actions). Je suis ouvert à me prouver le contraire, comme je l'ai dit, je l'ai lu rapidement.

En fait, j'étais un peu incorrect. La différence n'est pas dans les implémentations Redux vs Re-frame, mais vraiment dans «où vous mettez vos effets secondaires».

Si vous faites cela dans les gestionnaires d'événements (réducteurs) et interdisez aux créateurs d'actions d'affecter votre flux de contrôle, il n'y a techniquement aucune différence - vous pouvez également utiliser fsm / statecharts dans Redux et avoir votre flux de contrôle au même endroit.

Mais en réalité, il y a une différence. @gaearon l' a très bien expliqué: Redux s'attend à ce que vos réducteurs soient purs, sinon la fonction de relecture est interrompue. Vous n'êtes donc pas censé faire cela (mettre des effets secondaires dans les réducteurs), car c'est contre la conception de Redux.

Re-frame indique également que les gestionnaires d'événements sont purs, mais même dans Readme, ils mentionnent que les requêtes HTTP sont initiées dans les gestionnaires d'événements. Ce n'est donc pas clair pour moi, mais il semble que leurs attentes soient différentes: vous pouvez mettre vos effets secondaires dans des gestionnaires d'événements.

Ensuite, c'est intéressant comment ils abordent la relecture (et c'est ce que j'ai demandé à https://github.com/Day8/re-frame/issues/86).

Je suppose que la seule façon de le faire lorsque vos gestionnaires d'événements peuvent avoir des effets secondaires est ce que @ tomkis1 a mentionné - l'état d'enregistrement retourné par chaque gestionnaire d'événements (pas de réexécution des réducteurs à chaque événement).

Le rechargement à chaud peut toujours fonctionner, sauf qu'il ne peut pas affecter les événements «passés» - ne produire qu'une nouvelle branche d'état dans le temps.

La différence de conception est donc probablement subtile, mais pour moi, elle est importante.

Je pense qu'ils font des requêtes HTTP en composant des gestionnaires d'événements purs avec des middlewares à effet secondaire. Cette composition retourne un nouveau gestionnaire d'événements qui n'est plus pur mais le gestionnaire interne reste pur. Je ne pense pas qu'ils suggèrent de créer des gestionnaires d'événements qui mélangent la mutation app-db (état) et les effets secondaires. Cela rend le code plus testable.

Si vous enregistrez le résultat d'un gestionnaire d'événements, vous ne pouvez pas le recharger à chaud, ce qui signifie que vous pouvez modifier son code à la volée. Vous auriez à enregistrer le résultat du middleware et ce serait exactement la même chose que Redux - enregistrez ce résultat en tant qu'action et passez-le au réducteur (écoutez-le).

Je pense que je pourrais dire que les middlewares Re-frame sont proactifs alors que Redux permet la réactivité (tout réducteur ad-hoc peut écouter un résultat de middleware / ac). Une chose que je commence à réaliser est que le courant alternatif, principalement utilisé dans Flux et les middlewares, est la même chose que les contrôleurs.

Comme je l'ai compris, @ tomkis1 suggère un voyage dans le temps via un état de sauvegarde, probablement à des intervalles donnés car je suppose que cela pourrait avoir un impact sur les performances si cela était fait sur chaque mutation.

Ok, réalisant que les réducteurs / gestionnaires d'événements retournent un nouvel état complet, donc vous avez dit la même chose.

@merk J'essaierai d'écrire ma réflexion à ce sujet probablement lundi car je ne viendrai probablement pas ici pendant un week-end.

Les documents de recadrage recommandent de parler au serveur dans les gestionnaires d'événements: https://github.com/Day8/re-frame#talking -to-a-server

C'est donc là qu'il diverge de Redux. Et cette différence est plus profonde qu'il n'y paraît à première vue.

Hm, je ne vois pas la différence avec Redux:

Les gestionnaires d'événements initiateurs doivent faire en sorte que les gestionnaires en cas de

Remplacez l'événement par Action. Redux a juste un nom spécial pour un gestionnaire d'événements pur - un réducteur. Je pense qu'il me manque quelque chose ou que mon cerveau est en panne ce week-end.

Je pourrais sentir une différence plus significative dans la manière dont les événements sont exécutés - en file d'attente et asynchrones. Je pense que Redux exécute des actions de synchronisation et de verrouillage action par action pour garantir la cohérence de l'état. Cela peut également affecter la rejouabilité car je ne suis pas sûr que le simple fait de sauvegarder des événements et de les rejouer encore et encore aboutira au même état final. Sans tenir compte de cela, je ne peux pas simplement faire cela parce que je ne sais pas quels événements déclenchent des effets secondaires contrairement à Redux où les actions déclenchent toujours du code pur.

@vladap Pour résumer la différence telle que je la vois:

1.

  • dans Redux, vous lancez une requête de serveur dans le créateur d'action (voir @gaearon réponse à la question initiale de ce problème); vos réducteurs doivent être purs
  • dans Re-frame, vous le faites dans le gestionnaire d'événements (réducteur); vos gestionnaires d'événements (réducteurs) peuvent avoir des effets secondaires

2.

  • Dans le premier cas, votre flux de contrôle est divisé entre le courant alternatif et le réducteur.
  • Dans le second cas, tout votre flux de contrôle est en un seul endroit - gestionnaire d'événements (réducteur).

3.

  • Si votre flux de contrôle est divisé - vous avez besoin d'une coordination entre AC et réducteur (même si elle est implicite): AC lit l'état produit par le réducteur; le réducteur suppose que les événements sont correctement classés par AC. Techniquement, vous introduisez le couplage du courant alternatif et du réducteur, car les modifications apportées au réducteur peuvent également affecter le courant alternatif et vice versa.
  • Si votre flux de contrôle se trouve au même endroit - vous n'avez pas besoin de coordination supplémentaire + vous pouvez utiliser des outils tels que FSM ou Statecharts dans vos gestionnaires / réducteurs d'événements


    1. La façon Redux d'appliquer des réducteurs purs et de déplacer les effets secondaires vers les AC permet le rechargement à chaud et la relecture

  • Recadrer la manière d'avoir des effets secondaires dans les gestionnaires d'événements (réducteurs) ferme la porte pour rejouer comme cela se fait dans Redux (en réévaluant les réducteurs), mais d'autres options (probablement moins puissantes) sont toujours possibles - comme @ tomkis1 l'a mentionné

Quant à la différence d'expérience de développement, elle n'apparaît que lorsque vous avez beaucoup de choses asynchrones (minuteries, requêtes http parallèles, sockets Web, etc.). Pour les trucs de synchronisation, c'est pareil (puisqu'il n'y a pas de flux de contrôle dans les AC).

Je suppose que je dois préparer un exemple concret d'un scénario aussi complexe pour clarifier mon propos.

Je pourrais vous rendre fou, je suis désolé pour cela mais nous ne sommes pas d'accord sur une chose fondamentale. Peut-être que je devrais lire le code d'exemples de recadrage, j'apprendrai Clojure un jour. Peut-être que vous l'avez fait, donc vous en savez plus. Merci pour vos efforts.

1.

dans Redux, vous lancez une requête de serveur dans le créateur d'action (voir @gaearon réponse à la question initiale de ce problème); vos réducteurs doivent être purs
dans Re-frame, vous le faites dans le gestionnaire d'événements (réducteur); vos gestionnaires d'événements (réducteurs) peuvent avoir des effets secondaires

Doc I made bold dit explicitement que je devrais lancer un appel webapi dans un gestionnaire d'événements, puis envoyer l'événement dans son cas de succès. Je ne comprends pas comment je peux gérer cet événement distribué dans le même gestionnaire d'événements. Cet événement en cas de succès est envoyé au routeur (eq. Du répartiteur je pense) et j'ai besoin d'un autre gestionnaire d'événements pour le gérer. Ce deuxième gestionnaire d'événements est pur et est un équivalent du réducteur Redux, le premier est équivalent à ActionCreator. Pour moi, c'est la même chose ou je manque clairement un aperçu important.

Quant à la différence d'expérience de développement, elle n'apparaît que lorsque vous avez beaucoup de choses asynchrones (minuteries, requêtes http parallèles, sockets Web, etc.). Pour les trucs de synchronisation, c'est pareil (puisqu'il n'y a pas de flux de contrôle dans les AC).

Nous ne sommes pas d'accord sur celui-ci. Les éléments asynchrones n'ont pas d'importance, vous ne pouvez pas les rejouer dans Redux, vous n'avez donc aucune expérience de développement ici. La différence apparaît lorsque vous avez de nombreux réducteurs purs complexes. Dans Redux, vous pouvez prendre l'état A, une séquence d'action, un réducteur, le rejouer et obtenir l'état B.Le vous gardez tout le même mais vous modifiez le code du réducteur et le rechargez à chaud, rejouez la même séquence d'action à partir de l'état A et vous obtenir l'état C, voyant immédiatement l'impact de votre changement de code. Vous ne pouvez pas le faire avec l'approche @ tomkis1 .

Vous pouvez même créer des scénarios de test en plus. Enregistrez un état initial, enregistrez une séquence d'actions, réduisez la séquence d'actions à l'état A, obtenez l'état B et affirmez-le. Ensuite, apportez des modifications au code qui, selon vous, donneront le même état B, relisez le scénario de test pour affirmer qu'il est vrai et que vos modifications n'ont pas interrompu votre application. Je ne dis pas que c'est la meilleure approche de test, mais cela peut être fait.

En fait, je serais assez surpris si les Clojurists que je considère presque comme des pratiquants acharnés de la programmation fonctionnelle comme Haskellers recommanderaient de mélanger des effets secondaires avec du code qui pourrait être pur au moins sans un certain niveau de composition.

L'évaluation / réactivité paresseuse est la technique de base pour déplacer le code à effet secondaire aussi loin que possible d'un code par ailleurs pur. L'évaluation paresseuse est la façon dont Haskell a finalement appliqué un concept de pureté fonctionnelle peu pratique pour obtenir un programme qui fait quelque chose de pratique, car un programme complètement pur ne peut pas faire grand-chose. En utilisant une composition monadique et d'autres, l'ensemble du programme est créé sous forme de flux de données et pour garder la dernière étape du pipeline pure et ne jamais appeler d'effets secondaires dans votre code, le programme renvoie une description de ce qui doit être fait. Il est passé au runtime qui l'exécute paresseusement - vous ne déclenchez pas l'exécution par un appel impératif. Au moins, c'est ainsi que je comprends la programmation fonctionnelle du point de vue d'oiseau. Nous n'avons pas un tel runtime, nous le simulons avec ActionCreators et réactivité. Avis de non-responsabilité, ne le prenez pas comme acquis, c'est ce que j'ai compris après avoir lu peu de choses sur la PF comme étant une autorité quelconque ici. Je suis peut-être parti, mais je crois en fait que j'ai compris le point.

Je ne comprends pas comment je peux gérer cet événement distribué dans le même gestionnaire d'événements.

Je ne dis pas ça. Par «effets secondaires», j'entends avoir une requête HTTP lancée dans le gestionnaire d'événements (réducteur). L'exécution d'E / S est également un effet secondaire, même si cela n'affecte pas le retour de votre fonction. Je ne veux pas dire "effets secondaires" en termes de modification de l'état directement dans les gestionnaires de succès / erreurs.

Ce deuxième gestionnaire d'événements est pur et est un équivalent du réducteur Redux, le premier est équivalent à ActionCreator.

Je ne pense pas que le premier soit équivalent à Action Creator. Sinon, pourquoi avez-vous besoin d'Action Creators pour faire cela? Si vous pouvez faire la même chose en réducteur?

Vous ne pouvez pas le faire avec l'approche @ tomkis1 .

Se mettre d'accord. Et c'est ce que je voulais dire quand j'ai écrit "d'autres options (probablement moins puissantes)".

Je ne pense pas que le premier soit équivalent à Action Creator. Sinon, pourquoi avez-vous besoin d'Action Creators pour faire cela? Si vous pouvez faire la même chose en réducteur?

Les ActionCreators sont utilisés pour isoler les effets secondaires de l'application pure (qui peut être rejouée). Comme Redux est conçu, vous ne pouvez pas (ne devriez pas) effectuer d'effets secondaires dans les réducteurs. Vous avez besoin d'une autre construction dans laquelle vous pouvez placer du code à effet secondaire. Dans Redux, ces constructions sont AC ou middlewares. La seule différence que je vois dans le re-frame est ce que font les middlewares et ce re-frame n'a pas de middlewares pour transformer les événements (actions) avant que les gestionnaires ne soient déclenchés.

Au lieu d'AC / middlewares, tout peut être utilisé - module util, services (comme les services angulaires), vous pouvez le construire en tant que bibliothèque séparée que vous interfacerez ensuite par AC ou middleware et traduisez son API en actions. Si vous me demandez, AC n'est pas le bon endroit pour placer ce code. AC ne devrait être que de simples créateurs / usines construisant des objets d'action à partir d'arguments. Si je voulais être puriste, il serait préférable de placer le code à effet secondaire strictement dans les middlewares. ActionCreator est un mauvais terme pour désigner une sorte de responsabilité de contrôleur. Mais si ce code n'est qu'un appel webapi à une seule ligne, il y a une tendance à le mettre simplement dans AC et pour une application simple, cela peut aller.

AC, middlewares, bibliothèques tierces forment une couche que j'aime appeler une couche de service. Les WebApiUtils d'origine de Facebook feraient partie de cette couche, comme si vous utilisiez react-router et écoutiez ses modifications et les traduisiez en actions pour mettre à jour Redux.state, Relay peut être utilisé comme couche de service (plutôt que d'utiliser Redux pour l'application / vue Etat). Comme la plupart des bibliothèques tierces sont actuellement programmées, elles ne fonctionnent pas bien avec la relecture. De toute évidence, je ne veux pas tout réécrire à partir de zéro, alors ce serait le cas. Ce que j'aime à y penser, c'est que ces bibliothèques, services, utils, etc. étendent l'environnement du navigateur en formant ensemble une plate-forme sur laquelle je peux créer mon application rejouable. Cette plate-forme regorge d'effets secondaires, en fait c'est l'endroit où je déplace intentionnellement les effets secondaires. Je traduis cette API de plate-forme en actions et mon application se connecte indirectement à cette plate-forme en écoutant ces objets d'action (en essayant de simuler l'approche Haskell que j'ai essayé de décrire dans un article ci-dessus). C'est une sorte de piratage dans la programmation fonctionnelle pour savoir comment atteindre la pureté (peut-être pas dans un sens académique absolu) mais déclencher des effets secondaires.

Il y a une autre chose qui me trouble mais je pourrais vous mal comprendre. Je pense que vous avez dit que pour une logique complexe, il est avantageux d'avoir tout en un seul endroit. Je pense tout à fait contraire.

L'ensemble de cette activité d'actions est similaire dans son concept aux événements DOM. Je n'aimerais vraiment pas mélanger mon code métier avec un code permettant au navigateur de détecter l'événement mousemove et je suis sûr que vous ne le feriez pas non plus. Heureusement, j'ai l'habitude de travailler sur addListener ('mousemove', ...) car ces responsabilités sont bien séparées. Le but est de réaliser cette bonne séparation des préoccupations pour ma logique métier également. ActionCreators, middlewares sont des outils pour cela.

Imaginez que j'écrirais une application sur une webapi obsolète qui ne correspond pas bien aux besoins de mon entreprise. Pour obtenir les données requises, je devrais appeler quelques points de terminaison, utiliser leur résultat pour créer les prochains appels, puis fusionner tous les résultats dans une forme canonique que j'utiliserais dans Redux.state. Cette logique ne s'infiltrerait pas dans mes réducteurs pour ne pas avoir à transformer les données de mes réducteurs encore et encore. Ce serait dans les middlewares. En fait, j'isolerais les API obsolètes désordonnées et développerais mon application professionnelle comme elle serait écrite sur une belle API. Une fois cela fait, j'obtiendrais peut-être un changement pour reconstruire notre webapi, puis je réécrirais simplement mon middleware.

Par conséquent, avoir tout au même endroit semble facile pour une application facile. Mais bien au contraire pour chose complexe je vois une bonne séparation des préoccupations bénéfique. Mais peut-être que je n'ai pas compris votre point ou votre cas d'utilisation auquel vous faites référence.

En fait, je déplacerais probablement la plus grande partie de la transformation des données vers sa forme canonique vers des réducteurs et utiliserais plutôt la composition des réducteurs, car cela me donnerait une rejouabilité et une testabilité pour ce code qui est généralement pur.

Je ne comprends pas comment je peux gérer cet événement distribué dans le même gestionnaire d'événements.
Je ne dis pas ça.

Je pensais que vous vouliez mélanger les appels Webapi avec la logique des réducteurs pour les avoir au même endroit. Ce que je dis, c'est que vous ne le faites pas en recadrage non plus parce qu'ils suggèrent d'envoyer l'événement en cas de succès.

Il peut y avoir des situations où webapi -> beaucoup de logique -> webapi -> beaucoup de logique -> ... -> résultat pour le réducteur, toute la chaîne déclenchée par un simple clic. Dans de tels cas, la plupart de la logique serait probablement en AC, je ne la diviserais pas tant que tous les effets secondaires ne seraient pas terminés. Est-ce ce à quoi vous parlez?

Le mieux que je puisse faire ici est de déplacer autant de logique vers des fonctions pures pour la testabilité, mais elles seraient appelées dans la portée AC.

Je ne pense pas que j'aime la possibilité de le faire comme une série de gestionnaires d'événements indirectement connectés. Ce serait une chaîne de promesses ou d'observables.

Les Reduxers plus expérimentés pourraient avoir une opinion différente sur cette pensée ( @gaearon , @johanneslumpe , @acdlite , @emmenko ...)

@vladap Je pense que cette conversation s'est trop éloignée du sujet de ce problème. Vous avez déjà mentionné le point principal de mes commentaires ici:

Par conséquent, je peux imaginer qu'il peut être difficile de garder la logique entre le courant alternatif et les réducteurs synchronisés dans certains scénarios très complexes.

Exemple rapide:

Situation 1: Vous avez une liste des 10 meilleurs auteurs de blog quelque part sur la page. Vous avez maintenant supprimé une catégorie d'articles dans le blog. En cas de suppression réussie, vous devez mettre à jour cette liste d'auteurs à partir du serveur pour la mettre à jour.

Situation 2: Vous êtes sur une autre page. Il n'a pas de liste d'auteurs, mais il a une liste des 10 meilleurs commentaires. Vous pouvez également supprimer une catégorie d'articles de blog et devrez mettre à jour la liste des commentaires en cas de suppression réussie.

Alors, comment gérer cela au niveau du moteur d'état? (nous pourrions également utiliser les hooks React pour obtenir de l'aide, mais un bon état du moteur devrait être capable de rester cohérent par lui-même)

Options:
A) nous mettons cette logique dans AC. Ensuite, AC touchera CategoryApi (pour supprimer), lira à partir de userList state et commentList state (pour vérifier quelles listes sont présentes dans l'état maintenant), parlez à UserListApi et / ou CommentListApi (pour rafraîchir les listes) + dispatch TOP_AUTHORS_UPDATED et / ou TOP_COMMENTS_UPDATED . Donc, il touchera essentiellement 3 domaines différents.

B) nous le mettons dans les gestionnaires d'événements userList et commentList . Ces gestionnaires écoutaient à la fois l'événement DELETE_CATEGORY_SUCCESS , puis appelaient leur service API et à leur tour distribueraient l'événement TOP_AUTHORS_UPDATED / TOP_COMMENTS_UPDATED . Ainsi, chaque gestionnaire ne touche que les services / l'état de son propre domaine.

Cet exemple est probablement trop simpliste, mais même à ce niveau, les choses deviennent moins jolies avec les appels d'API dans les AC.

La différence vient du fait que contrairement aux gestionnaires d'événements dans le recadrage, les ActionCreators sont proactifs dans la gestion de la logique métier. Et un seul AC peut être exécuté en fonction d'un événement DOM. Cet AC de haut niveau peut cependant appeler d'autres AC. Il peut y avoir des AC séparés pour UserListApi et CommentListApi afin que les domaines soient mieux séparés, mais il doit toujours y avoir un AC (comme un contrôleur) qui les connecte. Cette partie est un code impératif assez classique tandis que le recadrage est entièrement basé sur les événements. Avec un peu de travail, il peut être remplacé par une approche préférée, basée sur des événements, des observables, des cps, etc.

@vladap Il serait intéressant de voir si une autre approche est viable: quand les réducteurs peuvent avoir des effets secondaires, mais peuvent aussi les isoler afin qu'ils puissent être ignorés lors de la relecture.

Supposons que la signature des réducteurs devienne: (state, action) => (state, sideEffects?) où sideEffects est une fermeture. Ensuite, en fonction du contexte, le cadre pourrait évaluer ces effets secondaires ou les ignorer (en cas de relecture).

Ensuite, l'exemple de la description originale du problème ressemblerait à ceci:

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;
  }
}

(tout le reste reste à peu près le même)

Au moins, votre flux de contrôle est au même endroit, vos effets secondaires sont toujours simples (car ils créent simplement une autre action / événement dans les gestionnaires asynchrones et la logique reste dans les réducteurs - donc une plus grande partie de votre code sera rejouée).

Vous pouvez également écrire du code FSM normal ou de type Statechart (avec composition de réducteurs / gestionnaires d'événements)

De plus, vous n'avez pas besoin de renvoyer la fermeture avec une signature prédéfinie dans les créateurs d'action.

Je ne sais pas quels problèmes cette approche pourrait potentiellement causer, mais moi, cela vaut la peine de jouer.

Supposons que la signature des réducteurs devienne: (état, action) => (état, effets secondaires?) Où sideEffects est une fermeture. Ensuite, en fonction du contexte, le cadre pourrait évaluer ces effets secondaires ou les ignorer (en cas de relecture).

C'est en fait similaire à ce que j'ai entendu Elm faire avec (State, Action) => (State, Request?) . Je n'ai pas encore vu d'exemples, mais si quelqu'un veut explorer cela, n'hésitez pas.

Une autre approche dont on peut peut-être s'inspirer est une implémentation de la sous-traitance d'événements au-dessus du modèle Actor - Akka Persistance , PersistentFSM . Je ne dis pas que c'est mieux mais ce n'est pas mal de connaître d'autres tentatives. Les acteurs peuvent utiliser des effets secondaires, mais si je me souviens bien, il faut parfois écrire du code explicite pour rejouer.

Peut-être que la fonction de relecture peut fausser nos décisions à qui appartient le code. Il semble que nous commençons à penser comme ... "Je veux ce rejouable donc il doit aller à un réducteur". Cela peut conduire à un code aux limites peu claires car nous ne le découpons pas en fonction de ses responsabilités / rôles et nous avons tendance à découper artificiellement notre logique métier pour la rendre rejouable.

Si je mettais de côté la fonction de relecture, comment j'écrirais mon code d'entreprise? Je viserais à avoir ma logique métier devant un journal d'actions et à l'avoir en un seul endroit. Les machines à états de logique métier, ou tout ce que j'utiliserais, feraient toutes les décisions complexes et difficiles résultant en un flux déjà résolu de FACTS (actions). Les réducteurs seraient généralement de simples mises à jour et leur principale responsabilité serait de savoir comment appliquer la charge utile d'action à Redux.state. Ils pourraient avoir une certaine logique, mais dans un sens, interpréter les faits différemment pour obtenir un autre type de vue sur le flux de faits (action) pour le présenter à un utilisateur.

Pour trancher les responsabilités, j'aime aussi y penser de cette façon. Quel code pourrait être potentiellement demandé pour être réimplémenté sur un serveur? Fe pour mieux prendre en charge les appareils lents en déplaçant la logique métier coûteuse de calcul, peut-être pour des raisons de sécurité, la propriété du renseignement cachant quoi que ce soit. Je ne peux pas déplacer la logique des réducteurs vers le serveur aussi facilement car elle est liée à une couche de vue. Si une logique métier complexe est écrite devant le journal des actions, je peux le remplacer par des appels REST se traduisant par le même flux d'action et ma couche de vue devrait fonctionner (probablement).

L'inconvénient est que contrairement à Akka Persistance, je n'ai pas la possibilité de rejouer ce code.

Je dois un jour examiner de plus près Elm et comprendre comment votre proposition fonctionnerait.

@gaearon Merci d'avoir mentionné Elm. J'ai trouvé cette conversation - https://gist.github.com/evancz/44c90ac34f46c63a27ae - avec des idées similaires (mais beaucoup plus avancées).

Ils introduisent le concept de tâches (http, db, etc.) et d'effets (juste une file d'attente de tâches). Ainsi votre "réducteur" peut en effet renvoyer de nouveaux états et effets.

Ce qui est cool à ce sujet (et je n'y avais pas pensé de cette façon) - c'est que si vous pouvez collecter des tâches tout en chaînant vos réducteurs - et appliquer plus tard une fonction à cette liste. Supposons que vous puissiez grouper les requêtes http, encapsuler les requêtes DB avec des transactions, etc.

Ou vous pourriez avoir, dites "gestionnaire de relecture" qui est juste un noop pour les effets.

@merk Je pense que la plupart ont déjà été écrits. Juste que ce type de code à effet secondaire autonome est probablement le plus délicat. Cela fera probablement des ravages lors de la relecture, car les intervalles ne seront pas synchronisés avec le parcours du temps en supposant que le parcours du temps fonctionne sur le même code et que les intervalles commenceront également en mode de lecture.

L'application habituelle n'a pas besoin de jeton et de compte à rebours d'expiration présenté à un utilisateur, donc techniquement, elle n'a pas besoin d'être dans Redux.state. L'application d'administration peut en avoir besoin. Par conséquent, vous pouvez le mettre en œuvre dans les deux sens, ce qui vous convient.

Une option consiste à démarrer le compte à rebours d'expiration dans la connexion AC et je suppose qu'il s'exécute sans fin et meurt avec l'application ou que la déconnexion doit le nettoyer. Lorsque l'intervalle se déclenche, il fait ce qui est nécessaire pour confirmer le jeton et s'il expire, il distribue l'action LOGIN_EXPIRED et le réducteur d'écoute efface la session utilisateur et change d'emplacement, ce qui déclenche la transition du routeur vers / login.

Une autre consiste à utiliser la bibliothèque tierce et à lui déléguer les préoccupations et à traduire son API en action LOGIN_EXPIRED. Ou écrivez le vôtre et conservez l'état du jeton et du compte à rebours ici si vous n'en avez pas besoin dans la couche de vue.

Vous pouvez le conserver dans Redux.state mais cet état est volatil. Avec un grand état, nous pouvons arriver aux mêmes problèmes que la programmation avec des variables globales. Et il est difficile, probablement impossible de le tester. Il est facile de tester les réducteurs, mais on prouve que cela fonctionne lorsqu'un seul réducteur met à jour cette clé particulière dans l'objet d'état. Cela peut devenir mauvais dans un grand projet avec de nombreux développeurs de qualités différentes. N'importe qui peut décider d'écrire un réducteur et penser qu'un élément d'état est bon à mettre à jour et je ne peux pas imaginer comment tester tous les réducteurs ensemble dans toutes les séquences de mise à jour possibles.

En fonction de votre cas d'utilisation, il peut être judicieux de protéger cet état, car il s'agit d'une connexion - une fonctionnalité assez importante, et de la séparer. Si cet état est nécessaire dans une couche de vue, répliquez-le sur Redux.state de la même manière que l'état d'emplacement de react-router est répliqué sur Redux.state dans certains exemples. Si votre état et votre équipe sont petits et bien élevés, vous pouvez être bien de l'avoir au même endroit.

@vladar @vladap

Wow, cet élément Elm est vraiment cool! Cela me rappelle aussi l' architecture des «pilotes» de

@merk En fait, le LOGIN_EXPIRED autonome n'affecterait pas beaucoup la relecture car il arriverait à la fin du journal des actions et ne serait pas traité immédiatement par une relecture et peut-être qu'il ne passerait pas du tout au journal des actions - je ne sais pas comment exactement la relecture est implémentée.

@gaeron Il semble qu'au plus haut niveau, Elm and Cycle implémente le modèle que j'essayais de décrire si je le comprends bien. J'ai un navigateur qui me donne une plateforme de base, j'étends cette plateforme avec ma couche service (http, db ...) pour construire une plateforme pour mon application. Ensuite, j'ai besoin d'une sorte de colle qui interagit avec la couche de service et permet à mon application pure de communiquer indirectement avec elle afin que mon application puisse décrire ce qu'elle veut faire mais pas l'exécuter elle-même (messages envoyés aux pilotes de Cycle, Elm Effects avoir une liste de tâches). Je pourrais dire que mon application construit un "programme" utilisant des structures de données (me rappelle le code comme mantra de données) qui est ensuite passé à la couche de service pour exécution et mon application ne s'intéresse qu'au résultat de ce "programme" qu'elle applique ensuite à son état.

@merk : Plus loin. Si le jeton et son expiration seraient placés dans Redux.state / ou n'importe où dans tout autre service js, il ne sera pas transféré lorsque l'utilisateur ouvre l'application dans un nouvel onglet. Je dirais que l'utilisateur s'attend à ce qu'il reste connecté. Je suppose donc que cet état devrait être en fait dans SessionStore.

Même sans cela, les états peuvent être séparés lorsque l'expiration du jeton ne signifie pas le nettoyage immédiat des informations utilisateur et la transition vers / login. Jusqu'à ce que l'utilisateur ne touche pas le serveur avec ses interactions (il a suffisamment de données en cache), il peut continuer à travailler et l'action LOGIN_EXPIRED ne se déclenche qu'une fois qu'il fait quelque chose nécessitant un serveur. L'état isLogged basé sur Redux.state ne signifie pas nécessairement qu'il existe un jeton valide. L'état isLogged dans Redux.state est utilisé pour décider quoi rendre, l'état autour d'un jeton peut être caché à une couche de vue et maintenu uniquement au niveau des créateurs d'action sans avoir à écrire des actions et des réducteurs pour cela, sauf ceux affectant une couche de vue.

@vladar Je pense que je pourrais comprendre votre point général. Je pense que je ne l'ai pas réalisé avant.

Il n'y a pas de moyen facile de renvoyer le contrôle du réducteur au créateur d'action et de transmettre certaines valeurs. Supposons que ces valeurs ne sont pas nécessaires pour le rendu, elles sont même temporaires mais elles sont nécessaires pour lancer une opération asynchrone.

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
}

Il existe 3 options pour lancer une opération asynchrone à l'aide de x, y calculés dans le réducteur. Aucun d'eux n'est parfait.

1) Déplacez la logique du réducteur au créateur d'action et réduisez la puissance de rechargement à chaud. Le réducteur n'est qu'un programme de mise à jour d'état stupide ou possède une logique L3 à partir de.

2) Enregistrez x, y dans Redux.state, en le polluant avec des valeurs temporaires / transitoires et en l'utilisant essentiellement comme un canal de communication global entre les réducteurs et les créateurs d'action, ce que je ne suis pas convaincu d'aimer. Je pense que c'est bien s'il s'agit d'un état réel, mais pas pour ce genre de valeurs. Le créateur d'action devient:

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) Enregistrez x, y dans l'état, utilisez-le comme accessoires dans certains composants et tunnelisez-le dans toute la boucle de la couche de vue en utilisant componentWillReceiveProps déclenchant un nouveau créateur d'action et l'opération asynchrone. La pire option si vous me le demandez, répandre la logique métier partout.

@vladar
En général, peu importe où async est lancé dans la mesure où le résultat de cette opération est compressé en tant qu'action, distribué et appliqué par un réducteur. Cela fonctionnera de la même manière. C'est la même discussion qu'avec le flux vanille si l'async doit être initié dans les créateurs d'action ou dans les magasins.

Il faudrait que Redux soit capable d'identifier et d'ignorer ces appels asynchrones lors de la relecture, ce qui est exactement ce que vous suggérez.

Il n'y a pas de moyen facile de renvoyer le contrôle du réducteur au créateur d'action et de transmettre certaines valeurs.

Je dirais que c'est l'un des symptômes. Votre deuxième exemple illustre très bien mon point de vue (voir ci-dessous).

Considérez le flux généralisé de transition d'état (dans le monde FSM):

  1. L'événement arrive
  2. Étant donné l'état actuel, FSM calcule le nouvel état cible
  3. FSM effectue des opérations pour passer de l'état actuel au nouvel état

Notez que l'état actuel est important! Un même événement peut conduire à des résultats différents selon l'état actuel. Et en conséquence à différentes séquences d'opérations (ou d'arguments) à l'étape 3.

C'est donc à peu près ainsi que fonctionne la transition d'état dans Flux lorsque votre action est synchronisée. Vos réducteurs / magasins sont en effet des FSM.

Mais qu'en est-il des actions asynchrones? La plupart des exemples de Flux avec des actions asynchrones vous obligent à penser qu'il est inversé:

  1. AC effectue une opération asynchrone (quel que soit l'état actuel)
  2. AC envoie une action à la suite de cette opération
  3. Le réducteur / magasin le gère et change d'état

L'état actuel est donc ignoré, l'état cible est supposé être toujours le même. Alors qu'en réalité, le flux de transition d'état cohérent est toujours le même. Et cela doit ressembler à ceci (avec les AC):

  1. AC obtient l'état actuel avant que l'action ne soit envoyée
  2. AC expédie l'action
  3. AC lit le nouvel état après l'action
  4. L'état donné avant l'action et après - il décide quelles opérations effectuer (ou quelles valeurs passer en arguments)

Exactement votre deuxième exemple. Je ne dis pas que toutes ces étapes sont nécessaires tout le temps. Souvent, vous pouvez en omettre certains. Mais dans les cas complexes, c'est ainsi que vous devrez agir. C'est le flux généralisé de toute transition d'état. Et cela signifie également que les limites de votre FSM ont été déplacées vers AC vs réducteur / magasin.

Mais le déplacement des frontières pose d'autres problèmes:

  1. Si les opérations asynchrones étaient en magasin / réducteur, vous pourriez avoir deux FSM indépendants réagissant au même événement. Donc, pour ajouter une nouvelle fonctionnalité - vous pouvez simplement ajouter un magasin / réducteur qui réagit à un événement existant et c'est tout.
    Mais avec les opérations dans AC - vous devrez ajouter un magasin / réducteur et également modifier l'AC existant en y ajoutant également une partie asynchrone de la logique. S'il contenait déjà une logique asynchrone pour un autre réducteur ... ça ne refroidit pas.
    Le maintien d'un tel code est évidemment plus difficile.
  2. Le flux de contrôle de votre application est différent en cas d'actions de synchronisation et d'asynchronisation. Pour les actions de synchronisation, le réducteur contrôle la transition, en cas d'asynchrone - AC contrôle effectivement toutes les transitions qui pourraient être potentiellement causées par cet événement / cette action.

Notez que la plupart des problèmes sont masqués par de simples exigences d'état de la plupart des applications Web. Mais si vous avez une application avec état complexe, vous serez certainement confronté à ces situations.

Dans la plupart des cas, vous trouverez probablement toutes sortes de solutions de contournement. Mais vous pourriez les éviter en premier lieu si votre logique de transition d'état était mieux encapsulée et que vos FSM n'étaient pas séparés entre les AC et les réducteurs.

Malheureusement, la partie «à effet secondaire» de la transition d'état fait toujours partie de la transition d'état, et non une partie de logique indépendante distincte. C'est pourquoi pour moi (state, action) => (state, sideEffects) semble plus naturel. Ouais, ce n'est plus une signature "réduire"%) Mais le domaine du framework n'est pas la transformation des données, mais les transitions d'état.

C'est la même discussion qu'avec le flux vanille si l'async doit être initié dans les créateurs d'action ou dans les magasins.

Oui, mais Flux ne vous interdit pas d'avoir des trucs asynchrones dans les magasins tant que vous distribuez () directement dans les rappels asynchrones par rapport à l'état de mutation. Ils sont en quelque sorte sans opinion, même si la plupart des implémentations recommandent d'utiliser des AC pour async. Personnellement, je pense que c'est une réminiscence de MVC, car il est pratique de traiter mentalement les AC comme des contrôleurs plutôt que de passer mentalement aux FSM.

@vladar

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

Juste à y penser. Cela signifie que nous devions changer le type de retour de la fonction combineReducers en [state, SideEffects] ou [state, [SideEffects]] et changer son code pour combiner SideEffects des réducteurs. Original (state, action) => le type de réducteur d'état pourrait rester pris en charge car combineReducers obligerait (state) de renvoyer le type à [state] ou [state, []].

Ensuite, nous devons exécuter et distribuer SideEffects quelque part dans Redux. Cela pourrait être fait dans Store.dispatch () qui appliquerait un nouvel état et exécuterait la liste SideEffects et la distribuerait. Je me demande si nous pouvons entrer dans une belle récursion sans fin.

Sinon, il pourrait être exécuté quelque part au niveau du middleware, mais je pense que Store.dispatch aurait besoin de renvoyer la liste SideEffects avec une action.

@vladar

Oui, mais Flux ne vous interdit pas d'avoir des trucs asynchrones dans les magasins tant que vous distribuez () directement dans les rappels asynchrones par rapport à l'état de mutation. Ils sont en quelque sorte sans opinion, même si la plupart des implémentations recommandent d'utiliser des AC pour async. Personnellement, je pense que c'est une réminiscence de MVC, car il est pratique de traiter mentalement les AC comme des contrôleurs plutôt que de passer mentalement aux FSM.

Je suis d'accord que traiter ActionCreators comme des contrôleurs n'est pas un bon état d'esprit. C'est la raison pour laquelle je finis par penser que pour une logique complexe, je dois renvoyer le contrôle du réducteur au courant alternatif. Je ne devrais pas avoir besoin de ça. Si AC gère asynchrone, ils devraient au plus ressembler à des gestionnaires de requêtes - s'exécuter en fonction d'une décision prise ailleurs et cette requête ne doit pas croiser les domaines.

Pour éviter leur rôle de contrôleurs, la seule option consiste désormais à acheminer la logique via des composants intelligents et à utiliser ActionCreators uniquement pour exécuter l'étape initiale d'une logique dans une réponse directe à l'interface utilisateur. La première décision réelle est prise dans un composant intelligent basé sur Redux.state et / ou l'état local du composant.

Cette approche dans votre exemple précédent de suppression de catégorie et d'actualisation / suppression de quelque chose d'autre en réponse à l'aide de webapi ne serait pas bien. Je suppose que cela conduira à un mélange de différentes techniques basées sur les préférences de développement - devrait-il être en AC, en réducteur, en composant intelligent? Voyons comment les modèles évolueront.

J'achetais le point pour garder les appels asynchrones loin des magasins. Mais je ne pense pas que ce soit valable car, à mon humble avis, cela augmente considérablement la complexité ailleurs. D'un autre côté, je ne vois pas beaucoup de complexité d'avoir une fonction asynchrone dans un magasin / réducteur dans la mesure où elle ne s'exécute et expédie que là-bas - ou une approche pure et gère ces fonctions comme des données et les déclenche quelque part loin.

Les acteurs font de même, ils peuvent exécuter async et envoyer un message (objet d'action) à un autre acteur, ou l'envoyer à vous-même pour continuer dans leur FSM.

Je termine parce qu'il ne semble pas y avoir quoi que ce soit d'action ici. Le document Async Actions devrait répondre à la plupart des questions. D'autres discussions théoriques ne me semblent pas utiles sans le code réel que nous pourrions exécuter. ;-)

@gaearon pouvez-vous ajouter du code réel mettant en œuvre la bonne manière de gérer les appels d'API à l'un des exemples avec redux (en dehors de l'exemple "réel" qui utilise un middleware)?

J'ai encore des questions sur le placement réel après avoir lu tous les documents et ce problème à quelques reprises. J'ai du mal à comprendre quels fichiers placer ces effets secondaires https://github.com/rackt/redux/issues/291#issuecomment -123010379 . Je pense que c'est l'exemple «correct» de ce fil de discussion.

Merci d'avance pour toute clarification dans ce domaine.

Je voudrais souligner une réflexion importante sur les créateurs d'action impurs:

Mettre des appels asynchrones dans les créateurs d'action, ou avoir des créateurs d'action impurs en général, a un gros inconvénient pour moi: cela fragmente la logique de contrôle de telle manière que le remplacement complet de la logique de contrôle de votre application n'est pas aussi simple que de simplement échanger votre boutique. créateur. J'ai l'intention de toujours utiliser un middleware pour les processus asynchrones et d'éviter complètement les créateurs d'actions impurs.

L'application que je développe aura au moins deux versions: une qui s'exécute dans un Raspberry Pi sur site et une autre qui exécute un portail. Il peut même exister des versions distinctes pour un portail / portail public géré par un client. Je ne sais pas si je dois utiliser différentes API, mais je veux me préparer à cette possibilité du mieux que je peux, et le middleware me permet de le faire.

Et pour moi, le concept de distribution des appels d'API entre les créateurs d'action va complètement à l'encontre du concept Redux de contrôle centralisé des mises à jour d'état. Bien sûr, le simple fait qu'une demande d'API s'exécute en arrière-plan ne fait pas partie de l'état du magasin Redux - mais c'est toujours l'état de l'application, quelque chose sur lequel vous voulez avoir un contrôle précis. Et je trouve que je peux avoir le contrôle le plus précis dessus quand, comme avec les magasins / réducteurs Redux, ce contrôle est centralisé plutôt que distribué.

@ jedwards1211 Vous pourriez être intéressé par https://github.com/redux-effects/redux-effects au cas où vous ne les auriez pas encore vérifiés, ainsi que par la discussion au # 569.

@gaearon cool, merci de l'avoir signalé! Dans tous les cas, utiliser mon propre middleware n'a pas été trop difficile jusqu'à présent :)

J'ai créé une approche semblable à Elm: redux-side-effect . Le README explique l'approche et la compare à des alternatives.

@gregwebs J'aime redux-side-effect , même si ce serait gênant avec une logique de contrôle échangeable aussi, car il y a la question de savoir comment j'obtiens la fonction sideEffect dans mes modules de

Je pourrais cependant utiliser un hack amusant: il suffit de stocker sideEffect dans le state lui-même, donc il est automatiquement disponible pour le réducteur! : stuck_out_tongue_winking_eye:

Quelque chose comme https://github.com/rackt/redux/pull/569/ est proche de l'idéal pour la façon dont je veux travailler, même si bien sûr je ne veux pas l'utiliser dans un projet de production à moins qu'il ne devienne une pièce standard de l'API.

Voici une idée: demandez à un middleware de coller une fonction sideEffect sur l'action. Légèrement piraté, mais le plus petit changement possible nécessaire pour rendre les réducteurs capables d'initier du code asynchrone:

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;
};

Il existe plusieurs façons d'adopter l'approche sideEffect simple. Veuillez essayer vos variantes et faire un rapport

J'ai refactoré mon code pour utiliser l'approche sideEffectMiddleware ci-dessus, et j'aime vraiment l'organisation du code qui en résulte. C'est bien de ne pas avoir à importer la fonction sideEffect de n'importe où dans mes réducteurs, elle vient juste dans le action .

@ jedwards1211 J'ai publié votre code en tant que actionSideEffectMiddleware dans la version 2.1.0 du paquet.

@gregwebs cool! Ça vous dérange de me lister comme collaborateur?

@ jedwards1211 vous êtes ajouté. Peut-être que vous ajouterez le support de replay approprié :) Je ne peux pas encore profiter de cela là où je l'utilise.

@gaearon Je

Ils le font, c'est juste qu'ils sont déjà des objets simples au moment où ils sont enregistrés, donc le middleware n'intervient généralement pas.

@gaearon Hmmm. Donc, je n'ai pas vraiment essayé cette fonctionnalité, mais ... je suppose que certains middlewares à effets secondaires battraient également le record / la relecture? Prenez redux-effects-fetch par exemple, il effectuerait toujours une requête ajax sur une action FETCH d'objet 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 l'enregistreur nettoie les rappels [success, failure] des steps de l'action FETCH, alors le middleware redux-effects ne distribuerait aucune action de modification d'état, mais on ne le voudrait pas rejouer pour déclencher un tas de requêtes ajax.

Existe-t-il un moyen de séparer le middleware "pur" (qui ne distribue jamais d'actions supplémentaires) comme redux-logger du middleware "impur" (qui peut envoyer des actions)?

Malheureusement, je n'ai pas examiné redux-effects près

Pas de problème, dans tous les cas l'enregistreur ajoute-t-il des indicateurs aux actions que le middleware peut utiliser pour décider d'effectuer ou non des effets secondaires? J'essaie juste de trouver un moyen dans mon propre middleware de pouvoir prendre en charge correctement les outils de développement

Ça ne l'est pas. On s'attend à ce que devTools() soit placé _after_ applyMiddleware dans la chaîne de composition de l'enrichisseur, de sorte qu'au moment où l'action est atteinte, il s'agit d'une action «finale» qui n'a pas besoin d'une interprétation supplémentaire.

Oh, je vois, donc après qu'un middleware a effectué un effet secondaire, il pourrait simplement supprimer tous les champs de l'action / l'ajuster afin qu'il ne déclenche pas l'effet secondaire lorsqu'il repasse par le middleware lors de la relecture, puis il devrait travailler avec les outils de développement, non?

Ouais, cela semble être un bon moyen.

Super, merci de m'éclairer!

@gaearon @vladar @ jedwards1211 Vous pourriez être intéressé par https://github.com/salsita/redux-side-effects. Fondamentalement, c'est une implémentation d'elmish (state, action) => (state, sideEffects) mais au lieu d'un reducer retournant un tuple (ce que vous avez déjà remarqué n'est pas possible), il produit des effets.
J'ai joué avec peu de temps, le rechargement à chaud et la relecture semblent être ok. Je ne vois aucune fonctionnalité de redux cassée, jusqu'à présent, mais peut-être que certains des reduxers seniors pourraient trouver quelque chose. :)
Pour moi, cela semble un ajout vraiment important à la pile redux car il permet à la logique d'être principalement dans les réducteurs, ce qui en fait des machines à états efficaces et testables, qui délèguent des effets (actions de transition dont le résultat ne dépend pas seulement de l'état actuel) à des services externes ( à peu près comme l'architecture, les effets et les services Elm).

Une autre approche est redux-saga qui utilise également des générateurs mais maintient les effets secondaires séparés des réducteurs.

@gaearon Je crois que @minedeljkovic voulait dire le

Pour moi, cela semble un ajout vraiment important à la pile redux car il permet à la logique d'être principalement dans les réducteurs, ce qui en fait des machines à états efficaces et testables, qui délèguent des effets (actions de transition dont le résultat ne dépend pas seulement de l'état actuel) à des services externes ( à peu près comme l'architecture, les effets et les services Elm).

comme principal avantage par rapport à l'approche traditionnelle, car https://github.com/yelouafi/redux-saga/ est très similaire au fonctionnement de redux-thunk , au lieu d'un sucre de syntaxe qui facilite les transactions de longue durée .

https://github.com/salsita/redux-side-effects , d'autre part, ressemble plus à l'architecture Elm.

Oui, redux-saga et redux-side-effects n'utilisent des générateurs que pour déclarer les effets secondaires, gardant les sagas et les réducteurs purs, respectivement. C'est pareil.

Deux raisons pour lesquelles je préfère cela dans les réducteurs:

  1. Vous avez un accès explicite à l'état actuel qui peut affecter la façon dont l'effet doit être déclaré (je pense que c'était l'un des points de @vladar au cours de cette discussion)
  2. Il n'y a pas de nouveau concept introduit (Saga dans redux-saga)

Mon commentaire dans https://github.com/rackt/redux/issues/1139#issuecomment -165419770 est toujours valide car redux-saga ne résoudra pas cela et il n'y a pas d'autre moyen de résoudre cela que d'accepter le modèle utilisé dans l'architecture Elm.

J'ai mis un élément essentiel qui essaie de souligner mes points sur les effets secondaires de redux: https://gist.github.com/minedeljkovic/7347c0b528110889aa50

J'ai essayé de définir le scénario le plus simple possible qui, à mon avis, est juste assez "dans le monde réel", tout en soulignant certains des points de cette discussion.

Le scénario est:
L'application a une partie "Enregistrement de l'utilisateur" où les données personnelles de l'utilisateur doivent être saisies. Parmi les autres données personnelles, le pays de naissance est sélectionné dans une liste de pays. La sélection est effectuée dans la boîte de dialogue "Sélection du pays", où l'utilisateur sélectionne un pays et a le choix de confirmer ou d'annuler une sélection. Si l'utilisateur tente de confirmer la sélection, mais qu'aucun pays n'est sélectionné, il doit être averti.

Contraintes d'architecture:

  1. La fonctionnalité CountrySelection doit être modulaire (dans l'esprit des canards redux ), afin de pouvoir être réutilisée dans plusieurs parties de l'application (par exemple également dans la partie "Administration des produits" de l'application, où le pays de production doit être indiqué)
  2. La tranche d'état de CountrySelection ne doit pas être considérée comme globale (être à la racine de l'état redux), mais elle doit être locale au module (et contrôlée par ce module) qui l'invoque.
  3. La logique de base de cette fonctionnalité doit inclure le moins de pièces mobiles possible (dans mon sens, cette logique de base est implémentée uniquement dans les réducteurs (et uniquement la partie la plus importante de la logique). être :) . Leur seule responsabilité serait de rendre l'état actuel et d'envoyer des actions.)

La partie la plus importante de l'essentiel, concernant cette conversation, est la façon dont le réducteur countrySelection gère l'action CONFIRM_SELECTION.

@gaearon , j'apprécierais surtout votre opinion sur ma déclaration selon laquelle les effets secondaires de redux en plus du redux standard fournissent une surface pour la solution la plus simple compte tenu des constructions.

Une idée alternative possible pour la mise en œuvre de ce scénario (sans utiliser redux-side-effects, mais en utilisant redux-saga, redux-thunk ou une autre méthode), serait également très appréciée.

@ tomkis1 , j'aimerais avoir ici votre opinion, est-ce que j'utilise ou abuse de votre bibliothèque ici. :)

(Il y a une implémentation légèrement différente dans ce gist https://gist.github.com/minedeljkovic/9d4495c7ac5203def688, où globalActions est évité. Ce n'est pas important pour ce sujet, mais peut-être que quelqu'un aimerait commenter ce modèle)

@minedeljkovic Merci pour l'essentiel. Je vois un problème conceptuel avec votre exemple. redux-side-effects est destiné à être utilisé uniquement pour les effets secondaires. Dans votre exemple, cependant, il n'y a pas d'effets secondaires mais plutôt une transaction commerciale de longue durée et donc redux-saga est beaucoup plus approprié. @slorber et @yelouafi peuvent apporter plus de lumière à ce sujet.

En d'autres termes, le problème le plus préoccupant pour moi est synchrone dispatching de la nouvelle action dans le réducteur (https://github.com/salsita/redux-side-effects/pull/9#issuecomment-165475991):

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

Je crois que @slorber est venu avec le terme «effet secondaire commercial» et c'est exactement votre cas. redux-saga brille dans la résolution de ce problème particulier.

@mindjuice Je ne suis pas tout à fait sûr de comprendre votre exemple mais j'aime l'exemple d'intégration que j'ai donné ici: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Le modèle de saga permet également de rendre explicites certaines choses implicites.
Par exemple, vous décrivez simplement ce qui s'est passé en déclenchant des actions (je préfère les événements car pour moi, ils devraient être au passé), puis certaines règles métier entrent en vigueur et mettent à jour l'interface utilisateur. Dans votre cas, l'affichage de l'erreur est implicite. Avec une saga, sur confirmation, si aucun pays n'est sélectionné, vous enverriez probablement une action "NO_COUNTRY_SELECTED_ERROR_DISPLAYED" ou quelque chose comme ça. Je préfère que ce soit totalement explicite.

Vous pouvez également voir la Saga comme un point de couplage entre les canards.
Par exemple, vous avez duck1 et duck2, avec des actions locales sur chacun. Si vous n'aimez pas l'idée de coupler les 2 canards (je veux dire qu'un canard utiliserait les actionsCreators du deuxième canard), vous pouvez simplement les laisser décrire ce qui s'est passé, puis créer une saga qui relie des règles commerciales complexes entre les 2 canards.

Sooooo, c'est un fil incroyablement long et y a-t-il encore une solution au problème?

Supposons que vous ayez un action() asynchrone et que votre code doit indiquer une erreur ou afficher le résultat.

La première approche était de faire comme

// 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')

Mais il s'avère que ce n'est pas de la manière Redux car maintenant le réducteur contient des "effets secondaires" (je ne sais pas ce que cela veut dire).
Pour faire court, la bonne façon est de déplacer ces alert() s hors du réducteur quelque part.

Cela pourrait être quelque part le composant React qui appelle cela action .
Alors maintenant, mon code ressemble à:

// 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
    })
}

Est-ce maintenant la bonne façon de procéder?
Ou ai-je besoin d'enquêter davantage et d'appliquer une bibliothèque tierce?

Redux n'est pas du tout simple. Ce n'est pas facile à comprendre dans le cas d'applications du monde réel. Mabye, un exemple de dépôt d'applications dans le monde réel est nécessaire comme exemple canonique illustrant la bonne manière.

@ halt-hammerzeit Redux lui-même c'est très simple; la fragmentation déroutante provient de différents besoins ou opinions sur l'intégration / la séparation des effets secondaires des réducteurs lors de l'utilisation de Redux.

En outre, la fragmentation vient du fait qu'il n'est vraiment pas si difficile de faire des effets secondaires comme vous le souhaitez avec Redux. Il ne m'a fallu qu'environ 10 lignes de code pour rouler mon propre middleware d'effets secondaires de base. Alors embrassez votre liberté et ne vous inquiétez pas de la «bonne» voie.

@ jedwards1211 "Redux lui-même" n'a aucune valeur ou signification et n'a aucun sens car son objectif principal est de résoudre les problèmes quotidiens des développeurs Flux. Cela implique AJAX, de belles animations et tous les autres trucs ordinaires et attendus

@ halt-hammerzeit vous avez raison. Redux semble certainement destiné à amener la plupart des gens à s'entendre sur la façon de gérer les mises à jour d'état, il est donc dommage que la plupart des gens ne s'entendent pas sur la façon d'effectuer les effets secondaires.

Avez-vous vu le dossier «exemples» dans le dépôt?

Bien qu'il existe plusieurs façons d'effectuer des effets secondaires, il est généralement recommandé de le faire en dehors de Redux ou à l'intérieur des créateurs d'action, comme nous le faisons dans chaque exemple.

Oui, je sais ... tout ce que je dis, c'est que ce fil a illustré un manque de consensus (au moins parmi les gens ici) sur la meilleure façon d'effectuer des effets secondaires.

Bien que la plupart des utilisateurs utilisent peut-être des créateurs d'action thunk et que nous ne sommes que des valeurs aberrantes.

^ ce qu'il a dit.

Je préférerais que tous les plus grands esprits se mettent d'accord sur une seule solution juste
et le graver dans la pierre pour ne pas avoir à lire un 9000 écrans
fil

Le jeudi 7 janvier 2016, Andy Edwards [email protected] a écrit:

Oui, je sais ... tout ce que je dis, c'est que ce fil a illustré un manque de
consensus (au moins parmi les personnes ici) sur la meilleure façon de jouer
effets.

-
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169751432.

Donc, je suppose, la solution actuellement convenue est de tout faire en action
créateurs. Je vais essayer d'utiliser cette approche et au cas où je trouverais des défauts dans
je le posterai ici

Le jeudi 7 janvier 2016, Николай Кучумов [email protected] a écrit:

^ ce qu'il a dit.

Je préférerais que tous les plus grands esprits se mettent d'accord sur une seule solution juste
et le graver dans la pierre pour ne pas avoir à lire un 9000 écrans
fil

Le jeudi 7 janvier 2016, Andy Edwards < [email protected]
<_e i = "16">

Oui, je sais ... tout ce que je dis, c'est que ce fil a illustré un manque de
consensus (au moins parmi les personnes ici) sur la meilleure façon de jouer
effets.

-
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169751432.

Ceci et des threads similaires existent parce que 1% des utilisateurs de Redux aiment rechercher des solutions plus puissantes / pures / déclaratives pour les effets secondaires. Veuillez ne pas avoir l'impression que personne ne peut être d'accord là-dessus. La majorité silencieuse utilise des réflexions et des promesses, et est généralement satisfaite de cette approche. Mais comme toute technologie, elle présente des inconvénients et des compromis. Si vous avez une logique asynchrone complexe, vous voudrez peut-être supprimer Redux et explorer Rx à la place. Le motif Redux est facilement expressif dans Rx.

OK merci

Le jeudi 7 janvier 2016, Dan Abramov [email protected] a écrit:

Ceci et des threads similaires existent car 1% des utilisateurs de Redux aiment rechercher
des solutions plus puissantes / pures / déclaratives pour les effets secondaires. Veuillez ne pas
prenez l'impression que personne ne peut s'entendre là-dessus. La majorité silencieuse utilise
pense et promet, et sont surtout satisfaits de cette approche. Mais comme tout
technologie, il a des inconvénients et des compromis. Si vous avez une logique asynchrone complexe
vous voudrez peut-être abandonner Redux et explorer Rx à la place. Le motif Redux est
facilement expressif dans Rx.

-
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169761410.

@gaearon ouais, tu as raison. Et j'apprécie que Redux soit suffisamment flexible pour s'adapter à toutes nos différentes approches!

@ halt-hammerzeit jetez un œil à ma réponse ici où j'explique pourquoi redux-saga peut être meilleur que redux-thunk: http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for- async-flow-in-redux / 34599594

@gaearon Au fait, votre réponse sur stackoverflow a le même défaut que la documentation - elle ne couvre que le cas le plus simple
http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594#34599594
Les applications du monde réel vont rapidement au-delà de la simple récupération de données via AJAX: elles doivent également gérer les erreurs et effectuer certaines actions en fonction de la sortie de l'appel à l'action.
Votre conseil est de juste dispatch un autre événement et c'est tout.
Alors que faire si mon code veut alert() un utilisateur à la fin de l'action?
C'est là que ces thunk n'aideront pas, mais les promesses le seront.
J'écris actuellement mon code en utilisant de simples promesses et cela fonctionne de cette façon.

J'ai lu le commentaire ci-contre sur redux-saga .
Je n'ai pas compris ce qu'il fait, lol.
Je ne suis pas tellement dans la chose monads et autres, et je ne sais toujours pas ce qu'est un thunk , et je n'aime pas ce mot bizarre pour commencer.

Ok, alors je continue à utiliser Promises dans les composants React.

Nous suggérons de renvoyer les promesses des thunks tout au long de la documentation.

@gaearon Ouais, c'est ce dont je parle: on_click() { dispatch(load_stuff()).then(show_modal('done')) }
Cela fera l'affaire.

Je pense qu'il vous manque encore quelque chose.
Veuillez consulter le fichier README de redux-thunk.
Il montre comment enchaîner les créateurs d'action thunk dans la section "Composition".

@gaearon Ouais, c'est exactement ce que je fais:

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

C'est exactement ce que j'ai écrit ci-dessus:

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

Cette section README est bien écrite, merci

Non, ce n'était pas mon propos. Jetez un œil à makeSandwichesForEverybody . C'est un créateur d'action thunk qui appelle d'autres créateurs d'action thunk. C'est pourquoi vous n'avez pas besoin de tout mettre en composants. Voir aussi l'exemple async dans ce dépôt pour en savoir plus.

@gaearon Mais je pense qu'il ne serait pas approprié de mettre, disons, mon code d'animation sophistiqué dans le créateur d'action, n'est-ce pas?
Prenons cet exemple:

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')
        })
}

Comment le réécririez-vous de la bonne manière?

@ halt-hammerzeit vous pouvez faire en sorte que actionCreator renvoie les promesses afin que le composant puisse afficher un spinner ou autre en utilisant l'état du composant local (difficile à éviter lors de l'utilisation de jquery de toute façon)

Sinon, vous pouvez gérer des minuteries complexes pour piloter des animations avec redux-saga.

Jetez un œil à ce billet de blog: http://jaysoo.ca/2016/01/03/managing-processes-in-redux-using-sagas/

Oh, dans ce cas, c'est bien de le garder en composant.
(Remarque: généralement dans React, il est préférable d'utiliser un modèle déclaratif pour tout plutôt que d'afficher les modaux impérativement avec jQuery. Mais ce n'est pas grave.)

@slorber Oui, je retourne déjà des promesses et des trucs de mes "thunks" (ou peu importe comment vous les appelez; heu, je n'aime pas ce mot bizarre), donc je peux gérer les fileurs et autres.

@gaearon Ok, je t'ai compris. Dans le monde des machines idéales, il serait bien sûr possible de rester à l'intérieur du modèle de programmation déclarative, mais la réalité a ses propres exigences, irrationnelles, par exemple, du point de vue d'une machine. Les gens sont des êtres irrationnels qui n'opèrent pas seulement avec des zéros et des uns purs et cela nécessite de compromettre la beauté et la pureté du code pour pouvoir faire des choses irrationnelles.

Je suis satisfait du support dans ce fil. Mes doutes semblent maintenant résolus.

Nouvelle discussion pertinente: https://github.com/reactjs/redux/issues/1528

@gaearon explication très impressionnante! : trophée: Merci: +1:

Cette page vous a été utile?
0 / 5 - 0 notes