Redux: Рекомендации по обработке потока данных для страниц входа/регистрации с перенаправлением

Созданный на 22 июл. 2015  ·  66Комментарии  ·  Источник: reduxjs/redux

РЕДАКТИРОВАТЬ: я пытаюсь понять, как обрабатывать поток данных, когда у вас есть страница с формой. После отправки формы вы либо перенаправляете на другую страницу, либо выдаете сообщение об ошибке .

Проблема заключается в том, где эта информация должна быть _временно сохранена_ в потоке данных. Потому что после того, как вы перенаправите или отобразите сообщение, состояние (или информация, которую вы сохранили) больше не имеет значения и должно быть сброшено или очищено.


В серверном веб-приложении у вас обычно есть такие страницы, как вход в систему, регистрация, сброс пароля и т. д.

Когда пользователь переходит в приложение и не вошел в систему, он перенаправляется на страницу входа. Это легко сделать с помощью хука RR onEnter .

Затем пользователь заполнит форму и отправит ее. На этом этапе обычно, если запрос был успешным, вы хотите перенаправить в корень приложения или на другую «защищенную» страницу. Если это не удалось, вы хотите остаться на странице и показать сообщение об ошибке.

(Это относится не только к странице входа, но и к другим страницам, а также упомянутым выше. Сейчас я сосредоточусь на входе в систему)

Пока у вас есть этот поток (который является распространенным вариантом использования):

  • пользователь переходит к /
  • перенаправить на /login
  • заполнить форму и отправить
  • если все в порядке перенаправить на / (или на другую страницу)
  • если не удалось остаться на форме входа и показать сообщение об ошибке

Если мы реализуем этот поток с избыточностью, я думаю, у меня есть следующая настройка:

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

Учитывая, что этот поток правильный - надеюсь :) - вы можете видеть, что у меня есть 2 флага в состоянии приложения: shouldRedirect и errorMessage .
Эти флаги используются только при отправке формы.

Первый вопрос: нормально ли иметь эти флаги в состоянии приложения?

Еще одна вещь, которую следует учитывать, это то, что я должен «сбросить» эти флаги при перенаправлении, потому что, если я хочу перейти на другую страницу (например, зарегистрироваться, ...), у меня должно быть чистое состояние ( { shouldRedirect: false, errorMessage: null } ).

Поэтому я мог бы захотеть отправить другое действие, например

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

У кого-нибудь была эта проблема? Я что-то упустил или это "правильный" способ сделать это?

Есть ли другие альтернативы?

Любая обратная связь очень приветствуется! :)

discussion docs

Самый полезный комментарий

Я думаю, что вопрос немного глубже, чем просто обобщение «форм отправки» — это больше о том, что на самом деле представляет собой состояние и какие данные должны отображаться в store , а какие нет.

Мне нравится думать о View части потоков данных как о чистой функции render(state): DOM , но это не совсем так.
Возьмем в качестве примера простой компонент формы, который отображает два поля ввода:

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

При вводе данных в эти поля выполняются некоторые внутренние изменения состояния, поэтому в конце мы получим что-то вроде этого:

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

Итак, мы получили какое-то состояние, которое не связано с состоянием приложения, и никому до него нет дела. Когда мы вернемся к этой форме в следующий раз, она будет показана стертой, а «Джон Сноу» исчезнет навсегда.
Похоже, мы модифицировали DOM, не касаясь потоков данных.

Предположим, что это форма подписки на какое-то письмо-уведомление. Теперь нам нужно взаимодействовать с внешним миром, и мы сделаем это с помощью нашего специального действия.

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

Когда пользователь нажимает кнопку отправки, он ожидает какого-либо ответа от пользовательского интерфейса, если это не удалось, и что делать дальше. Итак, мы хотим отслеживать состояние запроса для отображения экрана загрузки, перенаправления или отображения сообщения об ошибке.
Первая идея состоит в том, чтобы отправить некоторые внутренние действия, такие как loading , submitted , failed , чтобы за это отвечало состояние приложения. Потому что мы хотим визуализировать состояние и не думать о деталях.

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

Но тут появляется дьявол: теперь у нас это состояние страницы сохраняется, и следующий вызов render(state) будет отображать форму в грязном состоянии, поэтому нам нужно создать действие cleanup , чтобы вызывать его каждый раз, когда мы монтируем форму.
И для каждой формы, которая у нас есть, мы добавим один и тот же шаблон из loading , submitted , failed и cleanup действий с хранилищами/редьюсерами… пока мы не остановимся и не подумаем о состоянии, которое мы на самом деле хотим отобразить.

Принадлежит ли submitted к состоянию приложения или это просто состояние компонента, например значение поля name . Действительно ли пользователь заботится о том, чтобы это состояние сохранялось в кеше, или он хочет видеть чистую форму при повторном посещении? Ответ: это зависит.

Но в случае чистых форм отправки мы могли бы предположить, что состояние формы не имеет ничего общего с состоянием приложения: мы хотим знать, является ли факт пользователем logged in или нет в случае страницы входа, или мы не хотим чтобы узнать что-нибудь о том, изменил ли он пароль или потерпел неудачу: если запрос увенчается успехом, просто перенаправьте его куда-нибудь еще.

Так как насчет предложения, что нам не нужно это состояние где-то еще, кроме Компонента? Давайте попробуем.
Чтобы обеспечить более причудливую реализацию, мы могли бы предположить, что в 2015 году мы живем в асинхронном мире, и у нас везде есть Promises , поэтому пусть наше действие возвращает Promise :

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

На следующем шаге мы можем начать отслеживать действие в нашем 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')
}

Похоже, у нас есть все необходимое для рендеринга компонента без загрязнения состояния приложения и избавления от необходимости его очистки.
Итак, мы почти у цели. Теперь мы можем начать обобщать поток Component .
Прежде всего, давайте разделимся на проблемы: отслеживание действий и реакций на обновление состояния:
(Давайте сделаем это с точки зрения redux , чтобы оставаться в пределах репо)

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

а также

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

Теперь, с одной стороны, у нас есть decorator , которые предоставляют пропсы submitted и error с обратным вызовом onSubmit , а с другой стороны decorator , которые куда-то перенаправляют, если получает свойство submitted === true . Давайте прикрепим их к нашей форме и посмотрим, что у нас получится.

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

И вот она: чистая форма рендеринга без зависимостей библиотеки $# routing , реализация flux , приложение state , делающее то, для чего оно было создано: отправка и перенаправление.

Спасибо за чтение и извините, что отняли ваше время.

Все 66 Комментарий

Вместо того, чтобы иметь shouldRedirect там, как насчет редьюсера сеанса, который просто отслеживает isLoggedIn ? Таким образом, вы можете перенаправить со своей страницы входа на любую другую страницу, отметив isLoggedIn вместо shouldRedirect .

Кроме того, сброс состояния можно выполнить, просто обработав действие LOGGED_IN в редюсере. Если это действие произойдет, вы знаете, что можете безопасно сбросить состояние ошибки до значения null .

Имеет ли это смысл? :D

Это имеет смысл, если есть только страница входа, но это немного более общий подход. Например, для страницы регистрации или страницы сброса пароля. Даже если вы вошли в систему, вы все равно сможете видеть эти страницы.

И эти флаги должны использоваться всеми этими страницами, потому что все они имеют одинаковый поток.

Надеюсь, теперь стало понятнее :)

Да, с моей точки зрения, «shouldRedirect» является слишком общим, чтобы ассоциироваться с определенным действием. Это может вызвать ошибки, когда вы каким-то образом забываете установить этот флаг, а затем страница перенаправляется на другую страницу, на которую она на самом деле не должна перенаправляться.

Но, конечно, если вы хотите очистить свое состояние, вы можете просто сделать это с помощью действия. Или вы можете прослушать изменение истории вне реакции, а затем запустить действие по очистке состояния. Это может быть предпочтительнее, потому что вам не нужно разбрасывать эту логику по всем компонентам, но вы можете хранить ее в одном месте.

Но это только возможное решение для государственной очистки. Я еще не уверен, как решить проблему перенаправления.

Также возникает вопрос: есть ли смысл вообще иметь эти флаги в состоянии? Если нет, то как реализовать этот поток?

@gaearon , если у вас есть время, я также хотел бы услышать ваши отзывы. Спасибо! :+1:

В моем приложении я перенаправляюсь в промежуточном программном обеспечении и не использую никаких флагов. Но я не использую реактивный маршрутизатор.

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

Он не подвергался рефакторингу, он предназначен только для тестирования моего маршрутизатора :).

Это копирование и вставка из моего приложения и то, как я обрабатываю загрузку пользовательских данных и вход в систему. Я постараюсь прокомментировать код как можно лучше, и я буду реагировать на вопросы.

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)

Что касается ответов на ваши вопросы @emmenko , я думаю, что наличие shouldRedirect в вашем штате неприемлемо и должно быть сделано более функциональным подходом. Наличие errorMessage в состоянии приложения — это нормально, просто убедитесь, что оно находится в состоянии входа в систему, например state.auth.errorMessage , а не errorMessage верхнего уровня.

Я думаю, что наличие shouldRedirect в вашем состоянии неприемлемо и должно быть сделано в более функциональном подходе.

Я согласен. Вещи, которые полезны только один раз, не должны находиться в состоянии ИМО.

Кроме того, в соответствии со слабиной, я не думаю, что вы должны хранить isLoggedIn в состоянии приложения, так как это более реально вычисляемое свойство, которое можно вычислить с помощью простого селектора, такого как const isUserLoggedIn = state => state.user.id !== null

См. https://github.com/faassen/reselect для получения дополнительной информации об использовании селектора.

@frederickfogerty спасибо за пример.

Однако вопрос: это отлично работает для входа в систему, потому что у вас обычно есть флаг loggedIn в state . Но как насчет других страниц, таких как регистрация, сброс пароля и т. д.?
Эти страницы ведут себя так же, как и вход в систему: если все в порядке, перенаправить, в противном случае показать сообщение об ошибке.
Итак, как вы справляетесь с такими случаями? Я изо всех сил пытаюсь найти правильный поток для этих случаев, и я не уверен, где должно жить состояние.

Вещи, которые полезны только один раз, не должны находиться в состоянии ИМО.

@gaearon Я согласен, но я все еще не уверен, какой правильный подход для этого. Вам все еще нужно где-то иметь состояние...

Вот как я это делаю, ничего не сохраняя в состоянии.

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

Я не думаю, что вам следует хранить isLoggedIn в состоянии приложения, так как это скорее вычисляемое свойство.

Ага, имеет смысл.

@gaearon Я предполагаю, что вопрос для этого общего рабочего процесса (перенаправление или отображение ошибки) заключается в том, следует ли это делать с «сокращением» или другим способом. И какой может быть эта альтернатива?

связанный, и я прикреплю его здесь ... на самом деле я также имел дело с этой проблемой и в настоящее время пытаюсь повторно реализовать следующий пример в Redux. Пока ничего не видно, потому что я делаю это в свободное время:
https://auth0.com/blog/2015/04/09/добавление-аутентификации-к-вашему-реагированию-потоку-приложению/

Это чистый поток:
https://github.com/auth0/react-flux-jwt-authentication-sample

Может быть, это полезно для лучших практик?!

@эмменко

Итак, как вы справляетесь с такими случаями?

Пока вы используете вход в систему, isLoggedIn (который не находится в состоянии приложения, но незначителен) все еще изменяется, и это перенаправит их. Неважно, откуда произошло это изменение состояния :)

Лучший подход, который я использовал, - добавить эту логику в коннектор, где функциональность необходима для его дочерних элементов, например, для авторизации, скажем, у вас есть компонент AppProtected , где все, что является дочерним элементом этого компонента, нуждается в авторизации. , то вы помещаете эту логику в коннектор компонента AppProtected.

Вот как я это делаю, ничего не сохраняя в состоянии.

Мне определенно не нравится этот подход, который имеет двусторонний поток данных между пользовательским интерфейсом и AC, что идет вразрез с потоком. Если вы привязаны к этому императиву, вы можете сделать что-то вроде

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')
  }
}
  • Если вход пользователя выполнен успешно, он перенаправляется с соответствующей страницы.
  • Если авторизация пользователя не удалась, он не авторизуется и останется на той же странице (ничего не происходит)

Это работает для всех страниц, которые вы упомянули

Извините, но я все еще не понимаю, как я справляюсь с перенаправлением / рендерингом ошибок.

Давайте сделаем шаг назад и скажем, что у меня есть страница reset-password . Мы также предполагаем, что мы не должны касаться какого-либо состояния, сохраняя какие-то однократные флаги (как упоминалось ранее).
Когда я нажимаю отправить форму, что должно произойти?

Это то, что я хотел бы знать, каким должен быть поток и где он должен жить.

PS: спасибо за ваш отзыв до сих пор!

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

Это далеко не так элегантно, как страницы входа и регистрации, так как они очень связаны.

Мне пора спать, завтра отвечу на другие вопросы

Итак, мы снова храним флаги в состоянии ;)

ПС: спокойной ночи!

Я думаю, что вопрос немного глубже, чем просто обобщение «форм отправки» — это больше о том, что на самом деле представляет собой состояние и какие данные должны отображаться в store , а какие нет.

Мне нравится думать о View части потоков данных как о чистой функции render(state): DOM , но это не совсем так.
Возьмем в качестве примера простой компонент формы, который отображает два поля ввода:

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

При вводе данных в эти поля выполняются некоторые внутренние изменения состояния, поэтому в конце мы получим что-то вроде этого:

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

Итак, мы получили какое-то состояние, которое не связано с состоянием приложения, и никому до него нет дела. Когда мы вернемся к этой форме в следующий раз, она будет показана стертой, а «Джон Сноу» исчезнет навсегда.
Похоже, мы модифицировали DOM, не касаясь потоков данных.

Предположим, что это форма подписки на какое-то письмо-уведомление. Теперь нам нужно взаимодействовать с внешним миром, и мы сделаем это с помощью нашего специального действия.

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

Когда пользователь нажимает кнопку отправки, он ожидает какого-либо ответа от пользовательского интерфейса, если это не удалось, и что делать дальше. Итак, мы хотим отслеживать состояние запроса для отображения экрана загрузки, перенаправления или отображения сообщения об ошибке.
Первая идея состоит в том, чтобы отправить некоторые внутренние действия, такие как loading , submitted , failed , чтобы за это отвечало состояние приложения. Потому что мы хотим визуализировать состояние и не думать о деталях.

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

Но тут появляется дьявол: теперь у нас это состояние страницы сохраняется, и следующий вызов render(state) будет отображать форму в грязном состоянии, поэтому нам нужно создать действие cleanup , чтобы вызывать его каждый раз, когда мы монтируем форму.
И для каждой формы, которая у нас есть, мы добавим один и тот же шаблон из loading , submitted , failed и cleanup действий с хранилищами/редьюсерами… пока мы не остановимся и не подумаем о состоянии, которое мы на самом деле хотим отобразить.

Принадлежит ли submitted к состоянию приложения или это просто состояние компонента, например значение поля name . Действительно ли пользователь заботится о том, чтобы это состояние сохранялось в кеше, или он хочет видеть чистую форму при повторном посещении? Ответ: это зависит.

Но в случае чистых форм отправки мы могли бы предположить, что состояние формы не имеет ничего общего с состоянием приложения: мы хотим знать, является ли факт пользователем logged in или нет в случае страницы входа, или мы не хотим чтобы узнать что-нибудь о том, изменил ли он пароль или потерпел неудачу: если запрос увенчается успехом, просто перенаправьте его куда-нибудь еще.

Так как насчет предложения, что нам не нужно это состояние где-то еще, кроме Компонента? Давайте попробуем.
Чтобы обеспечить более причудливую реализацию, мы могли бы предположить, что в 2015 году мы живем в асинхронном мире, и у нас везде есть Promises , поэтому пусть наше действие возвращает Promise :

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

На следующем шаге мы можем начать отслеживать действие в нашем 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')
}

Похоже, у нас есть все необходимое для рендеринга компонента без загрязнения состояния приложения и избавления от необходимости его очистки.
Итак, мы почти у цели. Теперь мы можем начать обобщать поток Component .
Прежде всего, давайте разделимся на проблемы: отслеживание действий и реакций на обновление состояния:
(Давайте сделаем это с точки зрения redux , чтобы оставаться в пределах репо)

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

а также

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

Теперь, с одной стороны, у нас есть decorator , которые предоставляют пропсы submitted и error с обратным вызовом onSubmit , а с другой стороны decorator , которые куда-то перенаправляют, если получает свойство submitted === true . Давайте прикрепим их к нашей форме и посмотрим, что у нас получится.

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

И вот она: чистая форма рендеринга без зависимостей библиотеки $# routing , реализация flux , приложение state , делающее то, для чего оно было создано: отправка и перенаправление.

Спасибо за чтение и извините, что отняли ваше время.

Справедливо! Честно говоря, мне все еще интересно посмотреть на сохранение состояния локального компонента в Redux: #159.

@stremlenye Это довольно круто. Спасибо, что поделились!

@iclanzan спасибо за чтение :)
На самом деле сейчас я думаю о предложении @gaearon, которое он упомянул в предыдущем комментарии. Взгляните на обсуждение № 159, чтобы получить некоторые важные идеи.

В заключение — когда выйдет RR 1.0, мы расскажем об этом в рецепте «Использование с React Router».
Это можно отследить на https://github.com/rackt/redux/issues/637.

@stremlenye, у тебя потрясающий подход! Имеет ли смысл - вместо использования декоратора - использовать компонент более высокого порядка реакции-маршрутизатора, в который будут вложены все _unathorized_, и он будет отвечать за перенаправление, как только в хранилище авторизации будет пользователь?

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

Что-то вроде этого, где Auth заботится только о том, чтобы определить, вошел ли пользователь в систему. Таким образом, я все еще могу манипулировать своим магазином, отправляя действия из компонента Login . Не так чисто, но меньше кода и легче разобраться :)

@tomazzaman Спасибо :)
Большинство приложений React, которые я написал, включали что-то вроде ApplicationContainer , которые можно было использовать для той же цели без новой реализации Route .
Если вы используете RR-v1.0.0-rc1 , вы можете достичь тех же целей с помощью хука onEnter : просто проверьте, соответствует ли текущее состояние аутентифицированному или нет.

Я тоже ломал голову над этим, и, кажется, @stremlenye проделал отличную работу, показав, как это можно сделать. Проблема в том, что кто-то склонен застревать в цикле «все должно пройти через действие — редуктор — обновление-компонент-с-хранилищем-состоянием» (я тоже), и что затем мы склонны загрязнять хранилище всевозможными вещами. состояние, которое используется только в очень специфических сценариях в приложении (на 1 экране такие вещи, как «успешно ли я выполнил сброс пароля»), в то время как с этим можно легко справиться, просто выполнив асинхронные вызовы внутри компонента и обновив сам компонент используя this.setState вместо того, чтобы загрязнять глобальное состояние + необходимость сбрасывать все эти переменные состояния на случай, если позже мы откроем этот компонент во второй раз. Не говоря уже о загрязнении редукторов состояния всем кодом начала/успеха/обработки ошибок.

Попробую адаптировать мой код и здесь, чтобы посмотреть, исправит ли он ситуацию.

@ir-fuel, если будет время, не могли бы вы отправить небольшой пример того, что вы получите, на github?

Что ж, это будет сложно сделать, но я могу привести небольшой пример того, что я сделал. У меня есть компонент, в котором пользователь может попросить сбросить свой пароль. Фактическая операция сброса теперь происходит так:

  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 — это функция, которая выполняет вызов ajax на мой сервер.

Таким образом, никаких обращений к хранилищу/редукторам, никакого загрязнения глобального состояния вещами, которые вас интересуют только внутри одного компонента и используются исключительно для временного отображения состояния в этом компоненте, и нет необходимости сбрасывать кучу переменных состояния внутри ваших редукторов.

и рендер выглядит примерно так

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

Я надеюсь, что это ясно.

Выглядит хорошо :+1:
Единственный трюк, который следует охватить (с моей точки зрения), - это настройка промежуточного программного обеспечения для возврата обещания из отмененного действия (возможно, просто проксировать возвращаемое значение действия). Сделав это, мы позволим действию изменять состояние в обычном потоке, если это необходимо, сохраняя возможность подключения к материалам Promise в области компонента.

я думаю, что можно думать о состоянии вашего приложения как о состоянии, в котором вы уже вошли в систему. если ни один пользователь не вошел в систему, зачем вам нужно состояние приложения? конечно, вы могли бы иметь один внутри компонента входа в систему.

логика приложения на странице входа не должна быть такой сложной, чтобы владеть верхним состоянием приложения, верно?

Процесс входа @SkateFreak является частью приложения. Разве вы не думали о состоянии потока входа в систему (прогресс, ошибка), состоянии потока регистрации и т. Д., Для которых вам также лучше использовать React (вероятно, с Redux, если вы уже используете его для других компонентов приложения).

@sompylasar я за состояние приложения, я просто думаю, что пока пользователь находится ВНЕ моего приложения, я могу предоставить ему более простое приложение без магазина избыточности. мой магазин редуксов сработает, как только вы войдете в мое приложение.

этот подход работал для меня до этого самого момента, но, возможно, я пойму необходимость состояния приложения на этапе входа в мои будущие проекты

Вы можете на корневом уровне своего магазина разделить свой магазин на часть «приложение» и часть «пользователь/логин», каждая из которых имеет свой редуктор. Таким образом, в этом случае вы никогда не будете загрязнять редукторы приложений кодом редуктора входа в систему и наоборот.

От: SkateFreak < notifications @github.comnotifications@ github.com >
Кому ответить: ract/redux < [email protected] [email protected] >
Дата: Воскресенье, 13 декабря 2015 г., 13:48
Кому: ract/redux < [email protected] [email protected] >
Копия: Йорис Манс < [email protected] [email protected] >
Тема: Re: [redux] Лучшая практика обработки потока данных для страниц входа/регистрации с перенаправлением (#297)

@sompyl asarhttps://github.com/sompylasar Я за состояние приложения, я просто думаю, что пока оно используется ВНЕ моего приложения, я могу предоставить ему более простое приложение без магазина избыточности. мой магазин редуксов сработает, как только вы войдете в мое приложение.

этот подход работал для меня до этого самого момента, но, возможно, я пойму необходимость состояния приложения на этапе входа в мои будущие проекты

Ответьте на это письмо напрямую или просмотрите его на Gi tHubhttps://github.com/rackt/redux/issues/297#issuecomment -164287194.

поправьте меня, если я ошибаюсь, но разве целью хранилища потоков не является обмен данными и состоянием приложения между маршрутами и компонентами? зачем мне «уменьшать» свое состояние и использовать свои «действия» для поддержания верхнего состояния, если все это не имеет значения, когда пользователь входит в систему?

я не вижу преимуществ, хотя это очень организованный и чистый способ делать вещи. я управляю своим состоянием перед входом в свой компонент <Sign /> , который содержит SignIn, SignUp и ForgotPassword.

Потому что регистрация может быть более сложным процессом, чем просто запрос имени пользователя и пароля.
Все зависит от сложности вашего потока.

От: SkateFreak < notifications @github.comnotifications@ github.com >
Кому ответить: ract/redux < [email protected] [email protected] >
Дата: Воскресенье, 13 декабря 2015 г., 14:00
Кому: ract/redux < [email protected] [email protected] >
Копия: Йорис Манс < [email protected] [email protected] >
Тема: Re: [redux] Лучшая практика обработки потока данных для страниц входа/регистрации с перенаправлением (#297)

поправьте меня, если я ошибаюсь, но разве целью хранилища потоков не является обмен данными и состоянием приложения между маршрутами и компонентами? зачем мне «уменьшать» свое состояние и использовать свои «действия» для поддержания верхнего состояния, если все это не имеет значения, когда пользователь входит в систему?

я не вижу преимуществ, хотя это очень организованный и чистый способ делать вещи. я управляю своим состоянием перед входом в свой компонент, который содержит SignIn, SignUp и ForgotPassword.

Ответьте на это письмо напрямую или просмотрите его на Gi tHubhttps://github.com/rackt/redux/issues/297#issuecomment -164287893.

я согласен.
но я вижу SignUp как форму с состоянием, которого должно быть достаточно, независимо от того, насколько сложна imo...

с другой стороны, что я знаю о регистрации...
люди сумасшедшие

В настоящее время я встречаю некоторую похожую, но другую проблему.

Я работаю над таким потоком, как суперсложная регистрация, и у нас есть некоторая страница продолжения, позволяющая пользователю вводить необходимую информацию, каждая страница представляет собой форму. Поток выглядит так:

FormPage_A -> FormPage_B -> FormPage_C -> SuccessPage

Проблема в том, что мы не делаем никаких запросов API перед FormPage_C. Мы просто сохраняем данные в браузере на FormPage_A/B и отправляем их все в API на FormPage_C.

Итак, вопрос в том, должны ли мы помещать данные формы FormPage_A и FormPage_B в состояние приложения?

Если да, похоже, мы сохраняем некоторые временные данные, которые не влияют на пользовательский интерфейс, в состояние приложения. И поскольку состояние приложения redux живет в памяти, что произойдет, когда пользователь обновит страницу в FormPage_B?

Если нет, то куда его поставить? Должны ли мы построить еще один слой, например fake api , из избыточного потока, чтобы сохранить его?

@frederickfogerty @gaearon @ir-fuel @stremlenye

Я бы просто передал свойства в this.state , которые имеют отношение к вашей отправке, в качестве реквизита для следующего шага.
так что FormPage_A.state будет передано в FormPage_B.props и т. д., а отправка будет выполнена в FormPage_C

или, если у вас есть родительский компонент, который управляет этим, сохраните состояние в свойстве state этого родительского элемента управления и после последнего шага отправьте все

Просто убедитесь, что если пользователь решит перезагрузить страницу, вы снова начнете с самого начала.

@kpaxqin Я думаю, в этом случае вы могли бы объявить управляющий компонент, например Flow , который будет обрабатывать подмножество шагов с их состояниями и общей логикой back-forward для каждого.
Таким образом, вы можете сделать свои шаги как можно более апатридными и отслеживать состояние всего процесса во вложенном компоненте.
Что-то типа:

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

@ир- топливо

Спасибо за ваши предложения.
Кажется, мы все не хотим помещать эти временные вещи в состояние приложения.

Но когда пользователь обновляет страницу в половине потока

Просто убедитесь, что если пользователь решит перезагрузить страницу, вы снова начнете с самого начала.

Это кажется не очень хорошим пользовательским интерфейсом, мы мобильный сайт (это одна из причин, почему мы разделяем огромную форму на маленькие страницы), вводить данные на мобильном устройстве неудобно, и я думаю, что пользователь сойдет с ума, если потеряет всю информацию. вещи после нажатия кнопки обновления, может быть, они просто откажутся от этого.

Я думаю, что, возможно, сборка fake api из избыточного потока является приемлемым выбором, отправляйте вещи в fake api на каждом этапе и извлекайте их при необходимости, точно так же, как у нас есть API для этого, и поскольку мы используем это похоже на API, единственное, что к нему подключается, это создатели действий, страницы и компоненты будут чистыми, а также легко обрабатывать ошибки (используйте общий код для обработки ошибок API).

Для меня это имеет смысл, потому что, когда мы хотим сохранить эти данные, они не должны находиться в состоянии компонента или в состоянии приложения, побочный эффект должен обрабатываться в создателе действия.

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

Это проясняет для меня некоторые вещи, но кажется утомительным добавлять что-то из нашего избыточного потока в наше приложение.

Меня действительно не волнует, что пользователь перезагрузит вашу страницу.
С другой стороны, вы говорите нам, что у вас сложный процесс регистрации с несколькими формами, а затем вы говорите, что это мобильный сайт. Не думаете ли вы, что это также сведет пользователей с ума и заставит их сдаться, если им придется заполнять слишком много данных на мобильном устройстве? Я думаю, что это даже хуже, чем отсутствие поддержки перезагрузки. Я не думаю, что многие сайты поддерживают перезагрузку страницы в середине процесса регистрации.

Я думаю, что это просто делает вещи намного сложнее для чего-то, что на самом деле не происходит часто. Я бы больше сосредоточился на попытках упростить процесс регистрации или разделить его на несколько этапов на вашем веб-сайте, чтобы у пользователя не сложилось впечатление, что он заполняет X страниц форм, прежде чем, наконец, активирует свою учетную запись.

И если вы действительно хотите поддерживать перезагрузку во время регистрации, просто сделайте так, чтобы родительский компонент, управляющий всеми этими шагами, сохранял свое состояние в localStorage .

@kpaxqin Я бы выбрал подход «поддельный API». Зачем подделывать BTW, это просто API промежуточного хранилища, который вам нужен для вашего многоэтапного процесса. Не каждый API обязательно должен быть на сервере и доступен через HTTP или другой сетевой протокол.

В чем я согласен с @ir-fuel, так это в том, что следует сохранять все состояние многоэтапного процесса, а не только текущий шаг.

@ir-fuel Перезагрузка может происходить без явного намерения пользователя, например, пользователь может переключиться на другое мобильное приложение, такое как ноутбук, или на телефонный звонок, и браузер может потерять свое состояние из-за нехватки памяти на устройстве, поэтому он перезагрузится, когда пользователь вернется.

Проблема снова в том, что вы загрязняете глобальное состояние приложения чем-то, что вам нужно только на нескольких экранах в вашем веб-приложении.

@sompylasar @kpaxqin Я думаю, что @ir-fuel предложил правильный подход — если вам нужно что-то промежуточное, чтобы в конечном итоге сохраниться, сохраните его на стороне клиента: гораздо проще реализовать, на самом деле не вовлекает сервер в сохранение противоречивых данных, не не выполнять никаких http-запросов (что также является серьезной проблемой на мобильных устройствах) и чрезвычайно легко изменить подход в будущем.

А насчет сохранения каких-либо данных где-то — подумайте, как вы будете их очищать после сбоя процесса или уже сделанного — это была начальная тема темы.
Еще один вариант, который вы можете использовать — сохранить данные пользователя в URL-адресе. Если ваши формы состоят из пары текстовых полей, это может быть хорошим компромиссным решением.

Это не столько «сохранить его на стороне клиента или сервера», сколько вопрос «обрабатываю ли я этот« специальный »в моем родительском классе компонентов» против «выполняю ли я весь танец действия/редуктора/состояния».
Мне не нравится проходить действие/редукторы, если это временное состояние. Я делаю это только для реального состояния приложения.

@ir-топливо :+1:

@stremlenye Пожалуйста, прочитайте мой ответ еще раз. Я прямо говорю, что вам не нужно создавать API на стороне сервера, чтобы создать слой API сохранения.

@ir-fuel Невыполнение действий/редуктора отменяет предсказуемость и воспроизводимость переходов между состояниями.

@ir-fuel Вы можете легко очистить состояние своего приложения после отправки формы, чтобы оно не «загрязнялось».

РЕ: расчистка. Сделайте действие ЧИСТЫМ и добавьте кнопку в свой пользовательский интерфейс, чтобы сбросить процесс регистрации. Вы также можете сделать это через маршрутизатор (отправьте это действие при посещении исходного URL-адреса регистрации, и если регистрация началась, измените URL-адрес на что-то другое).

@sompylasar нет проблем сбросить ваше состояние с помощью явной кнопки - проблемы возникают после необработанных сбоев: трудно предсказать, как состояние может быть повреждено, и определить стратегию восстановления для каждого конкретного случая.

@stremlenye да, разработка программного обеспечения иногда бывает сложной. Предсказуемые состояния и переходы помогают смягчить это.

Не только это, но если ваше состояние больше, чем куча объектов json (которые я ненавижу, потому что все по соглашению и ничто не применяется), и вы используете что-то в строке Record , тогда вы вам нужно объявить свойства ваших объектов заранее, и это включает в себя все временные вещи, которые вам когда-либо понадобятся.

Для меня создание чего-то с редуксом без использования Immutable.js — это просто несчастный случай, ожидающий своего часа.

Я знаю, что вы можете сделать запись Signup и сделать ее ненулевой только при регистрации, но проблема более фундаментальна, потому что в вашем веб-приложении может быть много таких случаев, и это то, что беспокоит. меня.

@ir-топливо

Не только это, но если ваше состояние больше, чем куча объектов json (которые я ненавижу, потому что все по соглашению и ничего не применяется), и вы используете что-то в строке записи Immutable.js, вам нужно объявить свойства ваших объектов заранее, и это включает в себя все временные вещи, которые вам когда-либо понадобятся.

Для меня создание чего-то с редуксом без использования Immutable.js — это просто несчастный случай, ожидающий своего часа.

На данный момент я не уверен в использовании Immutable.js с избыточностью, он оказался слишком сложным для интеграции (и неэффективным). Глубокой заморозки во время разработки и тестирования, а также набора тестов должно быть достаточно, чтобы покрыть большинство случаев. И строгое соглашение, чтобы избежать изменения состояния в редьюсерах или других местах, где у вас есть ссылка.

Я знаю, что вы можете создать запись о регистрации, и при регистрации она будет не нулевой.

Да, это то, что я имел в виду.

но проблема более фундаментальна, потому что у вас может быть много таких случаев в вашем веб-приложении, и это меня беспокоит.

Это то, что мы называем явным состоянием приложения — в каждый момент времени мы знаем состояние приложения, которое состоит из множества компонентов. Если вы хотите сделать структуру состояния менее явной и более динамичной, следуйте https://github.com/tonyhb/redux-ui .

Вот и вся проблема JS-разработки. «Давайте придерживаться условностей». И именно поэтому люди начинают понимать, что это просто не работает в сложных средах, потому что люди совершают ошибки и приходят с такими решениями, как TypeScript и Immutable.js.

Почему было бы слишком сложно интегрировать? Если вы используете «Запись», вы используете все эти объекты (пока вы просто их читаете) как обычные объекты JS с точечной записью для доступа к свойствам, с преимуществом, которое вы объявляете заранее, какие свойства являются частью вашего объекта .

Я делаю все свои разработки с редуксом с помощью Immutable.js, без каких-либо проблем. Тот факт, что просто невозможно случайно изменить объект, является большим преимуществом в избыточности, а также удобна «безопасность типов» определения прототипов ваших объектов.

И если вы действительно хотите поддерживать перезагрузку во время регистрации, просто сделайте так, чтобы родительский компонент, управляющий всеми этими шагами, сохранял свое состояние в localStorage.

@ir-топливо
Для меня размещение вещей в localStorage выглядит как побочный эффект, и мне кажется, что делать это в компоненте не рекомендуется.

Проблема снова в том, что вы загрязняете глобальное состояние приложения чем-то, что вам нужно только на нескольких экранах в вашем веб-приложении.

Возможно, я недостаточно ясно описал поддельный API, самая важная причина для его создания - я не хочу помещать эти вещи only need in a few screens в состояние приложения Redux. В FormPage_A/B они просто вызывают API для хранения данных формы, а затем переходят к следующему шагу:

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

и в FormPage_C (окончательная форма):

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

Все эти временные данные находятся в поддельном API, а глобальное состояние приложения не было изменено. Если мы хотим очистить данные в поддельном API, чтобы перезапустить поток, просто отправьте действие, чтобы сделать это.

@sompylasar нет проблем сбросить ваше состояние с помощью явной кнопки - проблемы возникают после необработанных сбоев: трудно предсказать, как состояние может быть повреждено, и определить стратегию восстановления для каждого конкретного случая.

@stremlenye Я согласен с этим, но я думаю, что эти конкретные случаи сбоев, скорее всего, будут бизнес-вещами, они всегда здесь, независимо от того, где мы помещаем состояние, и с ними нужно обращаться правильно, но они просто недостаточно четко описаны в момент. Единственное, что могут сделать разработчики, — упростить отслеживание, чтобы мы могли исправить это с меньшими усилиями.

А размещение состояния в родительском компоненте может затруднить анализ сложных ситуаций. С поддельным API мы мутируем это состояние действиями, и оно кажется более предсказуемым.

Но я согласен, что поддельный API выглядит слишком тяжеловесным в большинстве случаев при меньшей сложности.

Не просмотрел все сообщения выше. Здесь я просто даю свой подход: обнаруживать изменения свойств в componentWillReceiveProps.

Скажем, форма состояния: {loginPending, loginError}, при начале входа установите loginPending = true. В случае успеха или неудачи установите { loginPending: false, loginError: 'какая-то ошибка'. }.

Затем я могу определить, произошло ли событие успешного входа в компонент, и перенаправить страницу:

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

Просто работает, хоть и не красиво.

Моя скромная реализация очистки формы при перенаправлении с использованием react-router-redux . Редуктор:

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

Вы считаете, что такой подход приемлем? Оно работает.

Я закончил с этим. Расскажите, что вы думаете о:

зарегистрировать избыточное действие:

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

Контейнер формы регистрации:

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

и помощники:

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

Что важно — только успешная регистрация попадает в хранилище Redux как действие, иначе ошибка переходит в состояние компонента контейнера. Пользователь получает сообщение об ошибке, затем переходит на другую страницу, затем снова на страницу формы - форма пуста и ошибок нет. Нет действий «USER_REGISTRATION_REQUEST» или «USER_REGISTRATION_FAILURE», поскольку они описывают состояние компонента, а не состояние приложения.

@sunstorymvp

Вы подсластили свой код async / await , и этот сахар скрывает тот факт, что существует три состояния запроса (до, в процессе, после). Эти состояния скрываются в компоненте представления. Запуск запроса игнорируется, а ошибка запроса обрабатывается в представлении и не отправляется через избыточность, поэтому состояние приложения здесь не при чем. Если по какой-то причине компонент, содержащий это состояние, размонтируется, а затем перемонтируется, состояние запроса на регистрацию будет потеряно, а его результат останется необработанным, что может привести к последующим ошибкам (например, пользователь уже зарегистрировался).

Пользователь получает сообщение об ошибке, затем переходит на другую страницу, затем снова на страницу формы - форма пуста и ошибок нет.

Для этого вы можете отправить действие вроде USER_REGISTRATION_RESET при монтировании компонента, которое сбрасывает ошибку в хранилище. Это действие следует обрабатывать только в том случае, если нет ожидающего запроса на регистрацию, иначе результат ожидающего запроса не будет обработан приложением.

спасибо за отзыв.

Вы подсластили свой код с помощью async/await, и этот сахар скрывает тот факт, что есть три состояния запроса (до, в процессе, после)

эти условия существуют независимо от того, использую ли я async/await или нет.

Эти состояния скрываются в компоненте представления.

как и предполагалось, это состояние компонента. Почему это должно быть в состоянии приложения?

Старт запроса игнорируется

в ходе выполнения...

и ошибка запроса обрабатывается в представлении и не отправляется через избыточность

как предполагалось

Если по какой-либо причине компонент, содержащий это состояние, размонтируется, а затем перемонтируется, состояние запроса на регистрацию будет потеряно, а его результат не будет обработан.

это неправда. пользователь будет зарегистрирован и перенаправлен, если запрос будет успешным, независимо от состояния или даже существования компонента. посмотрите на создателя действия register (handleData).

Для этого вы можете отправить действие...

В этом суть. Еще одно действие? Зачем столько шаблонов? Отключите или включите кнопку отправки, покажите ошибки формы или живую проверку, покажите счетчик, пока запрос открыт.. и эти действия приводят к новым действиям, которые восстанавливают состояние по умолчанию.. почему это должно быть в состоянии приложения?

это неправда. пользователь будет зарегистрирован и перенаправлен, если запрос будет успешным, независимо от состояния или даже существования компонента. посмотрите на создателя действия регистрации (handleData).

Да, но это оставляет возможность двух последовательных запросов register , поскольку состояние запроса не отслеживается в состоянии приложения.

Отключите или включите кнопку отправки, покажите ошибки формы или живую проверку, покажите счетчик, пока запрос открыт.. и эти действия приводят к новым действиям, которые восстанавливают состояние по умолчанию.. почему это должно быть в состоянии приложения?

Для предсказуемости и реиграбельности это цель Redux. Имея только действия и начальное состояние, вы можете перевести приложение в определенное состояние, только отправив действия. Если компонент начинает содержать некоторое состояние, состояние приложения начинает распадаться, не становится единого дерева состояний, представляющего состояние приложения.

Однако не относитесь к этому слишком догматично. Существует множество причин помещать вещи в состояние Redux и множество причин оставлять что-то в состоянии локального компонента, в зависимости от вашего сценария. См. http://redux.js.org/docs/FAQ.html#organizing-state-only-redux-state и https://news.ycombinator.com/item?id=11890229.

Хорошо, теперь мне ясно: это зависит. Спасибо!

Я использую componentWillReceiveProps и имею объект auth в моем компоненте. Не уверен, что это вообще помогает.

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

Есть ли что-то неправильное в том, чтобы делать это таким образом?

Как насчет такого подхода http://codereview.stackexchange.com/questions/138296/implementing-redirect-in-redux-middleware?
Передача URL-адреса перенаправления в действие и дополнительный вызов метода next с push из react-router-redux в промежуточном программном обеспечении.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги