编辑:我想在这里理解的是当你有一个带有表单的页面时如何处理数据流。 提交表单后,您要么重定向到另一个页面,要么呈现错误消息。
挑战在于这些信息应该在数据流中的_临时存储_位置。 因为在您重定向或呈现消息后,状态(或您存储的信息)不再相关,应该重置或清除。
在后端 Web 应用程序中,您通常具有登录、注册、重置密码等页面。
当用户导航到应用程序但未登录时,他会被重定向到登录页面。 这可以通过 RR onEnter
钩子轻松完成。
然后用户将填写表格并提交。 此时通常如果请求成功,您希望重定向到应用程序根目录或其他一些“安全”页面。 如果失败,您希望留在页面上并显示错误消息。
(这不仅适用于登录页面,也适用于其他页面以及前面提到的。我现在只关注登录)
到目前为止,您有这个流程(这是常见的用例):
/
/login
/
(或其他页面)如果我们用 redux 实现这个流程,我想我有以下设置:
// 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(...)
}
}
有人遇到过这个问题吗? 我错过了什么还是“正确”的方法?
还有其他选择吗?
非常欢迎任何反馈! :)
与其有shouldRedirect
,不如有一个会话缩减器,它只跟踪isLoggedIn
? 这样,您可以通过检查isLoggedIn
而不是shouldRedirect
从登录页面重定向到任何其他页面。
此外,只需在 reducer 中处理LOGGED_IN
操作即可重置状态。 如果发生该操作,您知道您可以安全地将错误状态重置为null
值。
这有意义吗? :D
如果只有登录页面是有道理的,但这是一种更通用的方法。 例如注册页面或密码重置页面。 即使您已登录,您仍然应该能够看到这些页面。
所有这些页面都应该使用这些标志,因为它们都有相同的流程。
希望现在更清楚:)
是的,从我的角度来看,“shouldRedirect”太笼统了,无法与某个动作相关联。 它可能会打开错误,您不知何故忘记设置该标志,然后页面重定向到它实际上不应该重定向到的另一个页面。
但可以肯定的是,如果你想清理你的状态,你可以通过一个动作来做到这一点。 或者你可以在 react 之外监听历史变化,然后触发状态清除动作。 这可能更可取,因为您不必将该逻辑分散在整个组件中,但您可以将其保存在一个地方。
但这只是状态清除的可能解决方案。 我还不确定如何以一种好的方式解决重定向。
问题也是:在该州拥有这些旗帜是否有意义? 如果不是,这个流程应该如何实施?
@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);
};
}
它没有被重构,它仅用于我的路由器解决方案的测试目的:)。
这是我的应用程序的复制和粘贴,以及我如何处理加载用户数据和登录。我会尽量评论代码,我会在 reactiflux 上提问。
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
很好 imo,只需确保它处于登录状态,例如state.auth.errorMessage
,而不是顶级errorMessage
我认为在您的状态下拥有 shouldRedirect 是不可接受的,应该以更实用的方式完成
我同意。 只有一次有用的东西不应该处于 IMO 状态。
另外,根据 slack,我认为您不应该将isLoggedIn
存储在应用程序状态中,因为它实际上是一个计算属性,可以使用像const isUserLoggedIn = state => state.user.id !== null
这样的简单选择器来计算
有关更多选择器用法,请参阅https://github.com/faassen/reselect
@frederickfogerty感谢您的示例。
但是问题:这对于登录很好,因为您通常在state
中有一个loggedIn
标志。 但是其他页面,如注册、密码重置等呢?
这些页面的行为与登录相同:如果正常则重定向,否则显示错误消息。
那么你如何处理这些情况呢? 我正在努力为这些案例找到正确的流程,我不确定该州应该住在哪里。
只有一次有用的东西不应该处于 IMO 状态。
@gaearon我同意,但我仍然不确定什么是正确的方法。 你仍然需要在某个地方有一个状态......
这就是我在不存储任何内容的情况下使用 atm 的方式。
// 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”或以不同的方式完成。 这个替代方案是什么?
相关的,我将把它固定在这里......实际上我也在处理这个问题,目前正在尝试将以下示例重新实现到 Redux 中。 还没有什么可展示的,因为我在空闲时间这样做:
https://auth0.com/blog/2015/04/09/adding-authentication-to-your-react-flux-app/
它是纯通量:
https://github.com/auth0/react-flux-jwt-authentication-sample
也许它对最佳实践有用?!
@emmenko
那么你如何处理这些情况呢?
只要您的使用正在登录,那么isLoggedIn
(不在应用程序状态,但较小)仍然会更改,这将重定向它们。 它不在乎状态变化来自哪里:)
我采取的最佳方法是将此逻辑添加到连接器中,其子级需要该功能,例如授权,假设您有一个AppProtected
组件,该组件的所有子级都需要授权,然后你把这个逻辑放在 AppProtected 组件的连接器中。
这就是我在不存储任何内容的情况下使用 atm 的方式。
我绝对不喜欢这种方法,它在 UI 和 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]
这远没有登录和注册页面那么优雅,因为它非常耦合
我要睡觉了,明天再回答问题
所以我们再次将标志存储在状态中;)
PS:晚安!
我认为这个问题比概括“提交表格”要深一些——它更多的是关于实际状态是什么,哪些数据应该出现在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'
}
所以我们得到了一些与应用程序状态无关的状态,没有人关心它。 当我们下次重新访问此表单时,它会显示为已删除,并且“John 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
,或者我们不希望要知道他是否更改了密码或失败:如果请求成功,只需将他重定向到其他地方。
那么,除了 Component,我们在其他地方不需要这个状态的命题怎么样? 试一试吧。
为了提供更多花哨的实现,我们可以假设 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 路由器”一节中介绍这一点。
这可以在https://github.com/rackt/redux/issues/637 中进行跟踪。
@stremlenye你的方法非常棒! 是否有意义 - 而不是使用装饰器 - 使用 react-router 高阶组件,其中所有 _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做得很好,展示了它是如何完成的。 问题在于,人们往往会陷入“一切都需要通过动作-reducer-update-component-with-store-state 循环”(我也是),然后我们倾向于用各种污染商店仅在应用程序中非常特定的场景中使用的状态(在 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 调用的函数。
因此,无需往返于 store/reducers,不会用您只关心一个组件内部的东西污染全局状态,并且纯粹用于临时显示该组件中的状态,并且无需在您的 reducer 中重置一堆状态变量。
和渲染看起来像这样
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
东西。
我认为可以将您的应用程序状态视为已登录状态。 如果没有用户登录,为什么需要应用状态? 当然,您可以在 Login 组件中拥有一个。
登录页面上的应用程序逻辑不应该那么复杂来拥有一个应用程序顶级状态,对吧?
@SkateFreak登录流程是应用程序的一部分。 你有没有想过你最好使用 React 的登录流状态(进度、错误)、注册流状态等(如果你已经将它用于其他应用程序组件,可能与 Redux 一起使用)。
@sompylasar我赞成应用程序状态,我只是认为只要用户在我的应用程序之外,我就可以为他提供一个更简单的应用程序,而无需使用 redux 商店。 一旦你进入我的应用程序,我的 redux 商店就会启动。
这种方法一直对我有用,但也许我会理解在我未来的项目中需要登录阶段的应用程序状态
您可以在商店的根级别将商店拆分为“应用程序”部分和“用户/登录”部分,每个部分都有自己的减速器。 因此,在这种情况下,您将永远不会使用登录减速器代码污染您的应用减速器,反之亦然。
来自:SkateFreak < [email protected] [email protected] >
回复:rackt/redux <回复@ reply.github.com回复@ reply.github.com >
日期:2015 年 12 月 13 日星期日 13:48
To: rackt/redux < [email protected] [email protected] >
抄送:Joris Mans < [email protected] [email protected] >
主题:回复:[redux] 使用重定向处理登录/注册页面数据流的最佳实践 (#297)
@sompyl asarhttps://github.com/sompylasar我赞成应用程序状态,我只是认为只要在我的应用程序之外使用,我就可以为他提供一个更简单的应用程序,而无需使用 redux 商店。 一旦你进入我的应用程序,我的 redux 商店就会启动。
这种方法一直对我有用,但也许我会理解在我未来的项目中需要登录阶段的应用程序状态
直接回复此邮件或在 Gi tHub上查看 https://github.com/rackt/redux/issues/297#issuecomment -164287194。
如果我错了,请纠正我,但通量存储的目的不是在路由和组件之间共享数据和应用程序状态吗? 如果用户登录时所有这些都无关紧要,为什么我需要“减少”我的状态并使用我的“操作”来维持最高状态?
我看不出有什么好处,尽管这是一种非常有条理和干净的做事方式。 我在我的<Sign />
组件中管理我的登录前状态,其中包含 SignIn、SignUp 和 ForgotPassword。
因为注册可能是一个比仅仅要求用户名和密码更复杂的过程。
这一切都取决于您的流程的复杂性。
来自:SkateFreak < [email protected] [email protected] >
回复:rackt/redux <回复@ reply.github.com回复@ reply.github.com >
日期:2015 年 12 月 13 日星期日 14:00
To: rackt/redux < [email protected] [email protected] >
抄送:Joris Mans < [email protected] [email protected] >
主题:回复:[redux] 使用重定向处理登录/注册页面数据流的最佳实践 (#297)
如果我错了,请纠正我,但通量存储的目的不是在路由和组件之间共享数据和应用程序状态吗? 如果用户登录时所有这些都无关紧要,为什么我需要“减少”我的状态并使用我的“操作”来维持最高状态?
我看不出有什么好处,尽管这是一种非常有条理和干净的做事方式。 我在我的组件中管理我的登录前状态,其中包含 SignIn、SignUp 和 ForgotPassword。
直接回复此邮件或在 Gi tHub上查看 https://github.com/rackt/redux/issues/297#issuecomment -164287893。
我同意。
但我认为 SignUp 是一种形式,有一个状态 - 无论 imo 多么复杂,这应该足够了......
另一方面,我对注册有什么了解...
人们疯了
目前我遇到了一些类似但不同的问题。
我正在处理一个超级复杂的注册流程,我们有一些继续页面让用户输入所需的信息,每个页面都是一个表单。 流程如下所示:
FormPage_A -> FormPage_B -> FormPage_C -> SuccessPage
问题是我们没有在 FormPage_C 之前发出任何 API 请求。 我们只是将浏览器中的数据保存在 FormPage_A/B 上,然后将它们全部发送到 FormPage_C 中的 API。
那么问题来了,我们应该把FormPage_A和FormPage_B的表单数据放到应用状态吗?
如果是,似乎我们正在将一些对 UI 没有影响的临时数据保存到应用程序状态中。 而且由于redux的应用程序状态是存在于内存中的,那么当用户在FormPage_B中刷新页面时会发生什么?
如果没有,是放置它的正确位置吗? 我们是否应该在 redux 流中构建另一个像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>
@ir-燃料@stremlenye
感谢您的建议。
看来我们都不想把那些临时的东西放到应用状态。
但是当用户在一半的流程中刷新页面时
只要确保如果用户决定重新加载您从头开始的页面。
看起来用户体验不太好,我们是移动网站(这也是我们将大页面拆分成小页面的原因之一),在移动设备上输入内容很痛苦,我相信如果他们失去所有内容,用户会发疯点击刷新按钮后的事情,也许他们会放弃它。
我认为也许从 redux 流程中构建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);
}
这对我来说很清楚,但是将我们的 redux 流程添加到我们的应用程序中看起来很奇怪。
我真的不在乎用户重新加载您的页面。
另一方面,你告诉我们你有一个复杂的注册过程,有几个表格,然后你说它是一个移动网站。 如果他们必须在移动设备上填写太多东西,您不认为这也会让用户发疯并让他们放弃吗? 我认为这比不支持重新加载还要糟糕。 无论如何,我认为许多网站都不支持在注册过程中重新加载页面。
我认为这只是让事情变得更加复杂,因为这些事情真的不会发生很多。 我会更专注于尝试简化注册过程,或者在您的网站上将其拆分为多个步骤,这样用户就不会觉得他在最终激活帐户之前填写了 X 页表格。
如果你真的想支持注册中期重新加载,只需让控制所有这些步骤的父组件将其状态存储在localStorage
中。
@kpaxqin我会采用“假 API”方法。 为什么要伪造 BTW,它只是多步骤流程所需的中间存储 API。 并非每个 API 都必须位于服务器上并通过 HTTP 或其他网络协议访问。
我同意@ir-fuel 的观点是应该存储所有多步骤过程状态,而不仅仅是当前步骤。
@ir-fuel Reload 可能会在没有明确用户意图的情况下发生,例如,用户可以切换到另一个移动应用程序,如笔记本或电话,并且浏览器可能会由于设备上的内存不足而丢失其状态,所以它当用户回来时将重新加载。
问题再次是您正在污染全局应用程序状态,而您只需要在 web 应用程序的几个屏幕中使用这些东西。
@sompylasar @kpaxqin我认为@ir-fuel 建议了正确的方法——如果你需要中间的东西最终被持久化,那么在客户端持久化它:更容易实现,不会真正让服务器参与持久化不一致的数据,不会' t 执行任何 http 请求(这也是移动设备上的一个主要问题)并且将来非常容易改变方法。
关于将任何数据保存在某个地方——想想在过程失败或已经完成后你将如何清理它——这是一个主题的最初主题。
您可以使用的另一个选项 - 在 url 中保留用户数据。 如果您的表单由几个文本字段组成,这可能是一个很好的权衡解决方案。
与其说是“将其存储在客户端或服务器端”,不如说是“我是否在我的父组件类中处理这个'ad hoc'”与“我是否执行整个动作/减速器/状态舞”。
如果是临时状态,我不喜欢通过动作/减速器。 我只为真正的应用程序状态这样做。
@ir-燃料:+1:
@stremlenye请再次阅读我的答案。 我明确地说,您不必制作服务器端 API 来制作持久性 API 层。
@ir-fuel 不执行操作/reducer 会取消状态转换的可预测性和可重放性。
@ir-fuel 您可以在提交表单后轻松清除您的应用程序状态,因此它不会被“污染”。
回复:清算。 清除操作并在您的 UI 中有一个按钮来重置注册流程。 您也可以通过路由器进行操作(在访问初始注册 URL 时发送该操作,如果注册已经开始,请将 URL 更改为其他内容)。
@sompylasar使用显式按钮重置您的状态没有问题 - 问题发生在未处理的故障之后:很难预测状态可能被破坏的方式并为每个特定情况定义恢复策略。
@stremlenye是的,软件工程有时很难。 可预测的状态和转换有助于缓解这种情况。
不仅如此,如果你的状态不仅仅是一堆 json 对象(我讨厌它,因为一切都是按照惯例,没有强制执行)并且你使用 Immutable.js 的Record
行中的东西,那么你需要预先声明对象的属性,其中包括您想要的所有临时内容。
对我来说,在不使用 Immutable.js 的情况下使用 redux 构建东西只是等待发生的意外。
我知道你可以创建一个Signup
记录,并且在注册时只让它不为空,但问题是更根本的,因为你的 webapp 中可能有很多这样的案例,这就是困扰我。
@ir-燃料
不仅如此,如果你的状态不仅仅是一堆 json 对象(我讨厌这一切,因为一切都是约定俗成的,没有强制执行)并且你在 Immutable.js 的 Record 行中使用了一些东西,那么你需要声明属性预先准备好您的对象,其中包括您想要的所有临时内容。
对我来说,在不使用 Immutable.js 的情况下使用 redux 构建东西只是等待发生的意外。
目前,我不相信将 Immutable.js 与 redux 一起使用,它似乎太复杂而无法集成(而且性能不佳)。 开发和测试过程中的深度冻结,一组测试应该足以涵盖大部分情况。 还有一个严格的约定,以避免在减速器或您有参考的其他地方发生变异。
我知道您可以创建一个注册记录,并且只有在注册时它不能为空
是的,这就是我的想法。
但问题更为根本,因为您的 web 应用程序中可能有很多这样的案例,而这正是困扰我的地方。
这就是我们所说的显式应用程序状态——在每时每刻我们都知道应用程序的状态,它由许多组件组成。 如果你想让状态结构不那么明确和更动态,请关注https://github.com/tonyhb/redux-ui
这就是 JS 开发的全部问题。 “让我们遵守约定”。 这就是为什么人们开始意识到它在复杂的环境中不起作用,因为人们会犯错误,并使用 TypeScript 和 Immutable.js 等解决方案。
为什么集成起来会太复杂? 如果您使用“记录”,您可以将所有这些对象(只要您只是在阅读它们)作为普通的 JS 对象使用,并使用点符号来访问属性,这样做的好处是您可以预先声明哪些属性是您的对象的一部分.
我正在使用 Immutable.js 进行所有 redux 开发,没有任何问题。 不能偶然修改对象这一事实是 redux 的一大优势,并且定义对象原型的“类型安全”也很方便。
如果你真的想支持中间注册重新加载,只需让控制所有这些步骤的父组件将其状态存储在 localStorage 中。
@ir-燃料
对我来说,将东西放入 localStorage 看起来像是副作用,在组件中这样做似乎不是一个好习惯。
问题再次是您正在污染全局应用程序状态,而您只需要在 web 应用程序的几个屏幕中使用这些东西。
可能我对fake 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中的prop变化。
假设状态形状为:{ loginPending, loginError },当开始登录时,设置 loginPending = true。 当成功或失败时,设置 { loginPending: false, loginError: 'some error.' }。
然后我可以检测组件中的登录成功事件并重定向页面:
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
};
// ...
您认为这种方法可以吗? 有用。
我结束了这个。 说出你的想法:
注册 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));
};
注册表单容器:
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
给你的代码加了糖,这个糖隐藏了请求有三种状态(之前、进行中、之后)的事实。 这些状态隐藏在视图组件中。 请求开始被忽略,请求错误在视图中得到处理,并没有通过 redux 分派,所以应用程序状态在这里停止运行。 如果由于某种原因,包含该状态的组件卸载然后重新安装,注册请求的状态将丢失并且其结果将未处理,这可能导致后续错误(例如用户已经注册)。
用户收到错误,然后导航到另一个页面,然后再次导航到表单页面 - 表单为空且没有错误。
为了实现这一点,您可以在组件挂载上调度USER_REGISTRATION_RESET
之类的操作,以重置商店中的错误。 仅当没有待处理的注册请求时才应处理此操作,否则待处理的请求结果不会被应用程序处理。
感谢您的反馈。
你已经用 async / await 给你的代码加了糖,这个糖隐藏了请求有三种状态(之前、进行中、之后)的事实
无论我是否使用 async/await,这些条件都存在。
这些状态隐藏在视图组件中
正如预期的那样,这是组件状态..为什么这应该处于应用状态?
请求开始被忽略
进行中...
并且请求错误在视图中得到处理,并且不会通过 redux 分派
如预期
如果由于某种原因包含此状态的组件卸载然后重新安装,则注册请求的状态将丢失并且其结果将未处理
那不是真的。 如果请求成功,用户将被注册并重定向,无论组件状态甚至存在。 查看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 传递给操作,并在中间件中使用push
中的react-router-redux
额外调用next
方法。
最有用的评论
我认为这个问题比概括“提交表格”要深一些——它更多的是关于实际状态是什么,哪些数据应该出现在
store
中,哪些不出现。我喜欢将通量数据流的
View
部分视为纯函数render(state): DOM
,但这并不完全正确。让我们以呈现两个输入字段的简单表单组件为例:
在输入此字段期间,执行了一些内部状态修改,因此我们将在最后得到类似的内容:
所以我们得到了一些与应用程序状态无关的状态,没有人关心它。 当我们下次重新访问此表单时,它会显示为已删除,并且“John Snow”将永远消失。
看起来我们修改了 DOM 而没有触及通量数据流。
让我们假设这是一些通知信的订阅表格。 现在我们应该与外部世界交互,我们将使用我们的特殊动作来完成它。
当用户按下提交按钮时,他期望用户界面的任何响应是成功还是失败以及下一步该做什么。 因此,我们希望跟踪请求状态以呈现加载屏幕、重定向或显示错误消息。
第一个想法是调度一些内部操作,如
loading
、submitted
、failed
以使应用程序状态对此负责。 因为我们要渲染状态而不考虑细节。但是这里出现了问题:现在我们保持了这个页面状态,并且下一个
render(state)
调用将在脏状态下显示表单,因此我们需要创建cleanup
操作来在每次挂载表单时调用它。对于我们拥有的每种形式,我们将添加相同的样板
loading
,submitted
,failed
和cleanup
操作与商店/减速器......直到我们停下来思考关于我们实际想要渲染的状态。submitted
是属于应用程序状态还是只是组件状态,例如name
字段值。 用户是否真的关心这个状态被持久化在缓存中,或者他想在重新访问时看到干净的表单? 答案是:视情况而定。但是在纯提交表单的情况下,我们可以假设表单状态与应用程序状态无关:在登录页面的情况下,我们想知道用户是否是
logged in
,或者我们不希望要知道他是否更改了密码或失败:如果请求成功,只需将他重定向到其他地方。那么,除了 Component,我们在其他地方不需要这个状态的命题怎么样? 试一试吧。
为了提供更多花哨的实现,我们可以假设 2015 年我们生活在异步世界中,并且到处都有
Promises
,所以让我们的操作返回Promise
:下一步,我们可以开始跟踪
Component
中的操作:看起来我们已经拥有了渲染组件所需的一切,而不会污染应用程序状态,并且不需要清理它。
所以我们快到了。 现在我们可以开始概括
Component
流了。首先让我们关注点:跟踪动作和状态更新反应:
(让我们按照
redux
来做,以保持在回购范围内)和
现在,一方面我们有
decorator
提供submitted
和error
道具与onSubmit
回调,另一方面decorator
如果重定向某处获取属性submitted === true
。 让我们将它们附加到我们的表单中,看看我们得到了什么。这里是:纯渲染表单,不依赖
routing
库,flux
实现,应用程序state
,执行它的创建目的:提交和重定向。感谢您的阅读,很抱歉占用您的时间。