Redux: Bonnes pratiques de gestion du flux de données pour les pages de connexion/d'inscription avec redirection

Créé le 22 juil. 2015  ·  66Commentaires  ·  Source: reduxjs/redux

EDIT : ce que j'essaie de comprendre ici, c'est comment gérer le flux de données lorsque vous avez une page avec un formulaire. Après avoir soumis le formulaire, soit vous redirigez vers une autre page , soit vous affichez un message d'erreur .

Le défi est de savoir où ces informations doivent être _stockées temporairement_ dans le flux de données. Parce qu'après avoir redirigé ou rendu le message, l'état (ou les informations que vous avez stockées) n'est plus pertinent et doit être réinitialisé ou nettoyé.


Dans une application Web backend, vous avez généralement des pages telles que la connexion, l'inscription, la réinitialisation du mot de passe, etc.

Lorsque l'utilisateur accède à l'application et n'est pas connecté, il est redirigé vers la page de connexion. Cela peut être facilement fait avec le crochet RR onEnter .

Ensuite, l'utilisateur remplira le formulaire et le soumettra. À ce stade, généralement, si la demande a réussi, vous souhaitez rediriger vers la racine de l'application ou vers une autre page "sécurisée". En cas d'échec, vous souhaitez rester sur la page et afficher un message d'erreur.

(Cela s'applique non seulement à la page de connexion, mais aussi aux autres pages ainsi qu'aux mentions précédentes. Je me concentrerai uniquement sur la connexion pour l'instant)

Jusqu'à présent, vous avez ce flux (ce qui est un cas d'utilisation courant):

  • l'utilisateur va à /
  • rediriger vers /login
  • remplir le formulaire et soumettre
  • si ok rediriger vers / (ou une autre page)
  • en cas d'échec, restez sur le formulaire de connexion et affichez le message d'erreur

Si nous implémentons ce flux avec redux, je suppose que j'ai la configuration suivante :

// actions
function login (form) {
  return dispatch => doLogin(form)
    .then(() => dispatch({ type: 'LOGGED_IN' }))
    .catch(() => dispatch({ type: 'LOGIN_FAILED' }))
}

// reducer
const initialState = {
  // some fields...,
  loggedIn: false,
  shouldRedirect: false,
  errorMessage: null
}
function application (state = initialState, action) {
  switch action.type {
    case 'LOGGED_IN':
      return Object.assign({}, state, { loggedIn: true, shouldRedirect: true })
    case 'LOGIN_FAILED':
      return Object.assign({}, state, { loggedIn: false, shouldRedirect: false, errorMessage: action.error.message })
  }
  return state
}

// component
@connect(state => { application: state.application })
class Login extends React.Component {
  componentWillUpdate () {
    const { router } = this.context
    const { application } = this.props
    if (application.shouldRedirect)
      router.transition(...)  
  }

  onSubmit () {
    const actions = bindActionCreators(applicationActions, dispatch)
    actions.login({...})
  }

  render () {
    const { errorMessage } = this.props
    return (
      <div>
        {errorMessage ? <p>{errorMessage}</p> : null}
        <Form {...props} onSubmit={onSubmit}>
          {...}
        </Form>
      </div>
    )
  }
}

Étant donné que ce flux est correct - j'espère :) - vous pouvez voir que j'ai 2 drapeaux dans l'état de l'application : shouldRedirect et errorMessage .
Ces drapeaux ne sont utilisés que lors de la soumission du formulaire.

Première question : est-ce correct d'avoir ces drapeaux dans l'état de l'application ?

Une autre chose à considérer est que je dois "réinitialiser" ces drapeaux lorsque je redirige car si je veux aller sur une autre page (par exemple, inscription, ...) je devrais avoir un état propre ( { shouldRedirect: false, errorMessage: null } ).

Donc, je pourrais vouloir envoyer une autre action comme

// actions
function resetSubmitState () {
  return { type: 'RESET_SUBMIT' }
}

// component
componentWillUpdate () {
  const { store, router } = this.context
  const { application } = this.props
  if (application.shouldRedirect) {
    store.dispatch(applicationActions.resetSubmitState())
    router.transition(...)  
  }
}

Quelqu'un a-t-il eu ce problème ? Ai-je raté quelque chose ou est-ce la "bonne" façon de le faire?

Y a-t-il d'autres alternatives?

Tout commentaire est le bienvenu ! :)

discussion docs

Commentaire le plus utile

Je pense que la question est un peu plus profonde que la simple généralisation des «soumettre des formulaires» - il s'agit davantage de savoir quel est réellement l'état et quelles données doivent apparaître dans store et lesquelles ne le sont pas.

J'aime penser à View une partie du flux de données de flux comme une fonction pure render(state): DOM mais ce n'est pas totalement vrai.
Prenons comme exemple un composant de formulaire simple qui affiche deux champs de saisie :

class Form extends React.Component {

  onFieldChanged (event) {
    this.setState({[event.target.name]: event.target.value})
  }

  render () {
    return (
      <form onChange={::this.onFieldChanged}>
        <input type="text" name="name" />
        <input type="text" name="surname" />
        <input type="email" name="email" />
      </form>
    )
  }
}

Lors de la saisie dans ces champs, certaines modifications d'état internes ont été effectuées, nous aurons donc quelque chose comme ceci à la fin :

{
  name: 'John'
  surname: 'Snow'
}

Nous avons donc un état qui n'est pas lié à l'état de l'application et personne ne s'en soucie. Lorsque nous reviendrons sur ce formulaire la prochaine fois, il sera affiché effacé et le « John Snow » aura disparu pour toujours.
On dirait que nous avons modifié DOM sans toucher au flux de données de flux.

Supposons qu'il s'agit d'un formulaire d'abonnement à une lettre de notification. Maintenant, nous devons interagir avec le monde extérieur et nous le ferons en utilisant notre action spéciale.

import { subscribe } from 'actions'

class Form extends React.Component {

  onFieldChanged (event) {
    this.setState({[event.target.name]: event.target.value})
  }

  onSubmit (event) {
    event.preventDefault()
    const { name, surname, email } = this.state
    subscribe(name, surname, email)
  }

  render () {
    return (
      <form onChange={::this.onFieldChanged} onSubmit={::this.onSubmit}>
        <input type="text" name="name" />
        <input type="text" name="surname" />
        <input type="email" name="email" />
        <button type="submit">Subscribe</button>
      </form>
    )
  }
}

Lorsque l'utilisateur appuie sur le bouton d'envoi, il s'attend à une réponse de l'interface utilisateur en cas de succès ou non et que faire ensuite. Nous voulons donc suivre l'état de la demande pour afficher l'écran de chargement, rediriger ou afficher un message d'erreur.
La première idée est d'envoyer des actions internes comme loading , submitted , failed pour rendre l'état de l'application responsable de cela. Parce que nous voulons rendre l'état et ne pensons pas aux détails.

  render () {
    //provided by connector of whatever
    const { props: { isLoading, error } } = this
    return (
      <form onChange={::this.onFieldChanged} onSubmit={::this.onSubmit}
        disabled={isLoading}>
        <input type="text" name="name" />
        <input type="text" name="surname" />
        <input type="email" name="email" />
        <button type="submit">Subscribe</button>
        {error ? <p>{ error }</p> : null}
      </form>
    )
  }

Mais ici, le diable apparaît : cet état de page est maintenant persistant et le prochain appel render(state) affichera le formulaire dans un état sale. Nous devons donc créer une action cleanup pour l'appeler chaque fois que nous montons le formulaire.
Et pour chaque formulaire que nous avons, nous ajouterons le même passe-partout de loading , submitted , failed et cleanup actions avec magasins/réducteurs… jusqu'à ce que nous nous arrêtions et réfléchissions sur l'état que nous voulons réellement rendre.

submitted appartient-il à l'état de l'application ou s'agit-il simplement de l'état du composant, comme la valeur du champ name . L'utilisateur se soucie-t-il vraiment que cet état soit conservé dans un cache ou souhaite-t-il voir la forme propre lorsqu'il le revisite ? La réponse : ça dépend.

Mais dans le cas de formulaires de soumission purs, nous pourrions supposer que l'état du formulaire n'a rien à voir avec l'état de l'application : nous voulons savoir si l'utilisateur est logged in ou non en cas de page de connexion, ou nous ne voulons pas pour savoir quoi que ce soit, a-t-il changé de mot de passe ou échoué: si la demande réussit, redirigez-le simplement ailleurs.

Alors, qu'en est-il de la proposition que nous n'avons pas besoin de cet état ailleurs que dans Component ? Essayons.
Pour fournir une implémentation plus sophistiquée, nous pourrions supposer qu'en 2015, nous vivons dans le monde asynchrone et que nous avons Promises partout, alors laissez notre action renvoyer un Promise :

function subscribe (name, surname, email) {
  return api.request('/subscribtions', 'create', { name, surname, email })
}

À l'étape suivante, nous pourrions commencer à suivre l'action dans notre Component :

onSubmit (event) {
  event.preventDefault()
  const { name, surname, email } = this.state
  subscribe(name, surname, email)
    .then(() => { this.setState({ submitted: true }) })
    .catch(error => { this.setState({ error }) })
}

componentWillUpdate (object nextProps, object nextState) {
  if(nextState.submitted)
    redirect('somewhere')
}

On dirait que nous avons tout ce dont nous avons besoin pour rendre le composant sans polluer l'état de l'application et que nous n'avons plus besoin de le nettoyer.
Nous y sommes donc presque. Nous pourrions maintenant commencer à généraliser le flux Component .
Tout d'abord, séparons-nous des préoccupations : garder une trace des réactions d'action et de mise à jour de l'état :
(Faisons-le en termes de redux pour rester dans les limites du repo)

import React, { PropTypes } from 'react'
import { bindActionCreators } from 'redux'

// Keeps track of action
export default function connectSubmitForm (Form, submitAction) {
  return React.createClass({
    contextTypes: {
      //redux Store
      store: PropTypes.object.isRequired
    },

    getInitialState () {
      return {}
    },

    onSubmit (...args) {
      const { context: { store: { dispatch } } } = this
      const { submitAction: submit }
        = bindActionCreators({ submitAction }, dispatch)
      submit(...args)
        .then(() => this.setState({ submitted: true }))
        .catch(error => this.setState({ error }))
    },

    render () {
      const {
        onSubmit,
        props,
        state: { submitted, error }
      } = this
      return (<Form {...props} onSubmit={onSubmit} submitted={submitted}
        error={error} />)
    }
  })
}

et

// redirect to path if predicate returns true
export default function redirect (path, predicate) {
  return Component =>
    class Composed extends React.Component {

      componentWillMount () {
        if (predicate(props))
          redirectTo(path)
      }

      componentWillReceiveProps (nextProps) {
        if (predicate(nextProps))
          redirectTo(path)
      }

      render () {
        return <Component {...this.props} />
      }
    }
}

//redirect to path if submitted
export default function redirectSubmitted (path) {
  return redirect(path, ({ submitted }) => submitted)
}

Maintenant, d'une part, nous avons decorator qui fournit des props submitted et error avec rappel onSubmit et d'autre part decorator qui redirige quelque part si obtient la propriété submitted === true . Attachons-les à notre formulaire et voyons ce que nous obtenons.

@redirectSubmitted('/')
class Form extends React.Component {

  onFieldChanged (event) {
    this.setState({[event.target.name]: event.target.value})
  }

  onSubmit (event) {
    event.preventDefault()
    const { name, surname, email } = this.state
    this.props.onSubmit(name, surname, email)
  }

  render () {
    return (
      <form onChange={::this.onFieldChanged} onSubmit={::this.onSubmit}>
        <input type="text" name="name" />
        <input type="text" name="surname" />
        <input type="email" name="email" />
        <button type="submit">Subscribe</button>
        {this.props.error ? <p>{ this.props.error }</p>: null}
      </form>
    )
  }
}

export default submitForm(Form, subscribe)

Et le voici : forme de rendu pure sans dépendances de la bibliothèque $# routing 8$#$, implémentation flux , application state , faisant ce pour quoi elle a été créée : soumettre et rediriger.

Merci d'avoir lu et désolé d'avoir pris votre temps.

Tous les 66 commentaires

Au lieu d'avoir shouldRedirect là-dedans, que diriez-vous d'avoir un réducteur de session, qui ne fait que suivre isLoggedIn ? De cette façon, vous pouvez rediriger depuis votre page de connexion vers n'importe quelle autre page en cochant isLoggedIn au lieu de shouldRedirect .

De plus, la réinitialisation de l'état peut être effectuée en traitant simplement l'action LOGGED_IN dans votre réducteur. Si cette action se produit, vous savez que vous pouvez réinitialiser en toute sécurité l'état d'erreur à une null .

Est-ce que ça a du sens? :RÉ

Cela a du sens s'il n'y a que la page de connexion, mais c'est une approche un peu plus générique. Par exemple pour une page d'inscription ou une page de réinitialisation de mot de passe. Même si vous êtes connecté, vous devriez toujours pouvoir voir ces pages.

Et ces drapeaux devraient être utilisés par toutes ces pages, car elles ont toutes le même flux.

J'espère que c'est plus clair maintenant :)

Ouais, de mon point de vue "shouldRedirect" est tout simplement trop générique pour être associé à une certaine action. Cela peut ouvrir des bogues où vous oubliez d'une manière ou d'une autre de définir cet indicateur, puis une page redirige vers une autre page vers laquelle elle ne devrait pas rediriger.

Mais bien sûr, si vous voulez nettoyer votre état, vous pouvez simplement le faire avec une action. Ou vous pouvez écouter un changement d'historique en dehors de réagir, puis déclencher l'action d'effacement d'état. Cela peut être préférable car vous n'avez pas à disperser cette logique dans vos composants, mais vous pouvez la conserver à un seul endroit.

Mais ce n'est qu'une solution possible pour le dédouanement de l'État. Je ne sais pas encore comment résoudre la redirection de manière agréable.

La question est également : est-il logique d'avoir ces drapeaux dans l'État ? Si non, comment ce flux doit-il être mis en œuvre ?

@gaearon si vous avez un peu de temps, j'aimerais aussi entendre vos commentaires. Merci! :+1:

Dans mon application, je redirige dans le middleware et je n'utilise aucun drapeau. Mais je n'utilise pas React-router.

// action

/**
 * Creates login action
 *
 * <strong i="6">@param</strong> {string} email
 * <strong i="7">@param</strong> {string} password
 *
 * <strong i="8">@returns</strong> {{types: *[], promise: *}}
 */
export function login(email, password) {
    return {
        types: [LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE],
        promise: post('/login').send({ email: email, password: password }).promise()
    };
}

// auth middleware 
/**
 * Intercepts LOGIN action and redirects to login screen
 * Otherwise just sends action to next middleware
 *
 * <strong i="9">@returns</strong> {Function}
 */
function authMiddleware({getState, dispatch}) {
    return (next) => (action) => {
        if (typeof action === 'object' && action.hasOwnProperty('type')) {
            if (action.type === LOGIN_SUCCESS) {
                next(action); // send it to next so identity will be set

                // get current route
                const state = getState();
                let path = '/dashboard';

                if (typeof state['router'] === 'object' && typeof state['router']['route'] === 'object' && null !== state['router']['route']) {
                    if (state.router.route.name === 'login' && typeof state.router.route.query['to'] === 'string') {
                        path = state.router.route.query.to;
                    }
                }

                return next(actions.transitionTo(path));
            }
        }

        return next(action);
    };
}

Il n'est pas refactorisé, il est uniquement destiné à des fins de test de ma solution de routeur :).

Ceci est un copier-coller de mon application et de la façon dont je gère le chargement des données utilisateur et la connexion. Je vais essayer de commenter le code du mieux possible, et je serai sur reactiflux pour les questions.

const AppConnector = createConnector((props$, state$, dispatch$, context$) => {
  // true when the user is logged in - drawn from Store state
  const loggedIn$ = state$
    .map(s => s.apiKey !== null)
    .distinctUntilChanged()

  // this returns an observable of a path to redirect to
  // in my app, I have a url like `/login?nextPath=dashboard`, but if that
  // isn't set, just go home
  const redirectTo$ = routeState$
    .map(s => s.router.query.nextPath || '/home')

  const redirect$ = loggedIn$
    .withLatestFrom(
      context$,
      redirectTo$,
      (loggedIn, context, path) => () => context.router.transitionTo(loggedIn ? path : '/login') // when the loggedIn state changes, i.e. user has logged in or out, respond to that change
    )
    .do(go => go())

  // It's awesome to have this here, because it means that no matter 
  // where the user loads in to the app (all child views of this view), 
  // the app will realise it needs to load data, and loads the according data 
  const userData$ = state$
    .map(getUser)

  const userDataIsEmpty$ = userData$
    .map(user => user.id === null || typeof user.id === 'undefined')
    .filter(s => s === true)

  const loadUserData$ = Rx.Observable.combineLatest(loggedIn$, userDataIsEmpty$, (logged, userDataIsEmpty) => loggedIn && userDataIsEmpty)
    // only when user is logged in but has no user data
    .filter(s => s === true)
    // fetch data with an action creator
    .withLatestFrom(dispatch$, (userData, dispatch) => () => dispatch(UserActions.loadUserData()))
    .forEach(go => go())


  return Rx.Observable.combineLatest(props$, state$, context$, redirect$, (props, state, context) => ({...props, ...state, ...context}))
}, render)

En ce qui concerne la réponse à vos questions @emmenko , je pense qu'avoir shouldRedirect dans votre état n'est pas acceptable et devrait être fait dans une approche plus fonctionnelle. Avoir errorMessage dans l'état de l'application est bien imo, assurez-vous simplement qu'il est sous un état de connexion, tel que state.auth.errorMessage , plutôt qu'un errorMessage niveau supérieur

Je pense qu'avoir shouldRedirect dans votre état n'est pas acceptable, et devrait être fait dans une approche plus fonctionnelle

Je suis d'accord. Les choses qui ne sont utiles qu'une seule fois ne devraient pas être dans l'état IMO.

De plus, selon slack, je ne pense pas que vous devriez stocker isLoggedIn dans l'état de l'application, car il s'agit plutôt d'une propriété calculée, qui peut être calculée avec un simple sélecteur comme const isUserLoggedIn = state => state.user.id !== null

Voir https://github.com/faassen/reselect pour plus d'utilisation du sélecteur

@frederickfogerty merci pour l'exemple.

Question cependant: cela fonctionne bien pour la connexion car vous avez généralement un drapeau loggedIn dans le state . Mais qu'en est-il des autres pages telles que l'inscription, la réinitialisation du mot de passe, etc. ?
Ces pages se comportent de la même manière que la connexion : rediriger si ok, sinon afficher un message d'erreur.
Alors, comment gérez-vous ces cas? J'ai du mal à trouver le flux correct pour ces cas, et je ne sais pas où l'État devrait vivre.

Les choses qui ne sont utiles qu'une seule fois ne devraient pas être dans l'état IMO.

@gaearon Je suis d'accord, mais je ne sais toujours pas quelle est la bonne approche pour cela. Encore faut-il avoir un état quelque part...

C'est comme ça que je fais atm sans rien stocker en l'état.

// actions
function login () {
  return { type: 'LOGGED_IN' }  
}

function submitLogin (form, dispatch) {
  return api.doLogin(form)
  .then(() => {
    dispatch(login()) // can dispatch something at this point
    return Promise.resolve()
  })
}

// in the component (login, signup, ...)
onSubmit () {
  actions.submitLogin(form, dispatch) // this returns a promise
  .then(() => this.setState({ shouldRedirect: true }))
  .catch(error => this.setState({ shouldRedirect: false, errorMessage: error }))
}

Je ne pense pas que vous devriez stocker isLoggedIn dans l'état de l'application, car il s'agit plutôt d'une propriété calculée

Oui, c'est logique.

@gaearon Je suppose que la question pour ce flux de travail général (redirection ou erreur d'affichage) est de savoir si cela doit être fait avec "redux" ou d'une manière différente. Et quelle serait cette alternative ?

connexe et je l'épinglerai ici ... en fait, je faisais également face à ce problème et j'essayais actuellement de réimplémenter l'exemple suivant dans Redux. Rien d'affichable pour l'instant car je fais ça sur mon temps libre :
https://auth0.com/blog/2015/04/09/adding-authentication-to-your-react-flux-app/

C'est du pur Flux :
https://github.com/auth0/react-flux-jwt-authentication-sample

Peut-être que c'est utile pour les meilleures pratiques ? !

@emmenko

Alors, comment gérez-vous ces cas?

Tant que votre utilisation se connecte, alors isLoggedIn (qui n'est pas dans l'état de l'application, mais mineur) est toujours modifié, et cela les redirigera. Peu importe d'où vient ce changement d'état :)

La meilleure approche que j'ai prise est d'ajouter cette logique à un connecteur où la fonctionnalité est nécessaire pour ses enfants, par exemple pour l'autorisation, disons que vous avez un composant AppProtected , où tout ce qui est un enfant de ce composant a besoin d'autorisation , puis vous placez cette logique dans le connecteur du composant AppProtected.

C'est comme ça que je fais atm sans rien stocker en l'état.

Je n'aime vraiment pas cette approche, qui a un flux de données bidirectionnel entre l'interface utilisateur et les AC, ce qui va à l'encontre du flux. Si vous êtes tenu de le faire impérativement, vous pouvez faire quelque chose comme

componentWillReceiveProps(nextProps) {
  if (['/login', '/sign-up'].indexOf(this.props.router.path) !== -1 && this.props.isLoggedIn) {
    this.context.router.transitionTo(this.props.router.query.nextPath || '/home')
  }
}
  • Si la connexion de l'utilisateur réussit, il est redirigé hors de la page concernée.
  • Si la connexion de l'utilisateur échoue, il n'est pas connecté et reste sur la même page (rien ne se passe)

Cela fonctionne pour toutes les pages que vous avez mentionnées

Désolé mais je ne vois toujours pas comment je gère le rendu de redirection/erreur.

Prenons du recul et disons que j'ai la page reset-password . Nous supposons également que nous ne devrions toucher à aucun état en stockant une sorte de drapeaux à un coup (comme mentionné précédemment).
Lorsque je clique pour soumettre le formulaire, que doit-il se passer ?

C'est ce que j'aimerais savoir, comment doit être le flux et où doit-il vivre.

PS : merci pour vos commentaires jusqu'à présent !

[navigates to reset-password page] -> 
[types in email and hits submit] -> 
[onSubmit fires bound AC] ->
on successful reset this continues with:
[RESET_PASSWORD_SUCCESS] -> 
[resetPassword reducer returns state {passwordReset: true}] ->
[resetPasswordComponent.componentWillReceiveProps will check if state.passwordReset is true, and then will transition to its route]
on a failed reset this continues with:
[RESET_PASSWORD_FAILURE] -> 
[resetPassword reducer returns state {passwordResetError}] -> 
[resetPasswordComponent.componentWillReceiveProps() will check if state.passwordReset is true, and since is it not, nothing will happen - in render(), the error will be displayed]

C'est loin d'être aussi élégant que pour les pages de connexion et d'inscription, car il est très couplé

Je dois dormir, je répondrai à d'autres questions demain

Nous stockons donc à nouveau les drapeaux dans l'état ;)

PS : bonne nuit !

Je pense que la question est un peu plus profonde que la simple généralisation des «soumettre des formulaires» - il s'agit davantage de savoir quel est réellement l'état et quelles données doivent apparaître dans store et lesquelles ne le sont pas.

J'aime penser à View une partie du flux de données de flux comme une fonction pure render(state): DOM mais ce n'est pas totalement vrai.
Prenons comme exemple un composant de formulaire simple qui affiche deux champs de saisie :

class Form extends React.Component {

  onFieldChanged (event) {
    this.setState({[event.target.name]: event.target.value})
  }

  render () {
    return (
      <form onChange={::this.onFieldChanged}>
        <input type="text" name="name" />
        <input type="text" name="surname" />
        <input type="email" name="email" />
      </form>
    )
  }
}

Lors de la saisie dans ces champs, certaines modifications d'état internes ont été effectuées, nous aurons donc quelque chose comme ceci à la fin :

{
  name: 'John'
  surname: 'Snow'
}

Nous avons donc un état qui n'est pas lié à l'état de l'application et personne ne s'en soucie. Lorsque nous reviendrons sur ce formulaire la prochaine fois, il sera affiché effacé et le « John Snow » aura disparu pour toujours.
On dirait que nous avons modifié DOM sans toucher au flux de données de flux.

Supposons qu'il s'agit d'un formulaire d'abonnement à une lettre de notification. Maintenant, nous devons interagir avec le monde extérieur et nous le ferons en utilisant notre action spéciale.

import { subscribe } from 'actions'

class Form extends React.Component {

  onFieldChanged (event) {
    this.setState({[event.target.name]: event.target.value})
  }

  onSubmit (event) {
    event.preventDefault()
    const { name, surname, email } = this.state
    subscribe(name, surname, email)
  }

  render () {
    return (
      <form onChange={::this.onFieldChanged} onSubmit={::this.onSubmit}>
        <input type="text" name="name" />
        <input type="text" name="surname" />
        <input type="email" name="email" />
        <button type="submit">Subscribe</button>
      </form>
    )
  }
}

Lorsque l'utilisateur appuie sur le bouton d'envoi, il s'attend à une réponse de l'interface utilisateur en cas de succès ou non et que faire ensuite. Nous voulons donc suivre l'état de la demande pour afficher l'écran de chargement, rediriger ou afficher un message d'erreur.
La première idée est d'envoyer des actions internes comme loading , submitted , failed pour rendre l'état de l'application responsable de cela. Parce que nous voulons rendre l'état et ne pensons pas aux détails.

  render () {
    //provided by connector of whatever
    const { props: { isLoading, error } } = this
    return (
      <form onChange={::this.onFieldChanged} onSubmit={::this.onSubmit}
        disabled={isLoading}>
        <input type="text" name="name" />
        <input type="text" name="surname" />
        <input type="email" name="email" />
        <button type="submit">Subscribe</button>
        {error ? <p>{ error }</p> : null}
      </form>
    )
  }

Mais ici, le diable apparaît : cet état de page est maintenant persistant et le prochain appel render(state) affichera le formulaire dans un état sale. Nous devons donc créer une action cleanup pour l'appeler chaque fois que nous montons le formulaire.
Et pour chaque formulaire que nous avons, nous ajouterons le même passe-partout de loading , submitted , failed et cleanup actions avec magasins/réducteurs… jusqu'à ce que nous nous arrêtions et réfléchissions sur l'état que nous voulons réellement rendre.

submitted appartient-il à l'état de l'application ou s'agit-il simplement de l'état du composant, comme la valeur du champ name . L'utilisateur se soucie-t-il vraiment que cet état soit conservé dans un cache ou souhaite-t-il voir la forme propre lorsqu'il le revisite ? La réponse : ça dépend.

Mais dans le cas de formulaires de soumission purs, nous pourrions supposer que l'état du formulaire n'a rien à voir avec l'état de l'application : nous voulons savoir si l'utilisateur est logged in ou non en cas de page de connexion, ou nous ne voulons pas pour savoir quoi que ce soit, a-t-il changé de mot de passe ou échoué: si la demande réussit, redirigez-le simplement ailleurs.

Alors, qu'en est-il de la proposition que nous n'avons pas besoin de cet état ailleurs que dans Component ? Essayons.
Pour fournir une implémentation plus sophistiquée, nous pourrions supposer qu'en 2015, nous vivons dans le monde asynchrone et que nous avons Promises partout, alors laissez notre action renvoyer un Promise :

function subscribe (name, surname, email) {
  return api.request('/subscribtions', 'create', { name, surname, email })
}

À l'étape suivante, nous pourrions commencer à suivre l'action dans notre Component :

onSubmit (event) {
  event.preventDefault()
  const { name, surname, email } = this.state
  subscribe(name, surname, email)
    .then(() => { this.setState({ submitted: true }) })
    .catch(error => { this.setState({ error }) })
}

componentWillUpdate (object nextProps, object nextState) {
  if(nextState.submitted)
    redirect('somewhere')
}

On dirait que nous avons tout ce dont nous avons besoin pour rendre le composant sans polluer l'état de l'application et que nous n'avons plus besoin de le nettoyer.
Nous y sommes donc presque. Nous pourrions maintenant commencer à généraliser le flux Component .
Tout d'abord, séparons-nous des préoccupations : garder une trace des réactions d'action et de mise à jour de l'état :
(Faisons-le en termes de redux pour rester dans les limites du repo)

import React, { PropTypes } from 'react'
import { bindActionCreators } from 'redux'

// Keeps track of action
export default function connectSubmitForm (Form, submitAction) {
  return React.createClass({
    contextTypes: {
      //redux Store
      store: PropTypes.object.isRequired
    },

    getInitialState () {
      return {}
    },

    onSubmit (...args) {
      const { context: { store: { dispatch } } } = this
      const { submitAction: submit }
        = bindActionCreators({ submitAction }, dispatch)
      submit(...args)
        .then(() => this.setState({ submitted: true }))
        .catch(error => this.setState({ error }))
    },

    render () {
      const {
        onSubmit,
        props,
        state: { submitted, error }
      } = this
      return (<Form {...props} onSubmit={onSubmit} submitted={submitted}
        error={error} />)
    }
  })
}

et

// redirect to path if predicate returns true
export default function redirect (path, predicate) {
  return Component =>
    class Composed extends React.Component {

      componentWillMount () {
        if (predicate(props))
          redirectTo(path)
      }

      componentWillReceiveProps (nextProps) {
        if (predicate(nextProps))
          redirectTo(path)
      }

      render () {
        return <Component {...this.props} />
      }
    }
}

//redirect to path if submitted
export default function redirectSubmitted (path) {
  return redirect(path, ({ submitted }) => submitted)
}

Maintenant, d'une part, nous avons decorator qui fournit des props submitted et error avec rappel onSubmit et d'autre part decorator qui redirige quelque part si obtient la propriété submitted === true . Attachons-les à notre formulaire et voyons ce que nous obtenons.

@redirectSubmitted('/')
class Form extends React.Component {

  onFieldChanged (event) {
    this.setState({[event.target.name]: event.target.value})
  }

  onSubmit (event) {
    event.preventDefault()
    const { name, surname, email } = this.state
    this.props.onSubmit(name, surname, email)
  }

  render () {
    return (
      <form onChange={::this.onFieldChanged} onSubmit={::this.onSubmit}>
        <input type="text" name="name" />
        <input type="text" name="surname" />
        <input type="email" name="email" />
        <button type="submit">Subscribe</button>
        {this.props.error ? <p>{ this.props.error }</p>: null}
      </form>
    )
  }
}

export default submitForm(Form, subscribe)

Et le voici : forme de rendu pure sans dépendances de la bibliothèque $# routing 8$#$, implémentation flux , application state , faisant ce pour quoi elle a été créée : soumettre et rediriger.

Merci d'avoir lu et désolé d'avoir pris votre temps.

Assez juste! Pour être honnête, je suis toujours intéressé par le stockage de l'état des composants locaux dans Redux : #159.

@stremlenye C'est assez lisse. Merci pour le partage!

@iclanzan merci pour la lecture :)
En fait, je pense maintenant à la proposition de @gaearon qu'il a mentionnée dans un commentaire précédent. Jetez un œil à la discussion #159 pour avoir des idées importantes.

Fermeture - lorsque RR 1.0 sortira, nous couvrirons cela dans la recette "Utilisation avec React Router".
Cela peut être suivi dans https://github.com/rackt/redux/issues/637.

@stremlenye votre approche est assez géniale ! Serait-il judicieux - au lieu d'utiliser un décorateur - d'utiliser un composant d'ordre supérieur react-router dans lequel tous les composants _non autorisés_ seraient imbriqués et il serait responsable de la redirection une fois que le magasin d'authentification aurait un utilisateur?

<ReduxRouter>
    <Route component={ Auth }>
      <Route path="/" component={ Login } />
      <Route path="/reset-password" component={ ResetPassword } />
    </Route>
  </ReduxRouter>

Quelque chose comme ça, où le Auth ne s'occupe que de détecter si l'utilisateur est connecté. De cette façon, je peux toujours manipuler mon magasin en envoyant des actions à partir du composant Login . Pas si pur, mais moins de code et plus facile à comprendre :)

@tomazzaman Merci :)
La plupart des applications React que j'ai écrites incluaient quelque chose comme ApplicationContainer qui pourrait être utilisé dans le même but sans nouvelle implémentation Route .
Si vous utilisez RR-v1.0.0-rc1 , vous pouvez atteindre les mêmes objectifs avec le crochet onEnter : vérifiez simplement si l'état actuel correspond à authentifié ou non.

J'ai aussi réfléchi à cela, et il semble que @stremlenye ait fait un excellent travail en montrant comment cela peut être fait. Le problème c'est qu'on a tendance à rester bloqué dans la boucle "tout doit passer par la boucle action - réducteur - mise à jour-composant-avec-état du magasin" (moi aussi) et qu'on a alors tendance à polluer le magasin avec toutes sortes de état qui n'est utilisé que dans des scénarios très spécifiques dans l'application (sur 1 écran, des choses telles que "ai-je exécuté avec succès le mot de passe de réinitialisation"), alors que cela peut être facilement géré en faisant simplement les appels asynchrones à l'intérieur du composant et en mettant à jour le composant lui-même en utilisant this.setState au lieu de polluer l'état global + avoir à réinitialiser toutes ces variables d'état au cas où nous devions ouvrir ce composant une seconde fois plus tard. Sans même mentionner la pollution des réducteurs d'état avec tout le code de gestion des débuts/succès/erreurs.

Je vais essayer d'adapter mon code ici aussi pour voir s'il nettoie les choses.

@ir-fuel si vous avez le temps, pourriez-vous pousser un petit exemple de ce que vous obtiendrez sur le github ?

Eh bien, ce sera difficile à faire, mais je peux donner un petit exemple de ce que j'ai fait. J'ai un composant où un utilisateur peut demander à réinitialiser son mot de passe. L'opération de réinitialisation réelle se passe comme ceci maintenant :

  submitForgotPassword = (username) => {
    this.setState({isLoading:true})
    doForgotPassword(username).then((result) => {
      this.setState({passwordIsReset:true,isLoading:false})
    }).catch((result) => {
      this.setState({passwordIsReset:false,isLoading:false,errorMessage:result.error})
    })
  }

doForgotPassword est une fonction qui fait un appel ajax à mon serveur.

Donc, pas d'allers-retours vers le magasin / les réducteurs, pas de pollution de l'état global avec des éléments dont vous ne vous souciez qu'à l'intérieur d'un composant et est uniquement utilisé pour afficher temporairement l'état dans ce composant, et pas besoin de réinitialiser un tas de variables d'état à l'intérieur de vos réducteurs.

et le rendu ressemble à ceci

  render() {

    let divClassName = ''

    if(this.state.isLoading) {
      divClassName = 'disable-controls'
    }

    let child = null

    if(this.state.passwordIsReset)
      child = (<ForgotPasswordSuccess/>)
    else
      child = (<ForgotPasswordForm submit={this.submitForgotPassword} errorMessage={this.state.errorMessage}/>)

    return (
      <div className={divClassName}>
        {child}
      </div>
    )
  }

J'espère que ceci est clair.

Ça a l'air bien :+1:
La seule astuce qui devrait être couverte (de mon point de vue) consiste à ajuster le middleware pour renvoyer la promesse de l'action levée (juste proxy la valeur de retour de l'action probablement). Ce faisant, nous autoriserons l'action à modifier l'état dans un flux normal si nécessaire, en gardant la possibilité de se connecter aux éléments Promise dans la portée du composant.

Je pense qu'il est normal de considérer l'état de votre application comme un état déjà connecté. si aucun utilisateur n'est connecté, pourquoi avez-vous besoin d'un état d'application ? bien sûr, vous pourriez en avoir un dans le composant de connexion.

La logique d'application sur la page de connexion ne devrait pas être si complexe pour posséder un état supérieur d'application, n'est-ce pas ?

Le flux de connexion @SkateFreak fait partie d'une application. N'avez-vous pas pensé à un état de flux de connexion (progression, erreur), à un état de flux d'inscription, etc. pour lequel vous feriez mieux d'utiliser également React (probablement avec Redux, si vous l'utilisez déjà pour d'autres composants d'application).

@sompylasar je suis en faveur d'un état d'application, je pense juste que tant que l'utilisateur est EN DEHORS de mon application, je peux lui servir une application plus simple sans magasin redux. mon magasin redux démarrera dès que vous ENTREZ dans mon application.

cette approche a fonctionné pour moi jusqu'à ce moment précis, mais je comprendrai peut-être la nécessité d'un état d'application en phase de connexion dans mes futurs projets

Vous pouvez au niveau racine de votre boutique diviser votre boutique en partie 'application' et partie 'utilisateur/login', chacune avec son propre réducteur. Ainsi, dans ce cas, vous ne polluerez jamais vos réducteurs d'application avec le code de réduction de connexion et vice versa.

De : SkateFreak < [email protected] [email protected] >
Répondre à : rackt/redux < [email protected] [email protected] >
Date : dimanche 13 décembre 2015 13h48
À : rackt/redux < [email protected] [email protected] >
Cc : Joris Mans < [email protected] [email protected] >
Objet : Re : [redux] Meilleures pratiques de gestion du flux de données pour les pages de connexion/d'inscription avec redirection (#297)

@sompyl asarhttps://github.com/sompylasar je suis en faveur d'un état d'application, je pense juste que tant que l'utilisation est EN DEHORS de mon application, je peux lui servir une application plus simple sans magasin redux. mon magasin redux démarrera dès que vous ENTREZ dans mon application.

cette approche a fonctionné pour moi jusqu'à ce moment précis, mais je comprendrai peut-être la nécessité d'un état d'application en phase de connexion dans mes futurs projets

Répondez directement à cet e-mail ou consultez-le sur Gi tHubhttps://github.com/rackt/redux/issues/297#issuecomment -164287194.

corrigez-moi si je me trompe, mais le but d'un magasin de flux n'est-il pas de partager des données et l'état de l'application entre les routes et les composants? pourquoi ai-je besoin de "réduire" mon état et d'utiliser mes "actions" pour maintenir un état supérieur si tout cela n'a pas d'importance lorsque l'utilisateur se connecte ?

Je n'en vois pas les avantages, même si c'est une façon très organisée et propre de faire les choses. Je gère mon état de pré-connexion sur mon composant <Sign /> , qui contient SignIn, SignUp et ForgotPassword.

Parce qu'une inscription peut être un processus plus compliqué que de simplement demander un nom d'utilisateur et un mot de passe.
Tout dépend de la complexité de votre flux.

De : SkateFreak < [email protected] [email protected] >
Répondre à : rackt/redux < [email protected] [email protected] >
Date : dimanche 13 décembre 2015 14h00
À : rackt/redux < [email protected] [email protected] >
Cc : Joris Mans < [email protected] [email protected] >
Objet : Re : [redux] Meilleures pratiques de gestion du flux de données pour les pages de connexion/d'inscription avec redirection (#297)

corrigez-moi si je me trompe, mais le but d'un magasin de flux n'est-il pas de partager des données et l'état de l'application entre les routes et les composants? pourquoi ai-je besoin de "réduire" mon état et d'utiliser mes "actions" pour maintenir un état supérieur si tout cela n'a pas d'importance lorsque l'utilisateur se connecte ?

Je n'en vois pas les avantages, même si c'est une façon très organisée et propre de faire les choses. Je gère mon état de pré-connexion sur mon composant, qui contient SignIn, SignUp et ForgotPassword.

Répondez directement à cet e-mail ou consultez-le sur Gi tHubhttps://github.com/rackt/redux/issues/297#issuecomment -164287893.

Je suis d'accord.
mais je vois SignUp comme un formulaire, avec un état - qui devrait être suffisant, quelle que soit la complexité de l'imo ...

par contre, que sais-je des inscriptions...
les gens sont fous

Actuellement, je rencontre un problème similaire mais différent.

Je travaille sur un flux comme une inscription super complexe, et nous avons une page continue pour permettre à l'utilisateur de saisir les informations requises, chaque page est un formulaire. Le flux ressemble à :

FormPage_A -> FormPage_B -> FormPage_C -> SuccessPage

Le problème est que nous ne faisons aucune requête API avant FormPage_C. Nous enregistrons simplement les données dans le navigateur sur FormPage_A/B et les envoyons toutes à l'API dans FormPage_C.

Donc, la question est de savoir si nous devons mettre les données de formulaire de FormPage_A et FormPage_B dans l'état de l'application ?

Si oui, il semble que nous enregistrions des données temporaires qui n'ont aucun effet sur l'interface utilisateur dans l'état de l'application. Et puisque l'état de l'application de redux vit en mémoire, que se passera-t-il lorsque l'utilisateur actualisera la page dans FormPage_B ?

Si non, quel est le bon endroit pour le mettre ? Devrions-nous construire une autre couche comme fake api à partir du flux redux pour la sauvegarder ?

@frederickfogerty @gaearon @ir-fuel @stremlenye

Je passerais simplement les propriétés dans this.state qui sont pertinentes pour votre soumission en tant qu'accessoires à l'étape suivante.
donc FormPage_A.state sera passé en FormPage_B.props etc. et faites la soumission en FormPage_C

ou si vous avez un composant parent qui gère cela, stockez l'état dans la propriété state de ce contrôle parent et après la dernière étape, soumettez tout

Assurez-vous simplement que si l'utilisateur décide de recharger la page, vous recommencez depuis le début.

@kpaxqin Je pense que dans ce cas, vous pourriez déclarer un composant de gestion comme Flow qui gérera le sous-ensemble d'étapes avec leurs états et la logique générique back-forward pour chacune.
Ainsi, vous pouvez rendre vos étapes aussi sans état que possible et suivre l'état de l'ensemble du processus dans le composant englobant.
Quelque chose comme:

<Flow onForward={…} onBackward={}>
  <Step1 />
  <Step2 />
  <Step3 />
  …
  <Finish onSubmit={this.props.submit(this.state.model) />
</Flow>

@ir-fuel @stremlenye

Merci pour vos suggestions.
Il semble que nous ne voulions pas tous mettre ces éléments temporaires en état d'application.

Mais lorsque l'utilisateur actualise la page dans la moitié du flux

Assurez-vous simplement que si l'utilisateur décide de recharger la page, vous recommencez depuis le début.

Cela ne semble pas être une bonne expérience utilisateur, nous sommes un site mobile (c'est l'une des raisons pour lesquelles nous avons divisé une forme énorme en petites pages), il est pénible de saisir des choses sur mobile, et je pense que l'utilisateur deviendra fou s'il perd tout le choses après avoir appuyé sur le bouton d'actualisation, peut-être qu'ils vont simplement abandonner.

Je pense que peut-être que construire le fake api hors du flux redux est un choix acceptable, soumettez les choses au fake api à chaque étape et récupérez-les en cas de besoin, tout comme nous avons une API pour cela, et puisque nous utilisons comme api, la seule chose qui s'y connecte est les créateurs d'action, les pages et les composants seront propres et il est également facile de gérer les erreurs (utilisez le code commun pour gérer l'erreur api).

Cela a du sens pour moi car une fois que nous voulons conserver ces données, elles ne doivent pas être dans l'état du composant ou de l'application, l'effet secondaire doit être géré dans le créateur d'action

//fake api
const fakeApi = {
  getStepA: ()=>{
    /* get things from LS */
    return Promise
  },
  setStepA: ()=>{
    /* put things into LS */
    return Promise
  }
}

// action creator
submitA(dispatch, getState) { // thunk function
  fakeApi
    .setStepA()
    .then(dispatchSuccess)
    .then(transitionToStepB)
    .catch(dispatchError);
}

Cela rend les choses claires pour moi, mais il semble bizarre d'ajouter quelque chose de notre flux redux dans notre application.

Je ne me soucierais vraiment pas que l'utilisateur recharge votre page.
De l'autre côté, vous nous dites que vous avez un processus d'inscription compliqué avec plusieurs formulaires, puis vous dites que c'est un site mobile. Ne pensez-vous pas que cela rendra également les utilisateurs fous et les fera abandonner s'ils doivent remplir trop de choses sur un appareil mobile ? Je pense que c'est encore pire que de ne pas supporter le rechargement. Je ne pense pas que de nombreux sites prennent en charge le rechargement de page au milieu d'un processus d'inscription de toute façon.

Je pense que cela rend les choses beaucoup plus compliquées pour quelque chose qui n'arrive vraiment pas beaucoup. Je me concentrerais davantage sur la tentative de simplifier le processus d'inscription ou de le diviser en plusieurs étapes sur votre site Web afin que l'utilisateur n'ait pas l'impression qu'il remplit X pages de formulaires avant d'activer enfin son compte.

Et si vous voulez vraiment prendre en charge le rechargement en cours d'inscription, demandez simplement au composant parent qui contrôle toutes ces étapes de stocker son état dans localStorage .

@kpaxqin J'opterais pour l'approche "fausse API". Pourquoi faux BTW, c'est juste l'API de stockage intermédiaire dont vous avez besoin pour votre processus en plusieurs étapes. Toutes les API ne doivent pas nécessairement se trouver sur un serveur et être accessibles via HTTP ou un autre protocole réseau.

Ce sur quoi je suis d'accord avec @ir-fuel, c'est que tout l'état du processus en plusieurs étapes doit être stocké, pas seulement l'étape en cours.

@ir-fuel Reload peut se produire sans intention explicite de l'utilisateur, par exemple, un utilisateur peut basculer vers une autre application mobile, comme un ordinateur portable ou un appel téléphonique, et le navigateur peut perdre son état en raison d'une mémoire insuffisante sur l'appareil, donc il se rechargera lorsque l'utilisateur reviendra.

Encore une fois, le problème est que vous polluez l'état global de l'application avec quelque chose dont vous n'avez besoin que dans quelques écrans de votre application Web.

@sompylasar @kpaxqin Je pense que @ir-fuel a suggéré la bonne approche - si vous avez besoin que quelque chose d'intermédiaire soit éventuellement persistant, persistez-le côté client: beaucoup plus facile à mettre en œuvre, n'implique pas vraiment le serveur dans la persistance de données incohérentes, n'est-ce pas? t effectuer des requêtes http (ce qui est également un problème majeur sur les appareils mobiles) et il est extrêmement facile de changer l'approche à l'avenir.

Et à propos de la persistance de données quelque part - pensez à la façon dont vous allez les nettoyer après l'échec du processus ou déjà terminé - c'était le thème initial d'un sujet.
Une autre option que vous pouvez utiliser - conserver les données utilisateur dans l'URL. Si vos formulaires se composent de quelques champs de texte, cela pourrait être la bonne solution de compromis.

Ce n'est pas tant "le stocker côté client ou serveur", c'est plus une question de "est-ce que je gère ce" ad hoc "dans ma classe de composants parent" vs "est-ce que je fais toute la danse action/réducteur/état".
Je n'aime pas passer par l'action/les réducteurs si c'est pour un état temporaire. Je ne fais cela que pour l'état réel de l'application.

@ir-carburant :+1:

@stremlenye Veuillez relire ma réponse. Je dis explicitement qu'il n'est pas nécessaire de créer une API côté serveur pour créer une couche d'API de persistance.

@ir-fuel Ne pas passer par des actions/réducteur annule la prévisibilité et la rejouabilité des transitions d'état.

@ir-fuel Vous pouvez facilement effacer l'état de votre application une fois le formulaire soumis, afin qu'il ne soit pas "pollué".

RE : dégagement. Effectuez une action CLEAR et ayez un bouton dans votre interface utilisateur pour réinitialiser le flux d'inscription. Vous pouvez également le faire via un routeur (envoyez cette action lors de la visite de l'URL d'inscription initiale, et si l'inscription a commencé, changez l'URL en autre chose).

@sompylasar il n'y a aucun problème pour réinitialiser votre état avec un bouton explicite - les problèmes surviennent après des échecs non gérés : il est difficile de prédire comment l'état pourrait être corrompu et de définir une stratégie de restauration pour chaque cas particulier.

@stremlenye oui, le génie logiciel est parfois difficile. Les états et les transitions prévisibles aident à atténuer cela.

Non seulement cela, mais si votre état est plus qu'un tas d'objets json (ce que je déteste parce que tout est par convention et rien n'est appliqué) et que vous utilisez quelque chose dans la ligne de Immutable.js' Record alors vous devez déclarer les propriétés de vos objets à l'avance et cela inclut tous les éléments temporaires que vous voudrez y inclure.

Pour moi, construire quelque chose avec redux sans utiliser Immutable.js n'est qu'un accident qui attend de se produire.

Je sais que vous pourriez créer un enregistrement Signup et qu'il ne soit pas nul lors de l'inscription, mais le problème est plus fondamental, car vous pourriez avoir beaucoup de ces cas dans votre application Web, et c'est ce qui dérange moi.

@ir-carburant

Non seulement cela, mais si votre état est plus qu'un tas d'objets json (ce que je déteste parce que tout est par convention et rien n'est appliqué) et que vous utilisez quelque chose dans la ligne de Immutable.js' Record alors vous devez déclarer les propriétés de vos objets à l'avance et cela inclut tous les trucs temporaires que vous voudrez jamais là-dedans.

Pour moi, construire quelque chose avec redux sans utiliser Immutable.js n'est qu'un accident qui attend de se produire.

Pour l'instant, je ne suis pas convaincu d'utiliser Immutable.js avec redux, il m'a semblé trop complexe à intégrer (et pas performant). Un gel pendant le développement et les tests, et un ensemble de tests devraient suffire à couvrir la plupart des cas. Et une convention stricte pour éviter de muter l'état dans les réducteurs ou autres endroits où vous avez la référence.

Je sais que vous pouvez créer un enregistrement d'inscription et qu'il ne soit pas nul lors de l'inscription

Oui, c'est ce que j'avais en tête.

mais le problème est plus fondamental, car vous pourriez avoir beaucoup de ces cas dans votre webapp, et c'est ce qui me dérange.

C'est ce que nous appelons l'état explicite de l'application - à chaque instant, nous connaissons l'état de l'application qui se compose de nombreux composants. Si vous souhaitez rendre la structure d'état moins explicite et plus dynamique, suivez https://github.com/tonyhb/redux-ui

C'est tout le problème du développement JS. « Restons-en aux conventions ». Et c'est pourquoi les gens commencent à se rendre compte que cela ne fonctionne tout simplement pas dans des environnements complexes, parce que les gens font des erreurs et proposent des solutions telles que TypeScript et Immutable.js.

Pourquoi serait-ce trop compliqué à intégrer ? Si vous utilisez 'Record', vous utilisez tous ces objets (tant que vous ne faites que les lire) comme des objets JS normaux, avec une notation par points pour accéder aux propriétés, avec comme avantage que vous déclarez à l'avance quelles propriétés font partie de votre objet .

Je fais tout mon développement redux avec Immutable.js, sans aucun problème. Le fait que vous ne pouvez tout simplement pas modifier accidentellement un objet est un gros avantage dans redux et la "sécurité de type" de la définition de vos prototypes d'objet est également pratique.

Et si vous voulez vraiment prendre en charge le rechargement en cours d'inscription, demandez simplement au composant parent qui contrôle toutes ces étapes de stocker son état dans localStorage.

@ir-carburant
Pour moi, mettre des choses dans localStorage ressemble à un effet secondaire et cela ne semble pas une bonne pratique de le faire dans component.

Encore une fois, le problème est que vous polluez l'état global de l'application avec quelque chose dont vous n'avez besoin que dans quelques écrans de votre application Web.

Peut-être que je n'ai pas décrit la fausse API assez clairement, la raison la plus importante pour la construire est que je ne veux pas mettre ces choses only need in a few screens dans l'état de l'application de redux. Dans FormPage_A/B, ils appellent simplement l'API pour stocker les données du formulaire, puis passent à l'étape suivante :

//action
submitFormA (formA) {
  fakeApi
    .saveFormA(formA)
    .then(()=>{ transitionTo(nextPage) })
    .catch()
}

et dans FormPage_C (le formulaire final):

submitFormC (formC) {
  fakeApi
    .getFormAnB()
    .then(mergeFormData(formC)) //mergeFormData is a curry function
    .then(realApi.submitSignUp)
    .catch()
}

Toutes ces données temporaires vivent dans de fausses API et l'état global de l'application n'a pas été pollué. Si nous voulons effacer les données dans une fausse API pour redémarrer le flux, envoyez simplement une action pour le faire.

@sompylasar il n'y a aucun problème pour réinitialiser votre état avec un bouton explicite - les problèmes surviennent après des échecs non gérés : il est difficile de prédire comment l'état pourrait être corrompu et de définir une stratégie de restauration pour chaque cas particulier.

@stremlenye Je suis d'accord avec cela, mais je pense que ces cas d'échec particuliers sont plus susceptibles d'être des affaires, ils sont toujours là, peu importe où nous plaçons l'état, et ils devraient être traités de la bonne manière, mais ils n'ont tout simplement pas été décrits assez clairement à le moment. La seule chose que les développeurs peuvent faire est de faciliter le suivi afin que nous puissions le réparer avec moins de douleur.

Et mettre l'état dans le composant parent peut rendre plus difficile de raisonner sur le moment où les choses deviennent complexes. Avec une fausse API, nous transformons cet état avec des actions et cela semble plus prévisible.

Mais je conviens que la fausse API semble trop lourde dans la plupart des cas avec moins de complexité.

Je n'ai pas parcouru tous les messages ci-dessus. Ici, je donne juste mon approche : détecter les changements d'accessoires dans componentWillReceiveProps.

Supposons que la forme de l'état soit : { loginPending, loginError }, lorsque vous commencez à vous connecter, définissez loginPending = true. En cas de succès ou d'échec, définissez { loginPending : false, loginError : 'some error.' }.

Ensuite, je peux détecter si l'événement de réussite de la connexion dans un composant et rediriger la page :

componentWillReceiveProps(nextProps) {
  if (this.props.loginPending && !nextProps.loginPending && !nextProps.loginError) {
    // Login success, redirect the page here.
  }
}

Cela fonctionne juste mais pas beau.

Mon humble implémentation concernant la suppression du formulaire lors de la redirection à l'aide react-router-redux . Réducteur :

const initialState = {
    login: initialLoginState,
    signup: initialSignupState,
    // ...
};


export default function(state = initialState, action) {
  switch (action.type) {
    case '@@router/LOCATION_CHANGE':
      return {
        ...state,
        login: initialLoginState,
        signup: initialSignupState
      };
      // ...

Pensez-vous que cette approche est OK? Ça marche.

Je suis fini avec ça. Dites ce que vous en pensez :

enregistrer l'action redux :

import { checkStatus } from 'helpers/fetch_helpers';
import { AUTH } from 'config/constants.endpoints';
import { USER_LOGGED_IN, USER_LOGGED_OUT, USER_REGISTERED } from 'config/constants.actions';
import { replace } from 'react-router-redux';

// other actions...

const userRegistered = (user) => ({
  type: USER_REGISTERED,
  user
});

export const register = (formData, redirectTo = '/') => {
  const fetchParams = {
    method: 'post',
    body: formData
  };

  const handleData = (dispatch) => (response) => response.json().then( (data) => {
    dispatch(userRegistered(data));
    dispatch(replace(redirectTo));
  });

  return (dispatch) => fetch(AUTH, fetchParams)
    .then(checkStatus)
    .then(handleData(dispatch));
};

Conteneur de formulaire d'inscription :

import React, { Component } from 'react';
import RegisterForm from './register_form';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as userActions from 'store/actions/user';
import { parseFormErrors } from 'helpers/fetch_helpers';

class RegisterFormContainer extends Component {
  state = {
    errors: {}
  }

  async handleSubmit(e) {
    e.preventDefault();

    const form = e.currentTarget;
    const formData = new FormData(form);
    const { register } = this.props.actions;

    try {
      await register(formData);
    } catch (error) {
      const errors = await parseFormErrors(error);

      this.setState({ errors });
    }
  }

  render() {
    return (
      <RegisterForm handleSubmit={ ::this.handleSubmit } errors={ this.state.errors } />
    );
  }
}

const mapDispatchToProps = (dispatch, ownProps) => ({
  actions: bindActionCreators({ register: userActions.register }, dispatch)
});

export default connect(
  null,
  mapDispatchToProps
)(RegisterFormContainer);

et aides :

export const checkStatus = (response) => {
  if (response.ok) {
    return response;
  } else {
    const error = new Error(response.statusText);

    error.response = response;
    throw error;
  }
};

export const parseFormErrors = (error) => error.response.json().then( (data) => {
  if (data.errors) {
    const joinedErrors = _.mapValues(data.errors, (errors) => errors.join(' ') );

    return { ...joinedErrors };
  } else {
    console.error('request failed:', error);

    return {};
  }
});

Ce qui est important - seule une inscription réussie entre dans le magasin Redux en tant qu'action, sinon l'erreur passe à l'état du composant de conteneur. L'utilisateur reçoit une erreur, puis navigue vers une autre page, puis à nouveau vers la page du formulaire - le formulaire est vide et sans erreur. Il n'y a pas d'actions "USER_REGISTRATION_REQUEST" ou "USER_REGISTRATION_FAILURE" car elles décrivent l'état du composant, pas l'état de l'application.

@sunstorymvp

Vous avez sucré votre code avec async / await , et ce sucre cache le fait qu'il y a trois états de la requête (avant, en cours, après). Ces états sont masqués dans le composant de vue. Le démarrage de la demande est ignoré et l'erreur de demande est gérée dans la vue et n'est pas envoyée via redux, de sorte que l'état de l'application est hors service ici. Si, pour une raison quelconque, le composant qui contient cet état se démonte puis remonte, l'état de la demande d'enregistrement sera perdu et son résultat ne sera pas géré, ce qui pourrait entraîner des erreurs ultérieures (par exemple, l'utilisateur s'est déjà enregistré).

L'utilisateur reçoit une erreur, puis navigue vers une autre page, puis à nouveau vers la page du formulaire - le formulaire est vide et sans erreur.

Pour y parvenir, vous pouvez envoyer une action telle que USER_REGISTRATION_RESET sur le montage du composant qui réinitialiserait l'erreur dans le magasin. Cette action ne doit être gérée que s'il n'y a pas de demande d'enregistrement en attente, sinon le résultat de la demande en attente n'est pas géré par l'application.

merci pour vos commentaires.

Vous avez sucré votre code avec async/attend, et ce sucre cache le fait qu'il y a trois états de la requête (avant, en cours, après)

ces conditions existent indépendamment du fait que j'utilise async/wait ou non.

Ces états sont masqués dans le composant de vue

comme prévu, c'est l'état du composant .. pourquoi cela devrait-il être dans l'état de l'application?

Le démarrage de la demande est ignoré

en cours...

et l'erreur de requête est gérée dans la vue et n'est pas envoyée via redux

comme prévu

Si pour une raison quelconque le composant qui contient cet état se démonte puis remonte, l'état de la demande de registre sera perdu et son résultat ne sera pas géré

ce n'est pas vrai. l'utilisateur sera enregistré et redirigé si la demande aboutit, quel que soit l'état ou même l'existence du composant. regardez le créateur d'action register (handleData).

Pour y parvenir, vous pouvez envoyer une action...

C'est le but. Une autre opération ? Pourquoi tant de passe-partout ? Désactivez ou activez le bouton d'envoi, affichez les erreurs de formulaire ou la validation en direct, affichez le spinner lorsque la demande est ouverte... et ces actions conduisent à de nouvelles actions qui restaurent l'état par défaut... pourquoi cela doit-il être dans l'état de l'application ?

ce n'est pas vrai. l'utilisateur sera enregistré et redirigé si la demande aboutit, quel que soit l'état ou même l'existence du composant. regardez le créateur d'action de registre (handleData).

Oui, mais cela laisse la possibilité de deux requêtes register consécutives car l'état de la requête n'est pas suivi dans l'état de l'application.

Désactivez ou activez le bouton d'envoi, affichez les erreurs de formulaire ou la validation en direct, affichez le spinner lorsque la demande est ouverte... et ces actions conduisent à de nouvelles actions qui restaurent l'état par défaut... pourquoi cela doit-il être dans l'état de l'application ?

Pour la prévisibilité et la rejouabilité, c'est le but de Redux. N'ayant que les actions et l'état initial, vous pouvez mettre l'application dans un certain état en envoyant uniquement des actions. Si un composant commence à contenir un état, l'état de l'application commence à se séparer, il n'y a plus d'arbre d'état unique qui représente l'état de l'application.

Ne soyez pas trop dogmatique à ce sujet, cependant. Il existe de nombreuses raisons de mettre des éléments dans l'état Redux et de nombreuses raisons de laisser quelque chose dans l'état des composants locaux, selon votre scénario. Voir http://redux.js.org/docs/FAQ.html#organizing -state-only-redux-state et https://news.ycombinator.com/item?id=11890229.

Ok, maintenant c'est clair pour moi : ça dépend. Merci!

J'utilise componentWillReceiveProps et j'ai un objet auth sur mon composant. Je ne sais pas si cela aide du tout.

// using reduxform
class LoginForm extends Component {
  static propTypes = {
    // this holds info on the current user.
    auth: PropTypes.shape({
      userId: PropTypes.number,
      token: PropTypes.string
    }),
     // react-router-redux.
    history: PropTypes.object.isRequired,
    submitting: PropTypes.bool.isRequired,
    fields: PropTypes.object.isRequired
  };
  componentWillReceiveProps(newProps) {
    const { auth } = newProps;
    auth && auth.token && this.props.history.go('#/');
  }
  render() {
    return (/**...*/) // form logic here.
  }
}

Y a-t-il quelque chose de mal à procéder ainsi ?

Qu'en est-il d'une telle approche http://codereview.stackexchange.com/questions/138296/implementing-redirect-in-redux-middleware ?
Passage de l'URL de redirection à l'action et appel supplémentaire de la méthode next avec push partir de react-router-redux dans le middleware.

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