Redux: أفضل الممارسات في التعامل مع تدفق البيانات لصفحات تسجيل الدخول / الاشتراك مع إعادة التوجيه

تم إنشاؤها على ٢٢ يوليو ٢٠١٥  ·  66تعليقات  ·  مصدر: reduxjs/redux

تحرير: ما أحاول فهمه هنا هو كيفية التعامل مع تدفق البيانات عندما يكون لديك صفحة بها نموذج. بعد إرسال النموذج ، تقوم إما بإعادة التوجيه إلى صفحة أخرى أو عرض رسالة خطأ .

يتمثل التحدي في المكان الذي يجب أن يتم فيه تخزين هذه المعلومات مؤقتًا في تدفق البيانات. لأنه بعد إعادة توجيه الرسالة أو عرضها ، لم تعد الحالة (أو المعلومات التي قمت بتخزينها) ذات صلة ويجب إعادة تعيينها أو تنظيفها.


في تطبيق الويب الخلفي ، عادة ما يكون لديك صفحات مثل تسجيل الدخول والاشتراك وإعادة تعيين كلمة المرور وما إلى ذلك.

عندما ينتقل المستخدم إلى التطبيق ولم يتم تسجيل دخوله ، تتم إعادة توجيهه إلى صفحة تسجيل الدخول. يمكن القيام بذلك بسهولة باستخدام خطاف 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>
    )
  }
}

نظرًا لأن هذا التدفق صحيح - نأمل :) - يمكنك أن ترى أن لديّ علامتين في حالة التطبيق: 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'
}

لذلك لدينا حالة لا ترتبط بحالة التطبيق ولا أحد يهتم بها. عندما سنعيد زيارة هذا النموذج في المرة القادمة سيظهر محوه وسيختفي "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 أو لا في حالة صفحة تسجيل الدخول ، أو لا نريد لمعرفة أي شيء عن أنه غير كلمة المرور أو فشل: إذا نجح الطلب ، فما عليك سوى إعادة توجيهه إلى مكان آخر.

إذن ، ماذا عن الاقتراح ، لسنا بحاجة إلى هذه الحالة في مكان آخر باستثناء المكون؟ دعنا نعطيها محاولة.
لتوفير المزيد من التنفيذ المتميز ، يمكننا أن نفترض أننا في عام 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 # $ 8 $ # $ ، تطبيق $ flux ، تطبيق state ، القيام بما تم إنشاؤه من أجله: إرسال وإعادة التوجيه.

شكرا للقراءة وآسف لأخذ وقتك.

ال 66 كومينتر

بدلاً من وجود shouldRedirect هناك ، ماذا عن وجود مخفض الجلسة ، الذي يتتبع فقط isLoggedIn ؟ بهذه الطريقة يمكنك إعادة التوجيه من صفحة تسجيل الدخول الخاصة بك إلى أي صفحة أخرى بتحديد isLoggedIn بدلاً من shouldRedirect .

أيضًا ، يمكن إعادة تعيين الحالة بمجرد معالجة الإجراء LOGGED_IN في المخفض. إذا حدث هذا الإجراء ، فأنت تعلم أنه يمكنك إعادة تعيين حالة الخطأ بأمان إلى قيمة null .

هل لهذا معنى؟ :د

من المنطقي أن تكون هناك صفحة تسجيل دخول فقط ، لكنها نهج أكثر عمومية. على سبيل المثال لصفحة التسجيل ، أو صفحة إعادة تعيين كلمة المرور. حتى إذا قمت بتسجيل الدخول ، فلا يزال بإمكانك رؤية هذه الصفحات.

ويجب استخدام هذه العلامات في كل تلك الصفحات ، لأن جميعها لها نفس التدفق.

أتمنى أن يكون الأمر أكثر وضوحًا الآن :)

نعم ، من وجهة نظري "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

أعتقد أن إعادة التوجيه في حالتك أمر غير مقبول ، ويجب أن يتم بطريقة أكثر فاعلية

أنا موافق. الأشياء التي تكون مفيدة مرة واحدة فقط لا ينبغي أن تكون في حالة IMO.

أيضًا ، وفقًا لـ Slack ، لا أعتقد أنه يجب عليك تخزين isLoggedIn في حالة التطبيق ، حيث إنها في الحقيقة خاصية محسوبة ، والتي يمكن حسابها باستخدام محدد بسيط مثل const isUserLoggedIn = state => state.user.id !== null

راجع https://github.com/faassen/reselect لمزيد من استخدام المحدد

frederickfogerty شكرا على المثال.

السؤال على الرغم من: هذا يعمل بشكل جيد لتسجيل الدخول لأن لديك عادة علامة loggedIn في state . ولكن ماذا عن الصفحات الأخرى مثل التسجيل وإعادة تعيين كلمة المرور وما إلى ذلك؟
تعمل هذه الصفحات بنفس سلوك تسجيل الدخول: إعادة التوجيه إذا كان موافقًا ، وإلا تظهر رسالة خطأ.
إذن كيف تتعامل مع هذه الحالات؟ أنا أكافح للعثور على التدفق الصحيح لتلك الحالات ، ولست متأكدًا من المكان الذي يجب أن تعيش فيه الدولة.

الأشياء التي تكون مفيدة مرة واحدة فقط لا ينبغي أن تكون في حالة IMO.

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/adding-authentication-to-your-react-flux-app/

إنه تدفق نقي:
https://github.com/auth0/react-flux-jwt-authentication-sample

ربما هو مفيد لأفضل الممارسات ؟!

تضمين التغريدة

إذن كيف تتعامل مع هذه الحالات؟

طالما أن استخدامك هو تسجيل الدخول ، فسيظل isLoggedIn (الذي ليس في حالة التطبيق ، ولكنه ثانوي) يتغير ، وسيؤدي ذلك إلى إعادة توجيههم. لا يهم من أين جاء هذا التغيير في الحالة :)

أفضل طريقة اتبعتها هي إضافة هذا المنطق إلى موصل حيث تكون الوظيفة مطلوبة لأبنائها ، على سبيل المثال للحصول على إذن ، لنفترض أن لديك مكون AppProtected ، حيث يحتاج كل شيء تابع لهذا المكون إلى إذن ، ثم تضع هذا المنطق في موصل مكون AppProtected.

هذه هي الطريقة التي أفعل بها أجهزة الصراف الآلي دون تخزين أي شيء في الولاية.

أنا بالتأكيد لا أحب هذا النهج ، حيث يتدفق البيانات في اتجاهين بين واجهة المستخدم وأجهزة التكييف ، والتي تتعارض مع التدفق. إذا كنت ملزمًا بفعل ذلك بشكل إلزامي ، فيمكنك فعل شيء مثل

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 . نفترض أيضًا أنه لا ينبغي أن نلمس أي حالة من خلال تخزين نوع من الأعلام ذات الطلقة الواحدة (كما ذكرنا من قبل).
عندما أنقر لإرسال النموذج ، ماذا سيحدث؟

هذا ما أود أن أعرفه ، كيف يجب أن يكون التدفق وأين يجب أن يعيش.

ملاحظة: شكرا لتعليقاتك حتى الآن!

[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'
}

لذلك لدينا حالة لا ترتبط بحالة التطبيق ولا أحد يهتم بها. عندما سنعيد زيارة هذا النموذج في المرة القادمة سيظهر محوه وسيختفي "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 أو لا في حالة صفحة تسجيل الدخول ، أو لا نريد لمعرفة أي شيء عن أنه غير كلمة المرور أو فشل: إذا نجح الطلب ، فما عليك سوى إعادة توجيهه إلى مكان آخر.

إذن ، ماذا عن الاقتراح ، لسنا بحاجة إلى هذه الحالة في مكان آخر باستثناء المكون؟ دعنا نعطيها محاولة.
لتوفير المزيد من التنفيذ المتميز ، يمكننا أن نفترض أننا في عام 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 # $ 8 $ # $ ، تطبيق $ flux ، تطبيق state ، القيام بما تم إنشاؤه من أجله: إرسال وإعادة التوجيه.

شكرا للقراءة وآسف لأخذ وقتك.

عادلة بما فيه الكفاية! لأكون صادقًا ، ما زلت مهتمًا بالنظر في تخزين حالة المكون المحلي في Redux: # 159.

stremlenye هذا هو بقعة جميلة. شكرا لك للمشاركة!

iclanzan thx للقراءة :)
في الواقع الآن أفكر في اقتراح gaearon الذي ذكره في التعليق السابق. ألق نظرة على المناقشة رقم 159 للحصول على بعض الأفكار المهمة.

الخاتمة - عند إصدار RR 1.0 ، سنغطي ذلك في وصفة "Usage with React Router".
يمكن تتبع ذلك في https://github.com/rackt/redux/issues/637.

stremlenye نهجك رائع جدا! هل من المنطقي - بدلاً من استخدام المصمم - استخدام مكون عالي الترتيب لجهاز التوجيه التفاعلي حيث يتم دمج جميع العناصر _ غير الممرضة_ وسيكون مسؤولاً عن إعادة التوجيه بمجرد أن يكون لدى متجر المصادقة مستخدم؟

<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 قام بعمل رائع لإظهار كيف يمكن القيام بذلك. تكمن المشكلة في أن المرء يميل إلى الوقوع في "كل شيء يحتاج إلى المرور عبر الإجراء - المخفض - تحديث المكون مع حلقة حالة المتجر" (وأنا أيضًا) ثم نميل بعد ذلك إلى تلويث المتجر بجميع أنواع الحالة التي تُستخدم فقط في سيناريوهات محددة جدًا في التطبيق (على شاشة واحدة ، أشياء مثل "هل نفذت بنجاح إعادة تعيين كلمة المرور") ، بينما يمكن التعامل مع هذا بسهولة عن طريق إجراء المكالمات غير المتزامنة داخل المكون وتحديث المكون نفسه باستخدام this.setState بدلاً من تلويث الحالة العالمية + الاضطرار إلى إعادة تعيين جميع متغيرات الحالة هذه في حال كنا سنفتح هذا المكون مرة أخرى لاحقًا. لم يذكر حتى تلويث مخفضات الحالة بكل كود معالجة البداية / النجاح / الخطأ.

سأحاول تكييف الكود الخاص بي هنا أيضًا لمعرفة ما إذا كان ينظف الأشياء.

@ ir-fuel إذا كان لديك وقت هل يمكنك دفع مثال صغير لما ستحصل عليه في جيثب؟

حسنًا ، سيكون من الصعب القيام بذلك ، لكن يمكنني إعطاء مثال صغير لما فعلته. لدي مكون حيث يمكن للمستخدم أن يطلب إعادة تعيين كلمة المرور الخاصة به. تحدث عملية إعادة الضبط الفعلية على النحو التالي الآن:

  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 أنا أؤيد حالة التطبيق ، أعتقد فقط أنه طالما أن المستخدم خارج تطبيقي ، يمكنني تقديم تطبيق أبسط له بدون متجر إعادة. سيبدأ متجر redux الخاص بي بمجرد إدخال تطبيقي.

نجح هذا النهج معي حتى هذه اللحظة ، لكن ربما سأفهم الحاجة إلى حالة تطبيق مرحلة تسجيل الدخول في مشاريعي المستقبلية

يمكنك على مستوى الجذر لمتجرك تقسيم متجرك في جزء "التطبيق" وجزء "المستخدم / تسجيل الدخول" ، ولكل منهما مخفض خاص به. لذلك في هذه الحالة لن تلوث مخفضات التطبيق الخاص بك بكود مخفض تسجيل الدخول والعكس صحيح.

من: SkateFreak < [email protected] [email protected] >
الرد على: Rackt / redux < [email protected] [email protected] >
التاريخ: الأحد 13 ديسمبر 2015 13:48
إلى: Rackt / redux < [email protected] [email protected] >
نسخة إلى: يوريس مان < [email protected] [email protected] >
الموضوع: Re: [إعادة] أفضل الممارسات في معالجة تدفق البيانات لصفحات تسجيل الدخول / الاشتراك مع إعادة التوجيه (# 297)

sompyl asarhttps: //github.com/sompylasar أنا أؤيد حالة التطبيق ، أعتقد فقط أنه طالما أن الاستخدام خارج تطبيقي ، يمكنني أن أقدم له تطبيقًا أبسط بدون متجر إعادة. سيبدأ متجر redux الخاص بي بمجرد إدخال تطبيقي.

نجح هذا النهج معي حتى هذه اللحظة ، لكن ربما سأفهم الحاجة إلى حالة تطبيق مرحلة تسجيل الدخول في مشاريعي المستقبلية

يمكنك الرد على هذه الرسالة الإلكترونية مباشرةً أو عرضها على Gi tHubhttps: //github.com/rackt/redux/issues/297#issuecomment -164287194.

صححني إذا كنت مخطئًا ولكن ليس الغرض من مخزن التدفق لمشاركة البيانات وحالة التطبيق بين المسارات والمكونات؟ لماذا أحتاج إلى "تقليل" حالتي واستخدام "أفعالي" للحفاظ على أعلى مستوى إذا لم يكن كل ذلك مهمًا عندما يقوم المستخدم بتسجيل الدخول؟

لا أرى المزايا ، على الرغم من أنها طريقة منظمة للغاية ونظيفة للقيام بالأشياء. أقوم بإدارة حالة تسجيل الدخول المسبق الخاصة بي في المكون <Sign /> الخاص بي ، والذي يحتوي على تسجيل الدخول و SignUp و ForgotPassword.

لأن الاشتراك قد يكون عملية أكثر تعقيدًا من مجرد طلب اسم مستخدم وكلمة مرور.
كل هذا يتوقف على مدى تعقيد تدفقك.

من: SkateFreak < [email protected] [email protected] >
الرد على: Rackt / redux < [email protected] [email protected] >
التاريخ: الأحد 13 ديسمبر 2015 14:00
إلى: Rackt / redux < [email protected] [email protected] >
نسخة إلى: يوريس مان < [email protected] [email protected] >
الموضوع: Re: [إعادة] أفضل الممارسات في معالجة تدفق البيانات لصفحات تسجيل الدخول / الاشتراك مع إعادة التوجيه (# 297)

صححني إذا كنت مخطئًا ولكن ليس الغرض من مخزن التدفق لمشاركة البيانات وحالة التطبيق بين المسارات والمكونات؟ لماذا أحتاج إلى "تقليل" حالتي واستخدام "أفعالي" للحفاظ على أعلى مستوى إذا لم يكن كل ذلك مهمًا عندما يقوم المستخدم بتسجيل الدخول؟

لا أرى المزايا ، على الرغم من أنها طريقة منظمة للغاية ونظيفة للقيام بالأشياء. أقوم بإدارة حالة تسجيل الدخول المسبق الخاصة بي في المكون الخاص بي ، والذي يحتوي على تسجيل الدخول و 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 من تدفق إعادة التحميل لحفظها؟

تضمين التغريدة _

أود فقط تمرير الخصائص الموجودة في 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

شكرا لاقتراحاتكم.
يبدو أننا جميعًا لا نريد وضع هذه الأشياء المؤقتة في حالة التطبيق.

ولكن عندما يقوم المستخدم بتحديث الصفحة في نصف التدفق

فقط تأكد من أنه إذا قرر المستخدم إعادة تحميل الصفحة التي تبدأ من البداية مرة أخرى.

يبدو أنها ليست تجربة مستخدم جيدة ، فنحن موقع جوّال (وهذا أحد أسباب تقسيم الشكل الضخم إلى صفحات صغيرة) ، ومن المؤلم إدخال أشياء على الهاتف المحمول ، وأعتقد أن المستخدم سيصاب بالجنون إذا فقد كل الأشياء بعد الضغط على زر التحديث ، فربما يتخلون عنها.

أعتقد أنه ربما يكون إنشاء fake api للخروج من التدفق المعاد اختيارًا مقبولاً ، أرسل الأشياء إلى fake 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 ، إنها مجرد واجهة برمجة تطبيقات تخزين وسيطة تحتاجها لعمليتك متعددة الخطوات. لا يجب بالضرورة أن تكون كل واجهة برمجة تطبيقات على خادم ويمكن الوصول إليها عبر HTTP أو بروتوكول شبكة آخر.

ما أتفق مع @ ir-fuel عليه هو أنه يجب تخزين كل حالة العملية متعددة الخطوات ، وليس الخطوة الحالية فقط.

@ ir-fuel Reload يمكن أن يحدث بدون قصد صريح من المستخدم ، على سبيل المثال ، يمكن للمستخدم التبديل إلى تطبيق جوال آخر ، مثل جهاز كمبيوتر محمول ، أو مكالمة هاتفية ، وقد يفقد المتصفح حالته بسبب عدم كفاية الذاكرة على الجهاز ، لذلك سيتم إعادة التحميل عندما يعود المستخدم.

تكمن المشكلة مرة أخرى في أنك تلوث حالة التطبيق العالمية بشيء لا تحتاجه إلا في بضع شاشات في تطبيق الويب الخاص بك.

sompylasarkpaxqin أعتقد أن @ ir-fuel اقترح النهج الصحيح - إذا كنت بحاجة إلى شيء متوسط ​​لتستمر في النهاية ، استمر في ذلك من جانب العميل: أسهل بكثير في التنفيذ ، لا يتضمن الخادم في الواقع استمرار عدم اتساق البيانات ، أليس كذلك ' لإجراء أي طلبات http (وهي أيضًا مشكلة كبيرة على الأجهزة المحمولة) ومن السهل للغاية تغيير النهج في المستقبل.

وحول استمرار أي بيانات في مكان ما - فكر في كيفية تنظيفها بعد فشل العملية أو تنفيذها بالفعل - كان هذا هو الموضوع الأولي للموضوع.
خيار آخر يمكنك استخدامه - الاحتفاظ ببيانات المستخدم في عنوان url. إذا كانت النماذج الخاصة بك تتكون من حقول نصية مزدوجة ، فقد يكون ذلك حلاً جيدًا للمقايضة.

لا يتعلق الأمر كثيرًا "بتخزينه من جانب العميل أو الخادم" ، بل يتعلق الأمر أكثر بـ "هل أتعامل مع هذا" المخصص "في صنف المكون الرئيسي" مقابل "هل أقوم برقصة الحركة / المخفض / الحالة بالكامل".
لا أحب المرور بالإجراء / مخفضات السرعة إذا كانت لحالة مؤقتة. أفعل هذا فقط لحالة التطبيق الحقيقية.

@ ir-fuel: +1:

stremlenye الرجاء قراءة إجابتي مرة أخرى. أقول صراحة أنك لست مضطرًا إلى إنشاء واجهة برمجة تطبيقات من جانب الخادم لإنشاء طبقة واجهة برمجة تطبيقات دائمة.

@ ir-fuel عدم المرور بالإجراءات / المخفض يلغي إمكانية التنبؤ وإمكانية إعادة تشغيل انتقالات الحالة.

@ ir-fuel يمكنك بسهولة مسح حالة تطبيقك بعد إرسال النموذج ، حتى لا يتم "تلويثه".

RE: المقاصة. قم بإجراء مسح ولديك زر في واجهة المستخدم لإعادة تعيين تدفق الاشتراك. يمكنك إجراء ذلك عبر جهاز التوجيه أيضًا (أرسل هذا الإجراء عند زيارة عنوان URL الأولي للتسجيل ، وإذا بدأ التسجيل ، فقم بتغيير عنوان URL إلى شيء آخر).

sompylasar لا توجد مشكلة في إعادة ضبط حالتك باستخدام زر واضح - تحدث المشكلات بعد حالات فشل غير معالج: من الصعب التنبؤ بالطرق التي يمكن أن تتلف بها الحالة وتحديد استراتيجية استعادة لكل حالة معينة.

stremlenye نعم ، هندسة البرمجيات صعبة في بعض الأحيان. تساعد الحالات والتحولات التي يمكن التنبؤ بها على التخفيف من حدة ذلك.

ليس هذا فقط ، ولكن إذا كانت حالتك أكثر من مجرد مجموعة من كائنات json (التي أكرهها لأن كل شيء يتم بالصدفة ولا يتم فرض أي شيء) وكنت تستخدم شيئًا في سطر Immutable.js ' Record فأنت تحتاج إلى الإعلان عن خصائص العناصر الخاصة بك مقدمًا وهذا يشمل جميع الأشياء المؤقتة التي تريدها هناك.

بالنسبة لي ، فإن بناء شيء باستخدام redux دون استخدام Immutable.js هو مجرد حادث ينتظر حدوثه.

أعلم أنه يمكنك إنشاء سجل Signup ولا يكون فارغًا إلا عند التسجيل ، لكن المشكلة أكثر جوهرية ، لأنه يمكن أن يكون لديك الكثير من هذه الحالات في تطبيق الويب الخاص بك ، وهذا ما يزعجك أنا.

@ ir- الوقود

ليس هذا فقط ، ولكن إذا كانت حالتك أكثر من مجرد مجموعة من كائنات json (التي أكرهها لأن كل شيء يتم بالصدفة ولا يتم فرض أي شيء) وكنت تستخدم شيئًا في سطر سجل Immutable.js ، فأنت بحاجة إلى إعلان الخصائص من كائناتك مقدمًا وهذا يتضمن كل الأشياء المؤقتة التي تريدها هناك.

بالنسبة لي ، فإن بناء شيء باستخدام redux دون استخدام Immutable.js هو مجرد حادث ينتظر حدوثه.

في الوقت الحالي ، لست مقتنعًا باستخدام Immutable.js مع redux ، فقد بدا الأمر معقدًا للغاية بحيث لا يمكن تكامله (وليس مؤديًا). يجب أن يكون التجميد العميق أثناء التطوير والاختبار ، ومجموعة من الاختبارات كافية لتغطية معظم الحالات. واتفاقية صارمة لتجنب تحور الحالة في مخفضات أو أماكن أخرى حيث يكون لديك المرجع.

أعلم أنه يمكنك إنشاء سجل اشتراك وجعله غير خالي فقط عند التسجيل

نعم ، هذا ما كان يدور في خلدي.

لكن المشكلة أكثر جوهرية ، لأنه يمكن أن يكون لديك الكثير من هذه الحالات في تطبيق الويب الخاص بك ، وهذا ما يزعجني.

هذا ما نسميه حالة التطبيق الصريحة - في كل لحظة من الزمن نعرف حالة التطبيق الذي يتكون من العديد من المكونات. إذا كنت ترغب في جعل بنية الحالة أقل وضوحًا وأكثر ديناميكية ، فاتبع https://github.com/tonyhb/redux-ui

هذه هي المشكلة برمتها في تطوير JS. "دعونا نتمسك بالاتفاقيات". ولهذا السبب يبدأ الناس في إدراك أنه لا يعمل في البيئات المعقدة ، لأن الناس يرتكبون أخطاء ، ويأتون بحلول مثل TypeScript و Immutable.js.

لماذا يكون الدمج معقدًا جدًا؟ إذا كنت تستخدم "تسجيل" ، فأنت تستخدم كل هذه الكائنات (طالما أنك تقرأها فقط) ككائنات JS عادية ، مع تدوين نقطي للوصول إلى الخصائص ، كميزة أنك تعلن مقدمًا الخصائص التي تشكل جزءًا من الكائن الخاص بك .

أقوم بكل ما عندي من تطوير للإعادة مع Immutable.js ، دون أي مشاكل على الإطلاق. إن حقيقة أنه لا يمكن ببساطة عن طريق الصدفة تعديل كائن ما هي ميزة كبيرة في إعادة التشغيل و "أمان النوع" لتحديد نماذج الكائن الخاصة بك مناسب أيضًا.

وإذا كنت تريد حقًا دعم إعادة تحميل منتصف التسجيل ، فكل ما عليك هو أن يكون المكون الرئيسي الذي يتحكم في كل هذه الخطوات يخزن حالته في localStorage.

@ ir- الوقود
بالنسبة لي ، فإن وضع الأشياء في localStorage يبدو وكأنه أثر جانبي ويبدو أنه ليس من الممارسات الجيدة القيام بذلك في المكون.

تكمن المشكلة مرة أخرى في أنك تلوث حالة التطبيق العالمية بشيء لا تحتاجه إلا في بضع شاشات في تطبيق الويب الخاص بك.

ربما لم أصف واجهة برمجة التطبيقات المزيفة بشكل واضح بما فيه الكفاية ، والسبب الأكثر أهمية لإنشائها هو أنني لا أريد وضع هذه الأشياء only need in a few screens في حالة تطبيق redux. في FormPage_A / B ، يتصلون فقط بواجهة برمجة التطبيقات لتخزين بيانات النموذج ثم الانتقال إلى الخطوة التالية:

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

كل هذه البيانات المؤقتة تعيش في واجهة برمجة تطبيقات مزيفة ولم يتم تلويث حالة التطبيق العالمية. إذا أردنا مسح البيانات في واجهة برمجة التطبيقات المزيفة لإعادة تشغيل التدفق ، فما عليك سوى إرسال الإجراء للقيام بذلك.

sompylasar لا توجد مشكلة في إعادة ضبط حالتك باستخدام زر واضح - تحدث المشكلات بعد حالات فشل غير معالج: من الصعب التنبؤ بالطرق التي يمكن أن تتلف بها الحالة وتحديد استراتيجية استعادة لكل حالة معينة.

stremlenye أتفق مع ذلك ، لكنني أعتقد أن حالات الفشل المعينة هذه من المرجح أن تكون أشياء تجارية ، فهي دائمًا هنا بغض النظر عن المكان الذي نضع فيه الدولة ، ويجب التعامل معها بالطريقة الصحيحة ولكن لم يتم وصفها بشكل واضح بما فيه الكفاية في اللحظة. الشيء الوحيد الذي يمكن للمطورين فعله هو تسهيل التتبع حتى نتمكن من إصلاحه بأقل ألم.

ووضع الحالة في المكون الرئيسي قد يجعل من الصعب التفكير في الوقت الذي تصبح فيه الأمور معقدة. باستخدام 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" لأنها تصف حالة المكون ، وليس حالة التطبيق.

تضمين التغريدة

لقد قمت بتحليل الكود الخاص بك بـ async / await ، وهذا السكر يخفي حقيقة أن هناك ثلاث حالات للطلب (قبل ، قيد التقدم ، بعد). يتم إخفاء هذه الحالات في مكون العرض. يتم تجاهل بدء الطلب ، ويتم التعامل مع خطأ الطلب في العرض ولا يتم إرساله عبر إعادة الإرسال ، وبالتالي فإن حالة التطبيق خارج العمل هنا. إذا تم إلغاء تثبيت المكون الذي يحتوي على هذه الحالة لسبب ما ثم إعادة تحميله ، فستفقد حالة طلب التسجيل ولن تتم معالجة نتيجته ، وقد يؤدي ذلك إلى أخطاء لاحقة (على سبيل المثال ، تم تسجيل المستخدم بالفعل).

يتلقى المستخدم خطأ ، ثم ينتقل إلى صفحة أخرى ، ثم مرة أخرى إلى صفحة النموذج - النموذج فارغ ولا توجد أخطاء.

لتحقيق ذلك ، يمكنك إرسال إجراء مثل USER_REGISTRATION_RESET على تثبيت المكون والذي من شأنه إعادة تعيين الخطأ في المتجر. يجب التعامل مع هذا الإجراء فقط في حالة عدم وجود طلب تسجيل معلق ، وإلا فلن يتم معالجة نتيجة الطلب المعلق بواسطة التطبيق.

شكرا لملاحظاتك.

لقد قمت بتحليل الكود الخاص بك مع عدم التزامن / انتظار ، وهذا السكر يخفي حقيقة أن هناك ثلاث حالات للطلب (قبل ، قيد التقدم ، بعد)

توجد هذه الشروط بغض النظر عما إذا كنت أستخدم التزامن / انتظار أم لا.

يتم إخفاء هذه الحالات في مكون العرض

على النحو المنشود ، هذه هي حالة المكون .. لماذا يجب أن يكون هذا في حالة التطبيق؟

تم تجاهل بدء الطلب

في تقدم...

ويتم التعامل مع خطأ الطلب في العرض ولا يتم إرساله عبر إعادة الإرسال

على النحو المنشود

إذا تم إلغاء تحميل المكون الذي يحتوي على هذه الحالة لسبب ما ثم إعادة تحميله ، فستفقد حالة طلب التسجيل ولن تتم معالجة نتيجته

هذا ليس صحيحا. سيتم تسجيل المستخدم وإعادة توجيهه إذا كان الطلب ناجحًا ، بغض النظر عن حالة المكون أو حتى الوجود. انظر إلى منشئ الإجراء register (handleData).

لتحقيق ذلك ، يمكنك إرسال إجراء ...

هذه هي النقطة. عمل آخر؟ لماذا كل هذا النمطي؟ تعطيل أو تمكين زر الإرسال ، إظهار أخطاء النموذج أو التحقق المباشر ، إظهار القرص الدوار عندما يكون الطلب مفتوحًا .. وتؤدي هذه الإجراءات إلى إجراءات جديدة تستعيد الحالة الافتراضية .. لماذا يجب أن يكون هذا في حالة التطبيق؟

هذا ليس صحيحا. سيتم تسجيل المستخدم وإعادة توجيهه إذا كان الطلب ناجحًا ، بغض النظر عن حالة المكون أو حتى الوجود. انظر إلى منشئ الإجراء المسجل (handleData).

نعم ، ولكن هذا يترك إمكانية طلبين متتاليين register لأن حالة الطلب لا يتم تعقبها في حالة التطبيق.

تعطيل أو تمكين زر الإرسال ، إظهار أخطاء النموذج أو التحقق المباشر ، إظهار القرص الدوار عندما يكون الطلب مفتوحًا .. وتؤدي هذه الإجراءات إلى إجراءات جديدة تستعيد الحالة الافتراضية .. لماذا يجب أن يكون هذا في حالة التطبيق؟

من أجل إمكانية التنبؤ وإعادة التشغيل ، هذا هو الغرض من إعادة التشغيل. بوجود الإجراءات والحالة الأولية فقط ، يمكنك نقل التطبيق إلى حالة معينة من خلال إرسال الإجراءات فقط. إذا بدأ أحد المكونات في احتواء بعض الحالات ، فإن حالة التطبيق تبدأ في التفكك ، ولا تصبح هناك شجرة حالة واحدة تمثل حالة التطبيق.

لا تكن دوغمائيًا بشكل مفرط حيال ذلك. هناك الكثير من الأسباب لوضع الأشياء في حالة إعادة التشغيل ، والعديد من الأسباب لترك شيء ما في حالة المكون المحلي ، اعتمادًا على السيناريو الخاص بك. راجع 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 التقييمات