Redux: Mejores prácticas en el manejo del flujo de datos para páginas de inicio de sesión/registro con redirección

Creado en 22 jul. 2015  ·  66Comentarios  ·  Fuente: reduxjs/redux

EDITAR: lo que estoy tratando de entender aquí es cómo manejar el flujo de datos cuando tiene una página con un formulario. Después de enviar el formulario, lo redirigirá a otra página o mostrará un mensaje de error .

El desafío es dónde se debe _almacenar temporalmente_ esta información en el flujo de datos. Porque después de redirigir o mostrar el mensaje, el estado (o la información que almacenó) ya no es relevante y debe restablecerse o limpiarse.


En una aplicación web de back-end, generalmente tiene páginas como inicio de sesión, registro, restablecimiento de contraseña, etc.

Cuando el usuario navega a la aplicación y no ha iniciado sesión, se le redirige a la página de inicio de sesión. Esto se puede hacer fácilmente con el gancho RR onEnter .

Luego, el usuario completará el formulario y lo enviará. En este punto, generalmente, si la solicitud fue exitosa, desea redirigir a la raíz de la aplicación o a alguna otra página "segura". Si falló, desea permanecer en la página y mostrar un mensaje de error.

(Esto se aplica no solo a la página de inicio de sesión, sino también a otras páginas, así como a las mencionadas anteriormente. Por ahora, solo me centraré en el inicio de sesión)

Hasta ahora tienes este flujo (que es un caso de uso común):

  • el usuario va a /
  • redirigir a /login
  • llenar formulario y enviar
  • si está bien redirigir a / (o alguna otra página)
  • si falla, permanezca en el formulario de inicio de sesión y muestre el mensaje de error

Si implementamos este flujo con redux, supongo que tengo la siguiente configuración:

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

Dado que este flujo es correcto, con suerte :), puede ver que tengo 2 indicadores en el estado de la aplicación: shouldRedirect y errorMessage .
Esas banderas solo se usan al enviar el formulario.

Primera pregunta: ¿está bien tener esas banderas en el estado de la aplicación?

Otra cosa a considerar es que tengo que "restablecer" esas banderas cuando redirijo porque si quiero ir a otra página (por ejemplo, registro, ...) debería tener un estado limpio ( { shouldRedirect: false, errorMessage: null } ).

Así que podría querer enviar otra acción como

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

¿Alguien ha tenido este problema? ¿Me estoy perdiendo algo o es esa la forma "correcta" de hacerlo?

¿Hay otras alternativas?

¡Cualquier comentario es muy bienvenido! :)

discussion docs

Comentario más útil

Creo que la pregunta es un poco más profunda que simplemente generalizar 'enviar formularios': se trata más de cuál es realmente el estado y qué datos deben aparecer en store y cuáles no.

Me gusta pensar en View parte del flujo de datos de flujo como función pura render(state): DOM pero no es totalmente cierto.
Tomemos como ejemplo un componente de formulario simple que representa dos campos de entrada:

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

Al escribir en estos campos, se realizaron algunas modificaciones de estado internas, por lo que tendremos algo como esto al final:

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

Así que tenemos un estado que no está vinculado al estado de la aplicación ya nadie le importa. Cuando volvamos a visitar este formulario la próxima vez, se mostrará borrado y el 'John Snow' desaparecerá para siempre.
Parece que modificamos DOM sin tocar el flujo de datos de flujo.

Supongamos que este es un formulario de suscripción para alguna carta de notificación. Ahora debemos interactuar con el mundo exterior y lo haremos usando nuestra acción especial.

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

Cuando el usuario presiona el botón Enviar, espera cualquier respuesta de la interfaz de usuario si tuvo éxito y qué hacer a continuación. Por lo tanto, queremos realizar un seguimiento del estado de la solicitud para mostrar la pantalla de carga, redirigir o mostrar un mensaje de error.
La primera idea es enviar algunas acciones internas como loading , submitted , failed para que el estado de la aplicación sea responsable de eso. Porque queremos representar el estado y no pensar en los detalles.

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

Pero aquí aparece el diablo: ahora tenemos este estado de página persistente y la próxima llamada render(state) mostrará el formulario en un estado sucio, por lo que necesitamos crear una acción cleanup para llamarlo cada vez que montemos el formulario.
Y para cada formulario que tengamos agregaremos el mismo modelo de loading , submitted , failed y cleanup acciones con tiendas/reductores… hasta que nos detengamos y pensemos sobre el estado que realmente queremos renderizar.

¿ submitted pertenece al estado de la aplicación o es solo el estado del componente, como el valor del campo name ? ¿Al usuario realmente le importa que este estado persista en un caché o quiere ver el formulario limpio cuando lo vuelva a visitar? La respuesta: depende.

Pero en el caso de formularios de envío puros, podríamos suponer que el estado del formulario no tiene nada que ver con el estado de la aplicación: queremos saber si el usuario logged in es el usuario o no en el caso de la página de inicio de sesión, o no queremos para saber si cambió la contraseña o falló: si la solicitud tiene éxito, simplemente rediríjalo a otro lugar.

Entonces, ¿qué tal la proposición de que no necesitamos este estado en ningún otro lugar excepto Componente? Hagamos un intento.
Para proporcionar una implementación más elegante, podríamos suponer que en 2015 vivimos en un mundo asincrónico y tenemos Promises todas partes, así que dejemos que nuestra acción devuelva un Promise :

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

En el siguiente paso, podríamos comenzar a realizar un seguimiento de la acción en nuestro 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')
}

Parece que tenemos todo lo que necesitamos para representar el componente sin contaminar el estado de la aplicación y eliminamos la necesidad de limpiarlo.
Así que estamos casi allí. Ahora podríamos empezar a generalizar el flujo Component .
En primer lugar, dividámonos en preocupaciones: realizar un seguimiento de la acción y las reacciones de actualización de estado:
(Hagámoslo en términos de redux para permanecer en los límites del repositorio)

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

y

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

Ahora, por un lado, tenemos decorator que proporcionan accesorios submitted y error con devolución de llamada onSubmit y, por otro lado decorator que redirige a algún lugar si obtiene la propiedad submitted === true . Adjuntémoslos a nuestro formulario y veamos qué obtenemos.

@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)

Y aquí está: forma de representación pura sin dependencias de la biblioteca $# routing 8$#$, la implementación flux , la aplicación state , haciendo lo que fue creado para: enviar y redirigir.

Gracias por leer y perdón por tomar su tiempo.

Todos 66 comentarios

En lugar de tener shouldRedirect allí, ¿qué tal tener un reductor de sesión, que solo rastrea isLoggedIn ? De esa manera, puede redirigir desde su página de inicio de sesión a cualquier otra página marcando isLoggedIn en lugar de shouldRedirect .

Además, se puede restablecer el estado simplemente procesando la acción LOGGED_IN en su reductor. Si ocurre esa acción, sabe que puede restablecer de forma segura el estado de error a un null .

¿Esto tiene sentido? :D

Tiene sentido si solo hay la página de inicio de sesión, pero es un enfoque un poco más genérico. Por ejemplo, para una página de registro o una página de restablecimiento de contraseña. Incluso si ha iniciado sesión, debería poder ver esas páginas.

Y esas banderas deberían ser utilizadas por todas esas páginas, porque todas tienen el mismo flujo.

Espero que sea más claro ahora :)

Sí, desde mi punto de vista, "shouldRedirect" es demasiado genérico para asociarlo con una determinada acción. Podría abrir errores en los que de alguna manera olvida establecer esa bandera y luego una página se redirige a otra página a la que en realidad no debería redirigir.

Pero claro, si quieres limpiar tu estado, puedes hacerlo con una acción. O puede escuchar un cambio de historial fuera de reaccionar y luego activar la acción de limpieza de estado. Eso podría ser preferible porque no tiene que dispersar esa lógica entre sus componentes, pero puede mantenerla en un solo lugar.

Pero esa es solo una posible solución para la compensación estatal. Todavía no estoy seguro de cómo resolver la redirección de una manera agradable.

La pregunta también es: ¿tiene sentido tener esas banderas en el estado? Si no, ¿cómo debería implementarse este flujo?

@gaearon , si tienes algo de tiempo, también me encantaría escuchar tus comentarios. ¡Gracias! :+1:

En mi aplicación, estoy redirigiendo en middleware y no uso ninguna bandera. Pero no estoy usando 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);
    };
}

No está refactorizado, está destinado solo para fines de prueba de mi solución de enrutador :).

Este es un copiar y pegar de mi aplicación y cómo manejo la carga de datos de usuario y el inicio de sesión. Intentaré comentar el código lo mejor posible, y estaré en reactiflux para preguntas.

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 términos de responder a sus preguntas, @emmenko , creo que tener shouldRedirect en su estado no es aceptable y debe hacerse con un enfoque más funcional. Tener errorMessage en el estado de la aplicación está bien, solo asegúrese de que esté en un estado de inicio de sesión, como state.auth.errorMessage , en lugar de un nivel superior errorMessage

Creo que tener shouldRedirect en su estado no es aceptable y debería hacerse con un enfoque más funcional.

Estoy de acuerdo. Las cosas que solo son útiles una vez no deberían estar en el estado de la OMI.

Además, según la holgura, no creo que deba almacenar isLoggedIn en el estado de la aplicación, ya que es más una propiedad calculada, que se puede calcular con un selector simple como const isUserLoggedIn = state => state.user.id !== null

Consulte https://github.com/faassen/reselect para obtener más información sobre el uso del selector

@frederickfogerty gracias por el ejemplo.

Sin embargo, una pregunta: esto funciona bien para iniciar sesión porque generalmente tiene un indicador loggedIn en el state . Pero, ¿qué pasa con otras páginas como registro, restablecimiento de contraseña, etc.?
Esas páginas se comportan igual que el inicio de sesión: redirigir si está bien, de lo contrario mostrar un mensaje de error.
Entonces, ¿cómo manejas esos casos? Estoy luchando por encontrar el flujo correcto para esos casos, y no estoy seguro de dónde debería vivir el estado.

Las cosas que solo son útiles una vez no deberían estar en el estado de la OMI.

@gaearon Estoy de acuerdo, pero todavía no estoy seguro de cuál es el enfoque correcto para eso. Todavía necesitas tener un estado en alguna parte...

Así es como lo estoy haciendo atm sin almacenar nada en el estado.

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

No creo que debas almacenar isLoggedIn en el estado de la aplicación, ya que es más una propiedad calculada

Sí, tiene sentido.

@gaearon Supongo que la pregunta para este flujo de trabajo general (redireccionar o mostrar error) es si debe hacerse con "redux" o de una manera diferente. ¿Y cuál sería esta alternativa?

relacionado y lo fijaré aquí... en realidad, también estaba lidiando con este problema y actualmente estoy tratando de volver a implementar el siguiente ejemplo en Redux. Nada mostrable todavía porque estoy haciendo esto en mi tiempo libre:
https://auth0.com/blog/2015/04/09/agregando-autenticación-a-su-aplicación-react-flux/

Es puro Flux:
https://github.com/auth0/react-flux-jwt-authentication-sample

¿Tal vez sea útil para las mejores prácticas?

@emmenko

Entonces, ¿cómo manejas esos casos?

Siempre que inicie sesión, entonces isLoggedIn (que no está en el estado de la aplicación, pero es menor) todavía se cambia, y esto los redirigirá. No importa de dónde vino ese cambio de estado :)

El mejor enfoque que he tomado es agregar esta lógica a un conector donde la funcionalidad es necesaria para sus hijos, por ejemplo, para autorización, digamos que tiene un componente AppProtected , donde todo lo que es hijo de ese componente necesita autorización , luego coloca esta lógica en el conector del componente AppProtected.

Así es como lo estoy haciendo atm sin almacenar nada en el estado.

Definitivamente no me gusta ese enfoque, que tiene un flujo de datos bidireccional entre la interfaz de usuario y los AC, lo que va en contra del flujo. Si está atado a hacerlo de manera imperativa, podría hacer algo como

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 el inicio de sesión del usuario tiene éxito, se le redirige fuera de la página correspondiente.
  • Si el inicio de sesión del usuario falla, no está conectado y permanece en la misma página (no pasa nada)

Esto funciona para todas las páginas que mencionaste.

Lo siento, pero todavía no veo cómo manejo la representación de redirección/error.

Demos un paso atrás y digamos que tengo la página reset-password . También asumimos que no debemos tocar ningún estado almacenando algún tipo de banderas de uso único (como se mencionó anteriormente).
Cuando hago clic para enviar el formulario, ¿qué debe suceder?

Esto es lo que me gustaria saber, como debe ser el flujo y donde debe vivir.

PD: ¡gracias por tus comentarios hasta ahora!

[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]

Esto no es tan elegante como las páginas de inicio de sesión y registro, ya que está muy acoplado

Tengo que dormir, responderé más preguntas mañana.

Así que estamos almacenando las banderas en el estado nuevamente;)

PD: buenas noches!

Creo que la pregunta es un poco más profunda que simplemente generalizar 'enviar formularios': se trata más de cuál es realmente el estado y qué datos deben aparecer en store y cuáles no.

Me gusta pensar en View parte del flujo de datos de flujo como función pura render(state): DOM pero no es totalmente cierto.
Tomemos como ejemplo un componente de formulario simple que representa dos campos de entrada:

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

Al escribir en estos campos, se realizaron algunas modificaciones de estado internas, por lo que tendremos algo como esto al final:

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

Así que tenemos un estado que no está vinculado al estado de la aplicación ya nadie le importa. Cuando volvamos a visitar este formulario la próxima vez, se mostrará borrado y el 'John Snow' desaparecerá para siempre.
Parece que modificamos DOM sin tocar el flujo de datos de flujo.

Supongamos que este es un formulario de suscripción para alguna carta de notificación. Ahora debemos interactuar con el mundo exterior y lo haremos usando nuestra acción especial.

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

Cuando el usuario presiona el botón Enviar, espera cualquier respuesta de la interfaz de usuario si tuvo éxito y qué hacer a continuación. Por lo tanto, queremos realizar un seguimiento del estado de la solicitud para mostrar la pantalla de carga, redirigir o mostrar un mensaje de error.
La primera idea es enviar algunas acciones internas como loading , submitted , failed para que el estado de la aplicación sea responsable de eso. Porque queremos representar el estado y no pensar en los detalles.

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

Pero aquí aparece el diablo: ahora tenemos este estado de página persistente y la próxima llamada render(state) mostrará el formulario en un estado sucio, por lo que necesitamos crear una acción cleanup para llamarlo cada vez que montemos el formulario.
Y para cada formulario que tengamos agregaremos el mismo modelo de loading , submitted , failed y cleanup acciones con tiendas/reductores… hasta que nos detengamos y pensemos sobre el estado que realmente queremos renderizar.

¿ submitted pertenece al estado de la aplicación o es solo el estado del componente, como el valor del campo name ? ¿Al usuario realmente le importa que este estado persista en un caché o quiere ver el formulario limpio cuando lo vuelva a visitar? La respuesta: depende.

Pero en el caso de formularios de envío puros, podríamos suponer que el estado del formulario no tiene nada que ver con el estado de la aplicación: queremos saber si el usuario logged in es el usuario o no en el caso de la página de inicio de sesión, o no queremos para saber si cambió la contraseña o falló: si la solicitud tiene éxito, simplemente rediríjalo a otro lugar.

Entonces, ¿qué tal la proposición de que no necesitamos este estado en ningún otro lugar excepto Componente? Hagamos un intento.
Para proporcionar una implementación más elegante, podríamos suponer que en 2015 vivimos en un mundo asincrónico y tenemos Promises todas partes, así que dejemos que nuestra acción devuelva un Promise :

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

En el siguiente paso, podríamos comenzar a realizar un seguimiento de la acción en nuestro 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')
}

Parece que tenemos todo lo que necesitamos para representar el componente sin contaminar el estado de la aplicación y eliminamos la necesidad de limpiarlo.
Así que estamos casi allí. Ahora podríamos empezar a generalizar el flujo Component .
En primer lugar, dividámonos en preocupaciones: realizar un seguimiento de la acción y las reacciones de actualización de estado:
(Hagámoslo en términos de redux para permanecer en los límites del repositorio)

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

y

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

Ahora, por un lado, tenemos decorator que proporcionan accesorios submitted y error con devolución de llamada onSubmit y, por otro lado decorator que redirige a algún lugar si obtiene la propiedad submitted === true . Adjuntémoslos a nuestro formulario y veamos qué obtenemos.

@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)

Y aquí está: forma de representación pura sin dependencias de la biblioteca $# routing 8$#$, la implementación flux , la aplicación state , haciendo lo que fue creado para: enviar y redirigir.

Gracias por leer y perdón por tomar su tiempo.

¡Lo suficientemente justo! Para ser honesto, todavía estoy interesado en ver el almacenamiento del estado del componente local en Redux: #159.

@stremlenye Eso es bastante ingenioso. ¡Gracias por compartir!

@iclanzan gracias por leer :)
En realidad ahora estoy pensando en la propuesta de @gaearon que mencionó en el comentario anterior. Eche un vistazo a la discusión n.º 159 para obtener algunas ideas significativas.

Cierre: cuando salga RR 1.0, cubriremos esto en la receta "Uso con React Router".
Esto se puede rastrear en https://github.com/rackt/redux/issues/637.

@stremlenye , ¡tu enfoque es increíble! ¿Tendría sentido, en lugar de usar un decorador, usar un componente de orden superior de enrutador de reacción en el que todos los _no autorizados_ estarían anidados y sería responsable de redirigir una vez que el almacén de autenticación tuviera un usuario?

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

Algo como esto, donde el Auth solo se encarga de detectar si el usuario ha iniciado sesión. De esta manera, todavía puedo manipular mi tienda enviando acciones desde el componente Login . No tan puro, pero menos código y más fácil de entender :)

@tomazzaman Gracias :)
La mayoría de las aplicaciones React que he escrito incluían algo como ApplicationContainer que podría usarse para el mismo propósito sin una nueva implementación Route .
Si está utilizando RR-v1.0.0-rc1 , podría alcanzar los mismos objetivos con onEnter gancho : solo verifique si el estado actual coincide con autenticado o no.

También he estado pensando en esto, y parece que @stremlenye hizo un gran trabajo mostrando cómo se puede hacer. El problema es que uno tiende a quedarse atascado en el "todo tiene que pasar por el bucle acción - reductor - actualización-componente-con-estado-de-la-tienda" (yo también) y que luego tendemos a contaminar la tienda con todo tipo de estado que solo se usa en escenarios muy específicos en la aplicación (en 1 pantalla, cosas como "ejecuté con éxito el restablecimiento de contraseña"), mientras que esto se puede manejar fácilmente simplemente haciendo las llamadas asíncronas dentro del componente y actualizando el componente en sí usando this.setState en lugar de contaminar el estado global + tener que restablecer todas esas variables de estado en caso de que tuviéramos que abrir ese componente por segunda vez más adelante. Ni siquiera mencionar la contaminación de los reductores de estado con todo el código de manejo de inicio/éxito/error.

Intentaré adaptar mi código aquí también para ver si limpia las cosas.

@ir-fuel si tendrá tiempo, ¿podría enviar un pequeño ejemplo de lo que obtendrá al github?

Bueno, será difícil hacer eso, pero puedo dar un pequeño ejemplo de lo que hice. Tengo un componente donde un usuario puede solicitar restablecer su contraseña. La operación de reinicio real ocurre así ahora:

  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 es una función que hace una llamada ajax a mi servidor.

Por lo tanto, no hay viajes de ida y vuelta a la tienda/reductores, no se contamina el estado global con cosas que solo le interesan dentro de un componente y se usa únicamente para mostrar temporalmente el estado en ese componente, y no es necesario restablecer un montón de variables de estado dentro de sus reductores.

y render se parece a esto

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

Espero que esto sea claro.

se ve bien :+1:
El único truco que debe cubrirse (desde mi perspectiva) es ajustar el middleware para devolver la promesa de la acción levantada (probablemente, solo represente el valor de retorno de la acción). Al hacer esto, permitiremos que la acción modifique el estado en un flujo normal si es necesario, manteniendo la oportunidad de conectarse a las cosas Promise en el alcance del componente.

Creo que está bien pensar en el estado de su aplicación como un estado que ya ha iniciado sesión. si ningún usuario inició sesión, ¿por qué necesita un estado de aplicación? claro, podría tener uno dentro del componente de inicio de sesión.

La lógica de la aplicación en la página de inicio de sesión no debería ser tan compleja para poseer un estado superior de la aplicación, ¿verdad?

El flujo de inicio de sesión de @SkateFreak es parte de una aplicación. ¿No pensaste en un estado de flujo de inicio de sesión (progreso, error), estado de flujo de registro, etc. para lo cual también sería mejor que usaras React (probablemente con Redux, si ya lo estás usando para otros componentes de la aplicación).

@sompylasar estoy a favor de un estado de aplicación, solo creo que mientras el usuario esté FUERA de mi aplicación, puedo servirle una aplicación más simple sin una tienda redux. mi tienda redux se activará tan pronto como INGRESES a mi aplicación.

este enfoque funcionó para mí hasta este mismo momento, pero tal vez entienda la necesidad de un estado de aplicación de fase de inicio de sesión en mis proyectos futuros

En el nivel raíz de su tienda, puede dividir su tienda en la parte de 'aplicación' y la parte de 'usuario/inicio de sesión', cada una con su propio reductor. Entonces, en ese caso, nunca contaminará los reductores de su aplicación con el código reductor de inicio de sesión y viceversa.

De: SkateFreak < [email protected] [email protected] >
Responder a: rackt /redux <[email protected] [email protected] >
Fecha: Domingo 13 Diciembre 2015 13:48
Para: rackt/redux < [email protected] [email protected] >
CC: Joris Mans < [email protected] [email protected] >
Asunto: Re: [redux] Mejores prácticas sobre el manejo del flujo de datos para páginas de inicio de sesión/registro con redireccionamiento (#297)

@sompyl asarhttps://github.com/sompylasar Estoy a favor de un estado de aplicación, solo creo que mientras el uso sea FUERA de mi aplicación, puedo servirle una aplicación más simple sin una tienda redux. mi tienda redux se activará tan pronto como INGRESES a mi aplicación.

este enfoque funcionó para mí hasta este mismo momento, pero tal vez entienda la necesidad de un estado de aplicación de fase de inicio de sesión en mis proyectos futuros

Responda a este correo electrónico directamente o véalo en Gi tHubhttps://github.com/rackt/redux/issues/297#issuecomment -164287194.

corríjame si me equivoco, pero ¿no es el propósito de una tienda de flujo compartir datos y el estado de la aplicación entre rutas y componentes? ¿Por qué necesito "reducir" mi estado y usar mis "acciones" para mantener un estado superior si todo eso no importará cuando el usuario inicie sesión?

No le veo ventajas, aunque es una forma muy ordenada y limpia de hacer las cosas. Administro mi estado previo al inicio de sesión en mi componente <Sign /> , que contiene SignIn, SignUp y ForgotPassword.

Porque registrarse puede ser un proceso más complicado que simplemente pedir un nombre de usuario y una contraseña.
Todo depende de la complejidad de su flujo.

De: SkateFreak < [email protected] [email protected] >
Responder a: rackt /redux <[email protected] [email protected] >
Fecha: Domingo 13 Diciembre 2015 14:00
Para: rackt/redux < [email protected] [email protected] >
CC: Joris Mans < [email protected] [email protected] >
Asunto: Re: [redux] Mejores prácticas sobre el manejo del flujo de datos para páginas de inicio de sesión/registro con redireccionamiento (#297)

corríjame si me equivoco, pero ¿no es el propósito de una tienda de flujo compartir datos y el estado de la aplicación entre rutas y componentes? ¿Por qué necesito "reducir" mi estado y usar mis "acciones" para mantener un estado superior si todo eso no importará cuando el usuario inicie sesión?

No le veo ventajas, aunque es una forma muy ordenada y limpia de hacer las cosas. Administro mi estado previo al inicio de sesión en mi componente, que contiene SignIn, SignUp y ForgotPassword.

Responda a este correo electrónico directamente o véalo en Gi tHubhttps://github.com/rackt/redux/issues/297#issuecomment -164287893.

estoy de acuerdo.
pero veo SignUp como un formulario, con un estado, que debería ser suficiente sin importar cuán complejo sea...

por otro lado, que se yo de inscripciones...
la gente está loca

Actualmente me he encontrado con un problema similar pero diferente.

Estoy trabajando en un flujo como un registro súper complejo, y tenemos una página continua para permitir que el usuario ingrese la información requerida, cada página es un formulario. El flujo se parece a:

FormPage_A -> FormPage_B -> FormPage_C -> SuccessPage

El problema es que no hacemos ninguna solicitud de API antes de FormPage_C. Simplemente guardamos los datos en el navegador en FormPage_A/B y los enviamos todos a la API en FormPage_C.

Entonces, la pregunta es ¿debemos poner los datos del formulario de FormPage_A y FormPage_B en estado de aplicación?

En caso afirmativo, parece que estamos guardando algunos datos temporales que no tienen efecto en la interfaz de usuario en el estado de la aplicación. Y dado que el estado de la aplicación de redux vive en la memoria, ¿qué sucederá cuando el usuario actualice la página en FormPage_B?

Si no, ¿cuál es el lugar correcto para ponerlo? ¿Deberíamos construir otra capa como fake api a partir del flujo de redux para guardarla?

@frederickfogerty @gaearon @ir-fuel @stremlenye

Solo pasaría las propiedades en this.state que son relevantes para su envío como accesorios para el siguiente paso.
entonces FormPage_A.state se pasará en FormPage_B.props etc. y se enviará en FormPage_C

o si tiene un componente principal que gestiona esto, almacene el estado en la propiedad state de ese control principal y, después del último paso, envíe todo

Solo asegúrese de que si el usuario decide volver a cargar la página, vuelva a comenzar desde el principio.

@kpaxqin Creo que en este caso podría declarar un componente de gestión como Flow que manejará el subconjunto de pasos con sus estados y la lógica genérica back-forward para cada uno.
Por lo tanto, puede hacer que sus pasos sean lo más apátridas posible y realizar un seguimiento del estado de todo el proceso en el componente adjunto.
Algo como:

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

@ir-fuel @stremlenye

Gracias por tus sugerencias.
Parece que no todos queremos poner esas cosas temporales en estado de aplicación.

Pero cuando el usuario actualiza la página en la mitad del flujo

Solo asegúrese de que si el usuario decide volver a cargar la página, vuelva a comenzar desde el principio.

Parece que no es una buena experiencia para el usuario, somos un sitio móvil (esa es una de las razones por las que dividimos el formulario grande en páginas pequeñas), es doloroso ingresar cosas en el móvil y creo que el usuario se volverá loco si pierde todo el cosas después de presionar el botón Actualizar, tal vez simplemente lo abandonen.

Creo que tal vez construir el fake api a partir del flujo de redux es una opción aceptable, enviar cosas al fake api en cada paso y recuperarlas cuando sea necesario al igual que tenemos una API para ello, y dado que usamos es como api, lo único que se conecta a él es que los creadores de acciones, las páginas y los componentes estarán limpios y también es fácil de manejar el error (use el código común para manejar el error de api).

Tiene sentido para mí porque una vez que queremos persistir esos datos, no debería estar en el estado del componente o en el estado de la aplicación, el efecto secundario debe manejarse en el creador de la acción.

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

Me aclara las cosas, pero parece extraño agregar algo de nuestro flujo de redux en nuestra aplicación.

Realmente no me importaría que el usuario recargara tu página.
Por otro lado, nos dice que tiene un proceso de registro complicado con varios formularios, y luego dice que es un sitio móvil. ¿No crees que también enloquecerá a los usuarios y hará que se rindan si tienen que completar demasiadas cosas en un dispositivo móvil? Creo que es incluso peor que no admitir la recarga. De todos modos, no creo que muchos sitios admitan la recarga de la página en medio de un proceso de registro.

Creo que solo está complicando mucho más las cosas para algo que realmente no sucede mucho. Me concentraría más en tratar de simplificar el proceso de registro, o dividirlo en varios pasos en su sitio web para que el usuario no tenga la impresión de que está completando X páginas de formularios antes de finalmente activar su cuenta.

Y si realmente desea admitir la recarga a mitad del registro, simplemente haga que el componente principal que controla todos estos pasos almacene su estado en localStorage .

@kpaxqin Iría con el enfoque de "API falsa". Por cierto, ¿por qué falsificar? Es solo una API de almacenamiento intermedio que necesita para su proceso de varios pasos. No todas las API deben estar necesariamente en un servidor y acceder a ellas a través de HTTP u otro protocolo de red.

En lo que estoy de acuerdo con @ir-fuel es que se debe almacenar todo el estado del proceso de varios pasos, no solo el paso actual.

@ir-fuel Reload podría ocurrir sin la intención explícita del usuario, por ejemplo, un usuario puede cambiar a otra aplicación móvil, como una computadora portátil o una llamada telefónica, y el navegador podría perder su estado debido a la falta de memoria en el dispositivo, por lo que se recargará cuando el usuario regrese.

El problema nuevamente es que está contaminando el estado de la aplicación global con algo que solo necesita en algunas pantallas en su aplicación web.

@sompylasar @kpaxqin Creo que @ir-fuel sugirió el enfoque correcto: si necesita algo intermedio para persistir eventualmente, persista en el lado del cliente: mucho más fácil de implementar, realmente no involucra al servidor en la persistencia de datos inconsistentes, no lo hace No realiza ninguna solicitud http (lo que también es un problema importante en los dispositivos móviles) y es extremadamente fácil cambiar el enfoque en el futuro.

Y sobre la persistencia de cualquier dato en algún lugar, piense cómo lo limpiará después de que el proceso falle o ya haya terminado, ese fue el tema inicial de un tema.
Otra opción que podría usar: conservar los datos del usuario en la URL. Si sus formularios consisten en un par de campos de texto, esa podría ser una buena solución de compensación.

No se trata tanto de "almacenarlo del lado del cliente o del servidor", es más una cuestión de "manejo este 'ad hoc' en mi clase de componente principal" frente a "hago toda la acción/reductor/baile de estado".
No me gusta pasar por la acción/reductores si es para un estado temporal. Solo hago esto para el estado real de la aplicación.

@ir-fuel :+1:

@stremlenye Por favor, lea mi respuesta nuevamente. Digo explícitamente que no es necesario crear una API del lado del servidor para crear una capa de API de persistencia.

@ir-fuel No pasar por acciones/reductor cancela la previsibilidad y la capacidad de reproducción de las transiciones de estado.

@ir-fuel Puede borrar fácilmente el estado de su aplicación después de enviar el formulario, para que no se "contamine".

RE: limpieza. Haga una acción BORRAR y tenga un botón en su interfaz de usuario para restablecer el flujo de registro. También puede hacerlo a través del enrutador (envíe esa acción cuando visite la URL de registro inicial y, si el registro ha comenzado, cambie la URL a otra cosa).

@sompylasar no hay problema para restablecer su estado con un botón explícito: los problemas ocurren después de fallas no controladas: es difícil predecir las formas en que el estado podría corromperse y definir una estrategia de restauración para cada caso particular.

@stremlenye sí, la ingeniería de software es difícil a veces. Los estados y transiciones predecibles ayudan a mitigar esto.

No solo eso, sino que si su estado es más que un montón de objetos json (que odio porque todo es por convención y no se aplica nada) y usa algo en la línea de Record de Immutable.js, entonces necesita declarar las propiedades de sus objetos por adelantado y eso incluye todas las cosas temporales que siempre querrá allí.

Para mí, construir algo con redux sin usar Immutable.js es solo un accidente esperando que suceda.

Sé que podrías hacer un registro Signup y solo que no sea nulo al registrarte, pero el problema es más fundamental, porque podrías tener muchos de esos casos en tu aplicación web, y eso es lo que molesta me.

@ir-combustible

No solo eso, sino que si su estado es más que un montón de objetos json (que odio porque todo es por convención y no se aplica nada) y usa algo en la línea de Immutable.js 'Record, entonces necesita declarar las propiedades de sus objetos por adelantado y eso incluye todas las cosas temporales que siempre querrá allí.

Para mí, construir algo con redux sin usar Immutable.js es solo un accidente esperando que suceda.

Por ahora, no estoy convencido de usar Immutable.js con redux, parecía demasiado complejo para integrar (y no funciona). Una congelación profunda durante el desarrollo y las pruebas, y un conjunto de pruebas deberían ser suficientes para cubrir la mayoría de los casos. Y una convención estricta para evitar la mutación de estado en reductores u otros lugares donde tenga la referencia.

Sé que podría hacer un Registro de registro y solo hacer que no sea nulo al registrarse

Sí, eso es lo que tenía en mente.

pero el problema es más fundamental, porque podrías tener muchos de esos casos en tu webapp, y eso es lo que me molesta.

Eso es lo que llamamos estado explícito de la aplicación: en cada momento sabemos el estado de la aplicación que consta de muchos componentes. Si desea que la estructura estatal sea menos explícita y más dinámica, siga https://github.com/tonyhb/redux-ui

Ese es todo el problema con el desarrollo de JS. “Apeguémonos a las convenciones”. Y es por eso que las personas comienzan a darse cuenta de que simplemente no funciona en entornos complejos, porque las personas cometen errores y vienen con soluciones como TypeScript e Immutable.js.

¿Por qué sería demasiado complicado de integrar? Si usa 'Record', usa todos esos objetos (siempre que solo los esté leyendo) como objetos JS normales, con notación de puntos para acceder a las propiedades, con la ventaja de que declara por adelantado qué propiedades son parte de su objeto .

Estoy haciendo todo mi desarrollo redux con Immutable.js, sin ningún problema. El hecho de que simplemente no pueda modificar un objeto por accidente es una gran ventaja en redux y la 'seguridad de tipo' de definir los prototipos de su objeto también es conveniente.

Y si realmente desea admitir la recarga a mitad de registro, simplemente haga que el componente principal que controla todos estos pasos almacene su estado en localStorage.

@ir-combustible
Para mí, poner cosas en localStorage parece un efecto secundario y no parece una buena práctica hacerlo en componentes.

El problema nuevamente es que está contaminando el estado de la aplicación global con algo que solo necesita en algunas pantallas en su aplicación web.

Tal vez no he descrito la API falsa lo suficientemente clara, la razón más importante para construirla es que no quiero poner esas cosas only need in a few screens en el estado de la aplicación de redux. En FormPage_A/B simplemente llaman a la API para almacenar los datos del formulario y luego van al siguiente paso:

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

y en FormPage_C (la forma final):

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

Todos esos datos temporales viven en una API falsa y el estado de la aplicación global no ha sido contaminado. Si queremos borrar datos en una API falsa para reiniciar el flujo, simplemente envíe la acción para hacerlo.

@sompylasar no hay problema para restablecer su estado con un botón explícito: los problemas ocurren después de fallas no controladas: es difícil predecir las formas en que el estado podría corromperse y definir una estrategia de restauración para cada caso particular.

@stremlenye Estoy de acuerdo con eso, pero creo que es más probable que esos casos de fallas en particular sean asuntos comerciales, siempre están aquí sin importar dónde coloquemos el estado, y deben manejarse de manera correcta, pero no se describen con suficiente claridad en el momento. Lo único que pueden hacer los desarrolladores es facilitar el seguimiento para que podamos solucionarlo con menos dolor.

Y poner el estado en el componente principal puede hacer que sea más difícil razonar sobre cuándo las cosas se vuelven complejas. Con fake api mutamos ese estado con acciones y parece más predecible.

Pero estoy de acuerdo en que la API falsa parece demasiado pesada en la mayoría de los casos con menos complejidad.

No revisé todos los mensajes anteriores. Aquí solo doy mi enfoque: detectar cambios de accesorios en componentWillReceiveProps.

Digamos que la forma del estado es: { loginPending, loginError }, cuando comience a iniciar sesión, configure loginPending = true. Cuando tenga éxito o falle, configure { loginPending: false, loginError: 'some error.' }.

Entonces puedo detectar si el evento de inicio de sesión fue exitoso en un componente y redirigir la página:

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

Simplemente funciona, aunque no es hermoso.

Mi humilde implementación con respecto a borrar el formulario en la redirección usando react-router-redux . Reductor:

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

¿Crees que este enfoque está bien? Funciona.

Terminé con esto. Di lo que piensas sobre:

registrar acción 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));
};

contenedor de formulario de registro:

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

y ayudantes:

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

Lo que es importante: solo el registro exitoso ingresa a la tienda Redux como acción; de lo contrario, el error pasa al estado del componente del contenedor. El usuario recibe un error, luego navega a otra página, luego nuevamente a la página del formulario: el formulario está vacío y no tiene errores. No hay acciones "USER_REGISTRATION_REQUEST" o "USER_REGISTRATION_FAILURE" porque describen el estado del componente, no el estado de la aplicación.

@sunstorymvp

Ha endulzado su código con async / await , y este azúcar oculta el hecho de que hay tres estados de la solicitud (antes, en curso, después). Estos estados se ocultan en el componente de vista. El inicio de la solicitud se ignora y el error de la solicitud se maneja en la vista y no se envía a través de redux, por lo que el estado de la aplicación está fuera de servicio aquí. Si por alguna razón el componente que contiene este estado se desmonta y luego se vuelve a montar, el estado de la solicitud de registro se perderá y su resultado no se manejará, y esto podría generar errores posteriores (por ejemplo, el usuario ya se registró).

El usuario recibe un error, luego navega a otra página, luego nuevamente a la página del formulario: el formulario está vacío y no tiene errores.

Para lograrlo, puede enviar una acción como USER_REGISTRATION_RESET en el montaje del componente que restablecería el error en la tienda. Esta acción solo debe manejarse si no hay una solicitud de registro pendiente; de ​​lo contrario, la aplicación no manejará el resultado de la solicitud pendiente.

Gracias por la retroalimentación.

Ha endulzado su código con async/await, y este azúcar oculta el hecho de que hay tres estados de la solicitud (antes, en curso, después)

estas condiciones existen independientemente de si uso async/await o no.

Estos estados se ocultan en el componente de vista

según lo previsto, ese es el estado del componente ... ¿por qué esto debería estar en el estado de la aplicación?

Se ignora el inicio de la solicitud.

en curso...

y el error de solicitud se maneja en la vista y no se envía a través de redux

Como era la intención

Si por alguna razón el componente que contiene este estado se desmonta y luego se vuelve a montar, el estado de la solicitud de registro se perderá y su resultado no se manejará.

eso no es verdad. el usuario será registrado y redirigido si la solicitud tiene éxito, independientemente del estado del componente o incluso de su existencia. mire el creador de la acción register (handleData).

Para lograr eso, puedes enviar una acción...

Ese es el punto. ¿Otra acción? ¿Por qué tanto repetitivo? Deshabilite o habilite el botón de envío, muestre errores de formulario o validación en vivo, muestre el control giratorio mientras la solicitud está abierta... y estas acciones conducen a nuevas acciones que restauran el estado predeterminado... ¿por qué esto tiene que estar en el estado de la aplicación?

eso no es verdad. el usuario será registrado y redirigido si la solicitud tiene éxito, independientemente del estado del componente o incluso de su existencia. mire el creador de la acción de registro (handleData).

Sí, pero esto deja la posibilidad de dos solicitudes posteriores register porque el estado de la solicitud no se rastrea en el estado de la aplicación.

Deshabilite o habilite el botón de envío, muestre errores de formulario o validación en vivo, muestre el control giratorio mientras la solicitud está abierta... y estas acciones conducen a nuevas acciones que restauran el estado predeterminado... ¿por qué esto tiene que estar en el estado de la aplicación?

Para la previsibilidad y la rejugabilidad, este es el propósito de Redux. Al tener solo las acciones y el estado inicial, puede hacer que la aplicación entre en un estado determinado solo enviando acciones. Si un componente comienza a contener algún estado, el estado de la aplicación comienza a dividirse, no se convierte en un único árbol de estado que represente el estado de la aplicación.

Sin embargo, no seas demasiado dogmático al respecto. Hay muchas razones para poner cosas en estado Redux y muchas razones para dejar algo en estado de componente local, dependiendo de su escenario. Consulte http://redux.js.org/docs/FAQ.html#organizing -state-only-redux-state y https://news.ycombinator.com/item?id=11890229.

Ok, ahora me queda claro: depende. ¡Gracias!

Uso componentWillReceiveProps y tengo un objeto auth en mi componente. No estoy seguro si esto ayuda en absoluto.

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

¿Hay algo de malo en hacerlo de esta manera?

¿Qué pasa con ese enfoque http://codereview.stackexchange.com/questions/138296/implementing-redirect-in-redux-middleware?
Pasar la URL de redireccionamiento a la acción y realizar una llamada adicional al método next con push desde react-router-redux en el middleware.

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

Temas relacionados

jbri7357 picture jbri7357  ·  3Comentarios

jimbolla picture jimbolla  ·  3Comentarios

rui-ktei picture rui-ktei  ·  3Comentarios

cloudfroster picture cloudfroster  ·  3Comentarios

benoneal picture benoneal  ·  3Comentarios