Redux: Prática recomendada para lidar com o fluxo de dados para páginas de login/inscrição com redirecionamento

Criado em 22 jul. 2015  ·  66Comentários  ·  Fonte: reduxjs/redux

EDIT: o que estou tentando entender aqui é como lidar com o fluxo de dados quando você tem uma página com um formulário. Depois de enviar o formulário, você redireciona para outra página ou exibe uma mensagem de erro .

O desafio é onde essas informações devem ser _armazenadas temporariamente_ no fluxo de dados. Porque depois de redirecionar ou renderizar a mensagem, o estado (ou informações que você armazenou) não é mais relevante e deve ser redefinido ou limpo.


Em um aplicativo da web de back-end, você geralmente tem páginas como login, inscrição, redefinição de senha etc.

Quando o usuário navega até o aplicativo e não está logado, ele é redirecionado para a página de login. Isso pode ser feito facilmente com o gancho RR onEnter .

Em seguida, o usuário preencherá o formulário e o enviará. Nesse ponto, geralmente, se a solicitação foi bem-sucedida, você deseja redirecionar para a raiz do aplicativo ou para alguma outra página "segura". Se falhar, você deseja permanecer na página e mostrar uma mensagem de erro.

(Isso se aplica não apenas à página de login, mas a outras páginas, bem como as mencionadas anteriormente. Vou me concentrar apenas no login por enquanto)

Até agora você tem este fluxo (que é um caso de uso comum):

  • usuário vá para /
  • redirecionar para /login
  • preencha o formulário e envie
  • se ok redirecionar para / (ou alguma outra página)
  • se falhar, fique no formulário de login e mostre a mensagem de erro

Se implementarmos esse fluxo com redux, acho que tenho a seguinte configuração:

// 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 fluxo está correto - espero :) - você pode ver que eu tenho 2 sinalizadores no estado do aplicativo: shouldRedirect e errorMessage .
Esses sinalizadores são usados ​​apenas ao enviar o formulário.

Primeira pergunta: tudo bem ter esses sinalizadores no estado do aplicativo?

Outra coisa a considerar é que eu tenho que "redefinir" esses sinalizadores quando redirecionar porque se eu quiser ir para outra página (por exemplo, inscrição, ...) devo ter um estado limpo ( { shouldRedirect: false, errorMessage: null } ).

Então, talvez eu queira despachar outra ação 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(...)  
  }
}

Alguém já teve esse problema? Estou faltando alguma coisa ou essa é a maneira "correta" de fazer isso?

Há alguma outra alternativa?

Qualquer feedback é muito bem vindo! :)

discussion docs

Comentários muito úteis

Eu acho que a questão é um pouco mais profunda do que apenas generalizar 'enviar formulários' – é mais sobre o que realmente é o estado e quais dados devem aparecer em store e quais não.

Eu gosto de pensar em View parte do fluxo de dados de fluxo como função pura render(state): DOM mas não é totalmente verdade.
Vamos tomar como exemplo um componente de formulário simples que renderiza dois 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>
    )
  }
}

Durante a digitação nestes campos foram realizadas algumas modificações de estado interno, então teremos algo assim no final:

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

Então, temos algum estado que não está vinculado ao estado do aplicativo e ninguém se importa com isso. Quando revisitarmos este formulário na próxima vez, ele será mostrado apagado e o 'John Snow' desaparecerá para sempre.
Parece que modificamos o DOM sem tocar no fluxo de dados de fluxo.

Vamos supor que este é um formulário de inscrição para alguma carta de notificação. Agora devemos interagir com o mundo exterior e faremos isso usando nossa ação 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>
    )
  }
}

Quando o usuário pressiona o botão de envio, ele espera qualquer resposta da interface do usuário e o que fazer a seguir. Portanto, queremos acompanhar o estado da solicitação para renderizar a tela de carregamento, redirecionar ou mostrar a mensagem de erro.
A primeira ideia é despachar algumas ações internas como loading , submitted , failed para tornar o estado do aplicativo responsável por isso. Porque queremos renderizar o estado e não pensamos em detalhes.

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

Mas aqui o diabo aparece: agora temos esse estado de página persistido e a próxima chamada render(state) exibirá o formulário em um estado sujo, então precisamos criar a ação cleanup para chamá-lo toda vez que montarmos o formulário.
E para cada formulário que temos vamos adicionar o mesmo clichê de ações loading , submitted , failed e cleanup com lojas/redutores… até pararmos e pensarmos sobre o estado que realmente queremos renderizar.

submitted pertence ao estado do aplicativo ou é apenas o estado do componente, como o valor do campo name . O usuário realmente se importa com esse estado a ser persistido em um cache ou ele deseja ver o formulário limpo ao revisitá-lo? A resposta: depende.

Mas no caso de formulários de envio puro, poderíamos supor que o estado do formulário não tem nada a ver com o estado do aplicativo: queremos saber se o fato é o usuário logged in ou não no caso da página de login, ou não queremos para saber alguma coisa sobre ele mudou a senha ou falhou: se a solicitação for bem-sucedida, basta redirecioná-lo para outro lugar.

Então, que tal a proposição de que não precisamos desse estado em nenhum outro lugar, exceto Componente? Vamos tentar.
Para fornecer uma implementação mais sofisticada, poderíamos supor que em 2015 vivemos no mundo assíncrono e temos Promises em todos os lugares, então deixe nossa ação retornar um Promise :

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

Na próxima etapa, poderíamos começar a acompanhar a ação em nosso 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 temos tudo o que precisamos para renderizar o componente sem poluir o estado do aplicativo e nos livramos da necessidade de limpá-lo.
Então estamos quase lá. Agora podemos começar a generalizar o fluxo Component .
Antes de tudo, vamos dividir as preocupações: acompanhar a ação e as reações de atualização do estado:
(Vamos fazer isso em termos de redux para ficar dentro dos limites do 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} />)
    }
  })
}

e

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

Agora, por um lado, temos decorator que fornecem submitted e error com retorno de chamada onSubmit e, por outro lado decorator que redireciona para algum lugar se obtém a propriedade submitted === true . Vamos anexá-los ao nosso formulário e ver o que temos.

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

E aqui está: forma de renderização pura sem dependências da biblioteca routing #$8$#$, implementação flux , aplicação state , fazendo o que foi criado para: enviar e redirecionar.

Obrigado por ler e desculpe por tomar seu tempo.

Todos 66 comentários

Em vez de ter shouldRedirect lá, que tal ter um redutor de sessão, que apenas rastreia isLoggedIn ? Dessa forma, você pode redirecionar da sua página de login para qualquer outra página marcando isLoggedIn em vez de shouldRedirect .

Além disso, redefinir o estado pode ser feito apenas processando a ação LOGGED_IN em seu redutor. Se essa ação acontecer, você sabe que pode redefinir com segurança o estado de erro para um null .

Isso faz sentido? :D

Faz sentido se houver apenas a página de login, mas é uma abordagem um pouco mais genérica. Por exemplo, para uma página de inscrição ou página de redefinição de senha. Mesmo se você estiver logado, ainda poderá ver essas páginas.

E esses sinalizadores devem ser usados ​​por todas essas páginas, porque todas têm o mesmo fluxo.

Espero que tenha ficado mais claro agora :)

Sim, do meu ponto de vista "shouldRedirect" é muito genérico para ser associado a uma determinada ação. Pode abrir bugs onde você de alguma forma esquece de definir esse sinalizador e, em seguida, uma página redireciona para outra página para a qual ela realmente não deveria redirecionar.

Mas claro, se você quiser limpar seu estado, basta fazer isso com uma ação. Ou você pode ouvir uma mudança de histórico fora do react e acionar a ação de limpeza de estado. Isso pode ser preferível porque você não precisa espalhar essa lógica por todos os seus componentes, mas pode mantê-la em um único lugar.

Mas essa é apenas uma solução possível para a compensação estadual. Ainda não tenho certeza de como resolver o redirecionamento de uma maneira agradável.

A questão também é: faz sentido ter essas bandeiras no estado? Se não, como esse fluxo deve ser implementado?

@gaearon , se você tiver algum tempo, adoraria ouvir seus comentários também. Obrigado! :+1:

Na minha aplicação estou redirecionando no middleware e não uso sinalizadores. Mas eu não estou 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);
    };
}

Não é refatorado, é destinado apenas para fins de teste da solução do meu roteador :).

Esta é uma cópia e colagem do meu aplicativo e como lido com o carregamento de dados do usuário e o login. Tentarei comentar o código da melhor maneira possível e estarei no reactiflux para perguntas.

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)

Em termos de responder às suas perguntas @emmenko , acho que ter shouldRedirect no seu estado não é aceitável e deve ser feito de uma maneira mais funcional. Ter errorMessage no estado do aplicativo é bom, apenas certifique-se de que esteja em um estado de login, como state.auth.errorMessage , em vez de um errorMessage de nível superior

Acho que o shouldRedirect em seu estado não é aceitável e deve ser feito em uma abordagem mais funcional

Eu concordo. Coisas que só são úteis uma vez não deveriam estar no estado IMO.

Além disso, de acordo com o slack, não acho que você deva armazenar isLoggedIn no estado do aplicativo, pois é mais uma propriedade computada, que pode ser calculada com um seletor simples como const isUserLoggedIn = state => state.user.id !== null

Veja https://github.com/faassen/reselect para mais uso do seletor

@frederickfogerty obrigado pelo exemplo.

Porém, pergunta: isso funciona bem para login porque você geralmente tem um sinalizador loggedIn no state . Mas e outras páginas, como inscrição, redefinição de senha, etc?
Essas páginas se comportam da mesma forma que o login: redireciona se estiver ok, caso contrário mostra mensagem de erro.
Então, como você lida com esses casos? Estou lutando para encontrar o fluxo correto para esses casos e não tenho certeza de onde o estado deve morar.

Coisas que só são úteis uma vez não deveriam estar no estado IMO.

@gaearon Eu concordo, mas ainda não tenho certeza de qual é a abordagem correta para isso. Você ainda precisa ter um estado em algum lugar...

É assim que estou fazendo atm sem armazenar nada no 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 }))
}

Eu não acho que você deva armazenar isLoggedIn no estado do aplicativo, pois é mais uma propriedade computada

Sim, faz sentido.

@gaearon Acho que a pergunta para esse fluxo de trabalho geral (redirecionar ou mostrar erro) é se deve ser feito com "redux" ou de uma maneira diferente. E qual seria essa alternativa?

relacionado e vou fixá-lo aqui ... na verdade, eu também estava lidando com esse problema e atualmente tentando reimplementar o seguinte exemplo no Redux. Nada mostrável ainda porque estou fazendo isso no meu tempo livre:
https://auth0.com/blog/2015/04/09/adding-authentication-to-your-react-flux-app/

É fluxo puro:
https://github.com/auth0/react-flux-jwt-authentication-sample

Talvez seja útil para as melhores práticas?!

@emmenko

Então, como você lida com esses casos?

Contanto que seu uso esteja fazendo login, isLoggedIn (que não está no estado do aplicativo, mas menor) ainda será alterado e isso os redirecionará. Não importa de onde veio essa mudança de estado :)

A melhor abordagem que tomei é adicionar essa lógica a um conector onde a funcionalidade é necessária para seus filhos, por exemplo, para autorização, digamos que você tenha um componente AppProtected , onde tudo que é filho desse componente precisa de autorização , você coloca essa lógica no conector do componente AppProtected.

É assim que estou fazendo atm sem armazenar nada no estado.

Eu definitivamente não gosto dessa abordagem, que tem fluxo de dados bidirecional entre a interface do usuário e os ACs, o que vai contra o fluxo. Se você está vinculado a fazê-lo imperativamente, você pode fazer 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')
  }
}
  • Se o login do usuário for bem-sucedido, ele será redirecionado para fora da página relevante.
  • Se o login do usuário falhar, ele não está logado e permanece na mesma página (nada acontece)

Isso funciona para todas as páginas que você mencionou

Desculpe, mas ainda não vejo como lido com o redirecionamento / renderização de erro.

Vamos dar um passo atrás e dizer que tenho a página reset-password . Também assumimos que não devemos tocar em nenhum estado armazenando algum tipo de sinalizador único (como mencionado anteriormente).
Quando clico para enviar o formulário, o que deve acontecer?

Isto é o que eu gostaria de saber, como deve ser o fluxo e onde deve viver.

PS: obrigado pelo seu feedback até agora!

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

Isso não é nem de longe tão elegante quanto as páginas de login e inscrição, pois é muito acoplada

Eu tenho que dormir, responderei mais perguntas amanhã

Então estamos armazenando as bandeiras no estado novamente ;)

P.S.: boa noite!

Eu acho que a questão é um pouco mais profunda do que apenas generalizar 'enviar formulários' – é mais sobre o que realmente é o estado e quais dados devem aparecer em store e quais não.

Eu gosto de pensar em View parte do fluxo de dados de fluxo como função pura render(state): DOM mas não é totalmente verdade.
Vamos tomar como exemplo um componente de formulário simples que renderiza dois 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>
    )
  }
}

Durante a digitação nestes campos foram realizadas algumas modificações de estado interno, então teremos algo assim no final:

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

Então, temos algum estado que não está vinculado ao estado do aplicativo e ninguém se importa com isso. Quando revisitarmos este formulário na próxima vez, ele será mostrado apagado e o 'John Snow' desaparecerá para sempre.
Parece que modificamos o DOM sem tocar no fluxo de dados de fluxo.

Vamos supor que este é um formulário de inscrição para alguma carta de notificação. Agora devemos interagir com o mundo exterior e faremos isso usando nossa ação 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>
    )
  }
}

Quando o usuário pressiona o botão de envio, ele espera qualquer resposta da interface do usuário e o que fazer a seguir. Portanto, queremos acompanhar o estado da solicitação para renderizar a tela de carregamento, redirecionar ou mostrar a mensagem de erro.
A primeira ideia é despachar algumas ações internas como loading , submitted , failed para tornar o estado do aplicativo responsável por isso. Porque queremos renderizar o estado e não pensamos em detalhes.

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

Mas aqui o diabo aparece: agora temos esse estado de página persistido e a próxima chamada render(state) exibirá o formulário em um estado sujo, então precisamos criar a ação cleanup para chamá-lo toda vez que montarmos o formulário.
E para cada formulário que temos vamos adicionar o mesmo clichê de ações loading , submitted , failed e cleanup com lojas/redutores… até pararmos e pensarmos sobre o estado que realmente queremos renderizar.

submitted pertence ao estado do aplicativo ou é apenas o estado do componente, como o valor do campo name . O usuário realmente se importa com esse estado a ser persistido em um cache ou ele deseja ver o formulário limpo ao revisitá-lo? A resposta: depende.

Mas no caso de formulários de envio puro, poderíamos supor que o estado do formulário não tem nada a ver com o estado do aplicativo: queremos saber se o fato é o usuário logged in ou não no caso da página de login, ou não queremos para saber alguma coisa sobre ele mudou a senha ou falhou: se a solicitação for bem-sucedida, basta redirecioná-lo para outro lugar.

Então, que tal a proposição de que não precisamos desse estado em nenhum outro lugar, exceto Componente? Vamos tentar.
Para fornecer uma implementação mais sofisticada, poderíamos supor que em 2015 vivemos no mundo assíncrono e temos Promises em todos os lugares, então deixe nossa ação retornar um Promise :

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

Na próxima etapa, poderíamos começar a acompanhar a ação em nosso 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 temos tudo o que precisamos para renderizar o componente sem poluir o estado do aplicativo e nos livramos da necessidade de limpá-lo.
Então estamos quase lá. Agora podemos começar a generalizar o fluxo Component .
Antes de tudo, vamos dividir as preocupações: acompanhar a ação e as reações de atualização do estado:
(Vamos fazer isso em termos de redux para ficar dentro dos limites do 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} />)
    }
  })
}

e

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

Agora, por um lado, temos decorator que fornecem submitted e error com retorno de chamada onSubmit e, por outro lado decorator que redireciona para algum lugar se obtém a propriedade submitted === true . Vamos anexá-los ao nosso formulário e ver o que temos.

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

E aqui está: forma de renderização pura sem dependências da biblioteca routing #$8$#$, implementação flux , aplicação state , fazendo o que foi criado para: enviar e redirecionar.

Obrigado por ler e desculpe por tomar seu tempo.

Justo! Para ser honesto, ainda estou interessado em armazenar o estado do componente local no Redux: #159.

@stremlenye Isso é muito esperto. Obrigado por compartilhar!

@iclanzan obrigado pela leitura :)
Na verdade, agora estou pensando na proposta do @gaearon que ele mencionou no comentário anterior. Dê uma olhada na discussão nº 159 para obter algumas ideias significativas.

Fechando – quando o RR 1.0 for lançado, abordaremos isso na receita “Uso com React Router”.
Isso pode ser rastreado em https://github.com/rackt/redux/issues/637.

@stremlenye sua abordagem é incrível! Faria sentido - em vez de usar um decorador - usar um componente de ordem superior do roteador de reação no qual todos os _não autorizados_ seriam aninhados e seria responsável por redirecionar uma vez que o armazenamento de autenticação tivesse um usuário?

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

Algo assim, onde o Auth só se encarrega de detectar se o usuário está logado. Dessa forma, ainda consigo manipular minha loja despachando ações do componente Login . Não tão puro, mas menos código e mais fácil de entender :)

@tomazzaman Obrigado :)
A maioria dos aplicativos React que escrevi incluíam algo como ApplicationContainer que poderia ser usado para o mesmo propósito sem uma nova implementação Route .
Se você estiver usando RR-v1.0.0-rc1 , poderá alcançar os mesmos objetivos com o gancho onEnter : apenas verifique se o estado atual corresponde ao autenticado ou não.

Também tenho pensado nisso e parece que @stremlenye fez um ótimo trabalho mostrando como isso pode ser feito. O problema é que a pessoa tende a ficar presa no loop "tudo precisa passar pelo loop de ação - redutor - atualização-componente-com-store-state" (eu também) e então tendemos a poluir a loja com todos os tipos de estado que é usado apenas em cenários muito específicos no aplicativo (em 1 tela, coisas como "executei com sucesso a senha de redefinição"), enquanto isso pode ser facilmente manipulado apenas fazendo as chamadas assíncronas dentro do componente e atualizando o próprio componente usando this.setState em vez de poluir o estado global + ter que redefinir todas essas vars de estado no caso de abrirmos esse componente uma segunda vez mais tarde. Sem mencionar a poluição dos redutores de estado com todo o código de início/sucesso/manuseio de erros.

Vou tentar adaptar meu código aqui também para ver se ele limpa as coisas.

@ir-fuel, se tiver tempo, você poderia enviar um pequeno exemplo do que você obterá no github?

Bem, vai ser difícil fazer isso, mas posso dar um pequeno exemplo do que fiz. Eu tenho um componente onde um usuário pode pedir para redefinir sua senha. A operação de redefinição real acontece assim agora:

  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 é uma função que faz uma chamada ajax para o meu servidor.

Portanto, sem viagens de ida e volta para a loja/redutores, sem poluir o estado global com coisas com as quais você só se importa dentro de um componente e é puramente usado para mostrar temporariamente o estado nesse componente, e não há necessidade de redefinir um monte de vars de estado dentro de seus redutores.

e render parece algo assim

  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 isso esteja claro.

Parece bom :+1:
O único truque que deve ser abordado (da minha perspectiva) é ajustar o middleware para retornar a promessa da ação levantada (apenas proxy do valor de retorno da ação provavelmente). Fazendo isso, permitiremos que a ação modifique o estado em um fluxo normal, se necessário, mantendo a oportunidade de conectar-se ao material Promise no escopo do componente.

acho que não há problema em pensar no estado do seu aplicativo como um estado já conectado. se nenhum usuário estiver logado, por que você precisa de um estado de aplicativo? claro, você poderia ter um dentro do componente Login.

a lógica do aplicativo na página de login não deve ser tão complexa para possuir um estado superior do aplicativo, certo?

O fluxo de login do @SkateFreak faz parte de um aplicativo. Você não pensou em um estado de fluxo de login (progresso, erro), estado de fluxo de inscrição etc. para o qual seria melhor usar o React também (provavelmente com Redux, se você já estiver usando para outros componentes do aplicativo).

@sompylasar sou a favor de um estado de aplicativo, só acho que, desde que o usuário esteja FORA do meu aplicativo, posso servir a ele um aplicativo mais simples sem uma loja redux. minha loja redux entrará em ação assim que você ENTRAR no meu aplicativo.

essa abordagem funcionou para mim até este momento, mas talvez eu entenda a necessidade de um estado de aplicativo de fase de login em meus projetos futuros

Você pode no nível raiz de sua loja dividir sua loja na parte 'app' e na parte 'usuário/login', cada uma com seu próprio redutor. Portanto, nesse caso, você nunca poluirá seus redutores de aplicativos com código redutor de login e vice-versa.

De: SkateFreak < [email protected] [email protected] >
Responder para: rackt/redux < [email protected] [email protected] >
Data: domingo, 13 de dezembro de 2015 13:48
Para: rackt/redux < [email protected] [email protected] >
Cc: Joris Mans < [email protected] [email protected] >
Assunto: Re: [redux] Prática recomendada para lidar com fluxo de dados para páginas de login/inscrição com redirecionamento (#297)

@sompyl asarhttps://github.com/sompylasar sou a favor de um estado de aplicativo, só acho que desde que o uso seja FORA do meu aplicativo, posso servir a ele um aplicativo mais simples sem uma loja redux. minha loja redux entrará em ação assim que você ENTRAR no meu aplicativo.

essa abordagem funcionou para mim até este momento, mas talvez eu entenda a necessidade de um estado de aplicativo de fase de login em meus projetos futuros

Responda a este e-mail diretamente ou visualize-o no Gi tHubhttps://github.com/rackt/redux/issues/297#issuecomment -164287194.

corrija-me se estiver errado, mas o objetivo de um armazenamento de fluxo não é compartilhar dados e estado do aplicativo entre rotas e componentes? por que preciso "reduzir" meu estado e usar minhas "ações" para manter um estado superior se tudo isso não importa quando o usuário entrar?

não vejo vantagens, embora seja uma forma muito organizada e limpa de fazer as coisas. eu gerencio meu estado de pré-login no meu componente <Sign /> , que contém SignIn, SignUp e ForgotPassword.

Porque uma inscrição pode ser um processo mais complicado do que apenas pedir um nome de usuário e uma senha.
Tudo depende da complexidade do seu fluxo.

De: SkateFreak < [email protected] [email protected] >
Responder para: rackt/redux < [email protected] [email protected] >
Data: Domingo, 13 de dezembro de 2015 14:00
Para: rackt/redux < [email protected] [email protected] >
Cc: Joris Mans < [email protected] [email protected] >
Assunto: Re: [redux] Prática recomendada para lidar com fluxo de dados para páginas de login/inscrição com redirecionamento (#297)

corrija-me se estiver errado, mas o objetivo de um armazenamento de fluxo não é compartilhar dados e estado do aplicativo entre rotas e componentes? por que preciso "reduzir" meu estado e usar minhas "ações" para manter um estado superior se tudo isso não importa quando o usuário entrar?

não vejo vantagens, embora seja uma forma muito organizada e limpa de fazer as coisas. eu gerencio meu estado de pré-login no meu componente, que contém SignIn, SignUp e ForgotPassword.

Responda a este e-mail diretamente ou visualize-o no Gi tHubhttps://github.com/rackt/redux/issues/297#issuecomment -164287893.

eu concordo.
mas vejo SignUp como um formulário, com um estado - o que deve ser suficiente, não importa o quão complexo seja ...

por outro lado, o que eu sei sobre inscrições...
pessoas são loucas

Atualmente, encontrei alguns problemas semelhantes, mas diferentes.

Estou trabalhando em um fluxo como uma inscrição super complexa, e temos algumas páginas de continuação para permitir que o usuário insira as informações necessárias, cada página é um formulário. O fluxo se parece com:

FormPage_A -> FormPage_B -> FormPage_C -> SuccessPage

O problema é que não fazemos nenhuma solicitação de API antes de FormPage_C. Apenas salvamos os dados no navegador no FormPage_A/B e enviamos todos eles para a API no FormPage_C.

Então, a questão é: devemos colocar os dados do formulário de FormPage_A e FormPage_B no estado do aplicativo?

Se sim, parece que estamos salvando alguns dados temporários que não têm efeito na interface do usuário no estado do aplicativo. E como o estado do aplicativo do redux vive na memória, o que acontecerá quando o usuário atualizar a página no FormPage_B?

Se não, onde é o lugar certo para colocá-lo? Devemos construir outra camada como fake api fora do fluxo redux para salvá-la?

@frederickfogerty @gaearon @ir-fuel @stremlenye

Eu apenas passaria as propriedades em this.state que são relevantes para o seu envio como adereços para a próxima etapa.
então FormPage_A.state será passado em FormPage_B.props etc. e faça o envio em FormPage_C

ou se você tiver um componente pai que gerencie isso, armazene o estado na propriedade state desse controle pai e após a última etapa envie todos

Apenas certifique-se de que, se o usuário decidir recarregar a página, você inicia do início novamente.

@kpaxqin Acho que neste caso você poderia declarar um componente de gerenciamento como Flow que lidará com o subconjunto de etapas com seus estados e lógica genérica back-forward para cada um.
Assim, você pode tornar suas etapas o mais sem estado possível e acompanhar o estado de todo o processo no componente delimitador.
Algo como:

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

@ir-fuel @stremlenye

Obrigado por suas sugestões.
Parece que todos nós não queremos colocar essas coisas temporárias no estado do aplicativo.

Mas quando o usuário atualiza a página na metade do fluxo

Apenas certifique-se de que, se o usuário decidir recarregar a página, você inicia do início novamente.

Parece não ser uma boa experiência do usuário, somos um site para celular (essa é uma das razões pelas quais dividimos formulários enormes em páginas pequenas), é doloroso inserir coisas no celular, e acredito que o usuário ficará louco se perder todo o coisas depois de apertar o botão de atualização, talvez eles simplesmente desistam.

Acho que talvez construir o fake api fora do fluxo redux seja uma escolha aceitável, enviar coisas para o fake api em cada etapa e recuperá-las quando necessário, assim como temos api para isso, e como usamos como api, a única coisa que se conecta a ela é que os criadores de ação, as páginas e os componentes serão limpos e também é fácil lidar com erros (use o código comum para lidar com erros de api).

Faz sentido para mim porque, uma vez que queremos persistir esses dados, eles não devem estar no estado do componente ou no estado do aplicativo, o efeito colateral deve ser tratado no criador da ação

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

Isso deixa as coisas claras para mim, mas parece estranho adicionar algo de fluxo redux em nosso aplicativo.

Eu realmente não me importaria se o usuário recarregasse sua página.
Por outro lado, você nos diz que tem um processo de inscrição complicado com vários formulários e depois diz que é um site para celular. Você não acha que isso também vai enlouquecer os usuários e fazê-los desistir se tiverem que preencher muitas coisas em um dispositivo móvel? Eu acho que é ainda pior do que não suportar recarregar. Eu não acho que muitos sites suportam o recarregamento de página no meio de um processo de inscrição de qualquer maneira.

Eu acho que está apenas tornando as coisas muito mais complicadas para algo que realmente não acontece muito. Eu me concentraria mais em tentar simplificar o processo de inscrição ou dividi-lo em várias etapas em seu site para que o usuário não tenha a impressão de que está preenchendo X páginas de formulários antes de finalmente ativar sua conta.

E se você realmente quiser dar suporte ao recarregamento no meio da inscrição, basta fazer com que o componente pai que controla todas essas etapas armazene seu estado em localStorage .

@kpaxqin eu iria com a abordagem "API falsa". Por que fingir BTW, é apenas a API de armazenamento intermediário que você precisa para o seu processo de várias etapas. Nem toda API deve necessariamente estar em um servidor e ser acessada via HTTP ou outro protocolo de rede.

O que eu concordo com @ir-fuel é que todo o estado do processo de várias etapas deve ser armazenado, não apenas a etapa atual.

@ir-fuel O recarregamento pode ocorrer sem a intenção explícita do usuário, por exemplo, um usuário pode alternar para outro aplicativo móvel, como um notebook ou uma chamada telefônica, e o navegador pode perder seu estado devido à memória insuficiente no dispositivo, portanto, recarregará quando o usuário voltar.

O problema novamente é que você está poluindo o estado global do aplicativo com algo que você só precisa em algumas telas do seu webapp.

@sompylasar @kpaxqin Acho que @ir-fuel sugeriu a abordagem certa - se você precisar de algo intermediário para ser eventualmente persistido, persista no lado do cliente: muito mais fácil de implementar, não envolve realmente o servidor na persistência de dados inconsistentes, não t realizar qualquer solicitação http (o que também é um grande problema em dispositivos móveis) e extremamente fácil de mudar a abordagem no futuro.

E sobre persistir qualquer dado em algum lugar – pense em como você irá limpá-lo depois que o processo falhou ou já foi concluído – esse foi o tema inicial de um tópico.
Outra opção que você pode usar – persistir os dados do usuário no URL. Se seus formulários consistem em dois campos de texto, essa pode ser a boa solução de compensação.

Não é tanto "armazená-lo no lado do cliente ou do servidor", é mais uma questão de "eu lido com esse 'ad hoc' na minha classe de componente pai" versus "eu faço toda a ação/redutor/dança de estado".
Eu não gosto de passar pela ação/redutores se for para o estado temporário. Eu só faço isso para o estado real do aplicativo.

@ir-combustível :+1:

@stremlenye Por favor, leia minha resposta novamente. Digo explicitamente que você não precisa criar uma API do lado do servidor para criar uma camada de API de persistência.

@ir-fuel Não passar por ações/redutor cancela a previsibilidade e a reprodutibilidade das transições de estado.

@ir-fuel Você pode limpar facilmente o estado do seu aplicativo após o envio do formulário, para que ele não fique "poluído".

RE: limpeza. Faça uma ação CLARO e tenha um botão em sua interface do usuário para redefinir o fluxo de inscrição. Você também pode fazer isso via roteador (envie essa ação ao visitar a URL de inscrição inicial e, se a inscrição tiver começado, altere a URL para outra coisa).

@sompylasar não há problema em redefinir seu estado com um botão explícito – os problemas ocorrem após falhas não tratadas: é difícil prever como o estado pode ser corrompido e definir uma estratégia de restauração para cada caso específico.

@stremlenye sim, a engenharia de software é difícil às vezes. Estados e transições previsíveis ajudam a mitigar isso.

Não apenas isso, mas se o seu estado for mais do que um monte de objetos json (o que eu odeio porque tudo é por convenção e nada é imposto) e você usa algo na linha de Record do Immutable.js, então você precisa declarar as propriedades de seus objetos antecipadamente e isso inclui todas as coisas temporárias que você sempre quis lá.

Para mim, construir algo com redux sem usar Immutable.js é apenas um acidente esperando para acontecer.

Eu sei que você poderia fazer um registro Signup e só ele ser não nulo ao se inscrever, mas o problema é mais fundamental, porque você pode ter muitos desses casos em seu webapp, e é isso que incomoda mim.

@ir-fuel

Não apenas isso, mas se o seu estado for mais do que um monte de objetos json (o que eu odeio porque tudo é por convenção e nada é imposto) e você usa algo na linha de Registro Immutable.js, então você precisa declarar as propriedades de seus objetos antecipadamente e isso inclui todas as coisas temporárias que você sempre quis lá.

Para mim, construir algo com redux sem usar Immutable.js é apenas um acidente esperando para acontecer.

Por enquanto, não estou convencido de usar Immutable.js com redux, parecia muito complexo para integrar (e não ter desempenho). Um congelamento profundo durante o desenvolvimento e teste e um conjunto de testes devem ser suficientes para cobrir a maioria dos casos. E uma convenção estrita para evitar a mutação de estado em redutores ou outros lugares onde você tem a referência.

Eu sei que você pode fazer um Registro de Inscrição e apenas fazer com que ele não seja nulo ao se inscrever

Sim, era isso que eu tinha em mente.

mas o problema é mais fundamental, porque você pode ter muitos desses casos em seu webapp, e é isso que me incomoda.

Isso é o que chamamos de estado explícito do aplicativo -- a cada momento sabemos o estado do aplicativo que consiste em muitos componentes. Se você deseja tornar a estrutura de estado menos explícita e mais dinâmica, siga https://github.com/tonyhb/redux-ui

Esse é todo o problema com o desenvolvimento JS. "Vamos manter as convenções". E é por isso que as pessoas começam a perceber que simplesmente não funciona em ambientes complexos, porque as pessoas cometem erros e vêm com soluções como TypeScript e Immutable.js.

Por que seria muito complicado de integrar? Se você usar 'Record' você usa todos esses objetos (desde que você esteja apenas lendo eles) como objetos JS normais, com notação de ponto para acessar as propriedades, com a vantagem de você declarar antecipadamente quais propriedades fazem parte do seu objeto .

Estou fazendo todo o meu desenvolvimento redux com Immutable.js, sem nenhum problema. O fato de que simplesmente não pode modificar um objeto por acidente é uma grande vantagem no redux e a 'segurança de tipo' de definir seus protótipos de objetos também é conveniente.

E se você realmente quiser dar suporte ao recarregamento no meio da inscrição, basta fazer com que o componente pai que controla todas essas etapas armazene seu estado em localStorage.

@ir-fuel
Para mim, colocar as coisas em localStorage parece um efeito colateral e não parece uma boa prática fazê-lo no componente.

O problema novamente é que você está poluindo o estado global do aplicativo com algo que você só precisa em algumas telas do seu webapp.

Talvez eu não tenha descrito a API falsa com clareza suficiente, a razão mais importante para construí-la é que eu não quero colocar essas coisas only need in a few screens no estado da aplicação do redux. No FormPage_A/B, eles apenas chamam a API para armazenar os dados do formulário e, em seguida, seguem a próxima etapa:

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

e em FormPage_C (o formulário final):

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

Todos esses dados temporários vivem em uma API falsa e o estado global do aplicativo não foi poluído. Se quisermos limpar os dados na api falsa para reiniciar o fluxo, basta enviar a ação para fazer isso.

@sompylasar não há problema em redefinir seu estado com um botão explícito – os problemas ocorrem após falhas não tratadas: é difícil prever como o estado pode ser corrompido e definir uma estratégia de restauração para cada caso específico.

@stremlenye Eu concordo com isso, mas acho que esses casos de falha específicos são mais propensos a ser coisas de negócios, eles estão sempre aqui, não importa onde colocamos o estado, e eles devem ser tratados da maneira correta, mas não foram descritos com clareza suficiente em o momento. A única coisa que os desenvolvedores podem fazer é facilitar o rastreamento para que possamos corrigi-lo com menos dor.

E colocar o estado no componente pai pode tornar mais difícil raciocinar quando as coisas ficam complexas. Com a API falsa, alteramos esse estado com ações e parece mais previsível.

Mas concordo que a API falsa parece muito pesada na maioria dos casos com menos complexidade.

Não passou por todas as mensagens acima. Aqui eu apenas dou minha abordagem: detectar alterações de prop em componentWillReceiveProps.

Digamos que a forma do estado seja: { loginPending, loginError }, quando iniciar o login, defina loginPending = true. Quando houver sucesso ou falha, defina { loginPending: false, loginError: 'some error.' }.

Então eu posso detectar se o evento de sucesso do login em um componente e redirecionar a página:

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

Ele apenas funciona embora não seja bonito.

Minha humilde implementação em relação à limpeza do formulário no redirecionamento usando react-router-redux . Redutor:

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

Você acha que essa abordagem está correta? Funciona.

Estou acabado com isso. Diga o que você pensa:

registrar ação 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));
};

recipiente de formulário 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);

e ajudantes:

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

O que é importante - apenas o registro bem-sucedido entra na loja Redux como ação, caso contrário, o erro passa para o estado do componente do contêiner. O usuário recebe um erro, navega para outra página e novamente para a página do formulário - o formulário está vazio e sem erros. Não há ações "USER_REGISTRATION_REQUEST" ou "USER_REGISTRATION_FAILURE" porque elas descrevem o estado do componente, não o estado do aplicativo.

@sunstorymvp

Você adoçou seu código com async / await , e esse açúcar oculta o fato de que existem três estados da solicitação (antes, em andamento, depois). Esses estados ficam ocultos no componente de exibição. O início da solicitação é ignorado e o erro da solicitação é tratado na exibição e não é despachado via redux, portanto, o estado do aplicativo está fora de operação aqui. Se por algum motivo o componente que contém este estado desmontar e depois remontar, o estado da requisição de registro será perdido e seu resultado não será tratado, podendo levar a erros subsequentes (por exemplo, usuário já cadastrado).

O usuário recebe um erro, navega para outra página e novamente para a página do formulário - o formulário está vazio e sem erros.

Para conseguir isso, você pode despachar uma ação como USER_REGISTRATION_RESET na montagem do componente que redefiniria o erro na loja. Essa ação só deve ser tratada se não houver solicitação de registro pendente, caso contrário, o resultado da solicitação pendente não será processado pelo aplicativo.

obrigado pelo feedback.

Você açucarou seu código com async/await, e esse açúcar esconde o fato de que existem três estados da solicitação (antes, em andamento, depois)

essas condições existem independentemente de eu usar o async/await ou não.

Esses estados ficam ocultos no componente de exibição

como pretendido, que é o estado do componente .. por que isso deve estar no estado do aplicativo?

O início da solicitação é ignorado

em andamento...

e o erro de solicitação é tratado na visualização e não é despachado via redux

como pretendido

Se por algum motivo o componente que contém este estado desmontar e depois remontar, o estado da requisição de registro será perdido e seu resultado não será tratado

isso não é verdade. o usuário será registrado e redirecionado se a solicitação for bem-sucedida, independentemente do estado do componente ou mesmo da existência. veja o criador da ação register (handleData).

Para conseguir isso, você pode despachar uma ação...

Esse é o ponto. Outra ação? Por que tanto clichê? Desabilite ou habilite o botão de envio, mostre erros de formulário ou validação ao vivo, mostre o spinner enquanto a solicitação estiver aberta.. e essas ações levam a novas ações que restauram o estado padrão.. por que isso deve estar no estado do aplicativo?

isso não é verdade. o usuário será registrado e redirecionado se a solicitação for bem-sucedida, independentemente do estado do componente ou mesmo da existência. veja o criador da ação de registro (handleData).

Sim, mas isso deixa a possibilidade de duas solicitações register subsequentes porque o estado da solicitação não é rastreado no estado do aplicativo.

Desabilite ou habilite o botão de envio, mostre erros de formulário ou validação ao vivo, mostre o spinner enquanto a solicitação estiver aberta.. e essas ações levam a novas ações que restauram o estado padrão.. por que isso deve estar no estado do aplicativo?

Para previsibilidade e replayability, este é o propósito do Redux. Tendo apenas as ações e o estado inicial, você pode colocar o aplicativo em um determinado estado apenas despachando ações. Se um componente começar a conter algum estado, o estado do aplicativo começará a se separar, não haverá uma única árvore de estado que represente o estado do aplicativo.

Não seja excessivamente dogmático sobre isso, no entanto. Há muitas razões para colocar coisas no estado Redux e muitas razões para deixar algo no estado do componente local, dependendo do seu cenário. Consulte http://redux.js.org/docs/FAQ.html#organizing -state-only-redux-state e https://news.ycombinator.com/item?id=11890229.

Ok, agora ficou claro para mim: depende. Obrigado!

Eu uso componentWillReceiveProps e tenho um objeto auth no meu componente. Não tenho certeza se isso ajuda em tudo.

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

Há algo de errado em fazer assim?

E sobre essa abordagem http://codereview.stackexchange.com/questions/138296/implementing-redirect-in-redux-middleware?
Passando a URL de redirecionamento para a ação e chamando o método next extra com push de react-router-redux no middleware.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

captbaritone picture captbaritone  ·  3Comentários

elado picture elado  ·  3Comentários

dmitry-zaets picture dmitry-zaets  ·  3Comentários

benoneal picture benoneal  ·  3Comentários

timdorr picture timdorr  ·  3Comentários