2016-11-29 26 views
0

我有一個immutablejs和重新選擇的錯誤。不可變的,爲什麼我使用fromJS的嵌套對象在使用重選時不是不可變的

我在我的reactjs應用以下Redux的店:

/* 
* The reducer takes care of our data 
* Using actions, we can change our application state 
* To add a new action, add it to the switch statement in the homeReducer function 
* 
* Example: 
* case YOUR_ACTION_CONSTANT: 
* return assign({}, state, { 
*  stateVariable: action.var 
* }); 
*/ 

import { fromJS } from 'immutable'; 

import { 
    CHANGE_FORM, 
    SENDING_REQUEST, 
    REQUEST_SUCCESS, 
    CLEAR_SUCCESS, 
    REQUEST_ERROR, 
    CLEAR_ERROR, 
} from './constants'; 

// The initial application state 
const initialState = fromJS({ 
    formState: { 
    username: 'dka', 
    password: '', 
    }, 
    success: false, 
    error: false, 
    isCurrentlySending: false, 
}); 

console.log(initialState.getIn(['formState','username'])); 

// Takes care of changing the application state 
function loginReducer(state = initialState, action) { 
    switch (action.type) { 
    case CHANGE_FORM: 
     return state 
     .set('formState', action.newFormState); 
    case SENDING_REQUEST: 
     return state 
     .set('isCurrentlySending', action.sending); 
    case REQUEST_SUCCESS: 
     return state 
     .set('success', action.success) 
     .set('isCurrentlySending', false); 
    case REQUEST_ERROR: 
     return state 
     .set('error', action.error) 
     .set('isCurrentlySending', false); 
    case CLEAR_SUCCESS: 
     return state 
     .set('success', null); 
    case CLEAR_ERROR: 
     return state 
     .set('error', null); 
    default: 
     return state; 
    } 
} 

export default loginReducer; 

據immutableJS文檔,fromJS創建嵌套的對象不可變對象。

這就是爲什麼這個記錄下一行正常工作:

console.log(initialState.getIn(['formState','username'])); 
// or 
console.log(initialState.get('formState').get('username'); 

然而,這並不能再當我嘗試與重新選擇使用它的工作:

import { createSelector } from 'reselect'; 

const selectLogin =() => (state) => state.get('login'); 

const selectFormState =() => createSelector(
    selectLogin(), 
    (loginState) => loginState.get('formState') 
); 

const selectUsername =() => createSelector(
    selectFormState(), 
    // (formState) => formState.username // work fine but disabled because username should be accessed using .get or .getIn 
    (formState) => formState.get('username') // doesn't work because formState is a plain object 
); 

起初閱讀後文檔,我雖然這是selectUsername的正確答案:

const selectUsername =() => createSelector(
    selectLogin(), 
    selectFormState(), 
    (formState) => formState.get('username') 
); 

這是我LoginForm的,我處理changeForm操作:

/** 
* LoginForm 
* 
* The form with a username and a password input field, both of which are 
* controlled via the application state. 
* 
*/ 
import React from 'react'; 
import Input from 'components/bootstrap/atoms/Input'; 
import Label from 'components/bootstrap/atoms/Label'; 
import H2 from 'components/bootstrap/atoms/H2'; 
import Form from 'components/bootstrap/atoms/Form'; 
import Button from 'components/bootstrap/atoms/Button'; 
import LoadingButton from 'components/kopax/atoms/LoadingButton'; 
import { FormattedMessage } from 'react-intl'; 
import messages from './messages'; 
import { url } from 'config'; 
import { changeForm, requestError, clearError, clearSuccess } from 'containers/LoginPage/actions'; 
import Alert from 'components/bootstrap/atoms/Alert'; 
import LocaleToggle from 'containers/LocaleToggle'; 

export class LoginForm extends React.Component { // eslint-disable-line react/prefer-stateless-function 

    static propTypes = { 
    isCurrentlySending: React.PropTypes.bool.isRequired, 
    onSubmit: React.PropTypes.func.isRequired, 
    data: React.PropTypes.object.isRequired, 
    success: React.PropTypes.object, 
    error: React.PropTypes.object, 
    dispatch: React.PropTypes.func.isRequired, 
    }; 

    render() { 
    const { success, error } = this.props; 
    return (
     <Form action={url.login} onSubmit={this.onSubmit}> 
     <H2><FormattedMessage {...messages.title} /></H2> 
     {success && <Alert className="alert-success" onDismiss={this.hideSuccess}><FormattedMessage {...success} /></Alert>} 
     {error && <Alert className="alert-danger" onDismiss={this.hideError}><FormattedMessage {...error} /></Alert>} 
     <Label htmlFor="username"><FormattedMessage {...messages.username} /></Label> 
     <Input 
      type="text" 
      onChange={this.changeUsername} 
      placeholder="bob" 
      autoCorrect="off" 
      autoCapitalize="off" 
      spellCheck="false" 
     /> 
     <Label htmlFor="password"><FormattedMessage {...messages.password} /></Label> 
     <Input 
      type="password" 
      onChange={this.changePassword} 
      placeholder="••••••••••" 
     /> 
     {this.props.isCurrentlySending ? (
      <LoadingButton className="btn-primary"> 
      <FormattedMessage {...messages.buttonLogin} /> 
      </LoadingButton> 
     ) : (
      <div> 
      <LocaleToggle /> 
      <Button className="primary"> 
       <FormattedMessage {...messages.buttonLogin} /> 
      </Button> 
      </div> 
     )} 
     </Form> 
    ); 
    } 

    // Change the username in the app state 
    changeUsername = (evt) => { 
    const newState = this.mergeWithCurrentState({ 
     username: evt.target.value, 
    }); 
    this.emitChange(newState); 
    } 
    // Change the password in the app state 
    changePassword = (evt) => { 
    const newState = this.mergeWithCurrentState({ 
     password: evt.target.value, 
    }); 
    this.emitChange(newState); 
    } 
    // Merges the current state with a change 
    mergeWithCurrentState(change) { 
    return this.props.data.merge(change); 
    } 
    // Emits a change of the form state to the application state 
    emitChange(newState) { 
    this.props.dispatch(changeForm(newState)); 
    } 
    // onSubmit call the passed onSubmit function 
    onSubmit = (evt) => { 
    evt.preventDefault(); 
    const username = this.props.data.get('username').trim(); 
    const password = this.props.data.get('password').trim(); 
    const isValidated = this.validateForm(username, password); 
    if (isValidated) { 
     this.props.onSubmit(username, password); 
    } else { 
     this.props.dispatch(requestError(messages.errorFormEmpty)); 
    } 
    } 
    // validate the form 
    validateForm(username, password) { 
    this.props.dispatch(clearError()); 
    this.props.dispatch(clearSuccess()); 
    return username.length > 0 && password.length > 0; 
    } 

    hideError =() => { 
    this.props.dispatch(clearError()); 
    } 

    hideSuccess =() => { 
    this.props.dispatch(clearSuccess()); 
    } 

} 

export default LoginForm; 

這裏是,它包括與被包含在

/** 
* FormPageWrapper 
*/ 

import React from 'react'; 
import Alert from 'components/bootstrap/atoms/Alert'; 
import styled, { keyframes } from 'styled-components'; 
import defaultThemeProps from 'styled/themes/mxstbr/organisms/FormPageWrapper'; 
import LoginForm from '../../molecules/LoginForm'; 
import { FormattedMessage } from 'react-intl'; 
import messages from './messages'; 
import cn from 'classnames'; 

const propTypes = { 
    isCurrentlySending: React.PropTypes.bool.isRequired, 
    onSubmit: React.PropTypes.func.isRequired, 
    className: React.PropTypes.string, 
    data: React.PropTypes.object.isRequired, 
    success: React.PropTypes.oneOfType([ 
    React.PropTypes.object, 
    React.PropTypes.bool, 
    ]), 
    error: React.PropTypes.oneOfType([ 
    React.PropTypes.object, 
    React.PropTypes.bool, 
    ]), 
    dispatch: React.PropTypes.func.isRequired, 
}; 

const defaultProps = { 
    theme: { 
    mxstbr: { 
     organisms: { 
     FormPageWrapper: defaultThemeProps, 
     }, 
    }, 
    }, 
}; 

class FormPageWrapper extends React.Component { 
    render() { 
    const { className, onSubmit, dispatch, data, isCurrentlySending, success, error } = this.props; 
    return (
     <div className={cn(className, 'form-page__wrapper')}> 
     <div className="form-page__form-wrapper"> 
      <div className="form-page__form-header"> 
      <h2 className="form-page__form-heading"><FormattedMessage {...messages.title} /></h2> 
      </div> 
      {success && <Alert className="mx-2 alert-success" onDismiss={this.hideSuccess}><FormattedMessage {...success} /></Alert>} 
      {error && <Alert className="mx-2 alert-danger" onDismiss={this.hideError}><FormattedMessage {...error} /></Alert>} 
      <LoginForm 
      onSubmit={onSubmit} 
      data={data} 
      dispatch={dispatch} 
      isCurrentlySending={isCurrentlySending} 
      /> 
     </div> 
     </div> 
    ); 
    } 

} 


const shake = keyframes` 
    0% { 
    transform: translateX(0); 
    } 
    25% { 
    transform: translateX(10px); 
    } 
    75% { 
    transform: translateX(-10px); 
    } 
    100% { 
    transform: translateX(0); 
    } 
`; 

// eslint-disable-next-line no-class-assign 
FormPageWrapper = styled(FormPageWrapper)` 
    ${(props) => ` 

    margin-top: ${props.theme.mxstbr.organisms.FormPageWrapper['$margin-x']}; 

    &.form-page__wrapper { 
     display: flex; 
     align-items: center; 
     justify-content: center; 
     height: 100%; 
     width: 100%; 
    } 

    .form-page__form-wrapper { 
     max-width: 325px; 
     width: 100%; 
     border: 1px solid ${props.theme.mxstbr.organisms.FormPageWrapper['$very-light-grey']}; 
     border-radius: 3px; 
     box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); 
     background-color: #fff; 
    } 

    .form-page__form-heading { 
     text-align: center; 
     font-size: 1em; 
     user-select: none; 
    } 

    .form-page__form-header { 
     padding: 1em; 
    } 

    & .js-form__err-animation { 
     animation: ${shake} 150ms ease-in-out; 
    } 

    `} 
`; 

FormPageWrapper.propTypes = propTypes; 
FormPageWrapper.defaultProps = defaultProps; 

export default FormPageWrapper; 

這裏是我的

/* 
* LoginPage 
* 
* This is the first thing users see of our App, at the '/' route 
* 
*/ 
import React from 'react'; 
import { connect } from 'react-redux'; 
import { createStructuredSelector } from 'reselect'; 
import { selectLogin } from './selectors'; 
import { loginRequest } from './actions'; 
import FormPageWrapper from 'components/mxstbr/organisms/FormPageWrapper'; 

export class LoginPage extends React.Component { // eslint-disable-line react/prefer-stateless-function 

    static propTypes = { 
    data: React.PropTypes.object.isRequired, 
    dispatch: React.PropTypes.func.isRequired, 
    onSubmitFormLogin: React.PropTypes.func.isRequired, 
    }; 

    render() { 
    const dispatch = this.props.dispatch; 
    const formState = this.props.data.get('formState'); 
    const isCurrentlySending = this.props.data.get('isCurrentlySending'); 
    const success = this.props.data.get('success'); 
    const error = this.props.data.get('error'); 
    return (
     <FormPageWrapper 
     onSubmit={this.props.onSubmitFormLogin} 
     success={success} 
     error={error} 
     data={formState} 
     dispatch={dispatch} 
     isCurrentlySending={isCurrentlySending} 
     /> 
    ); 
    } 

} 

export function mapDispatchToProps(dispatch) { 
    return { 
    dispatch, 
    onSubmitFormLogin: (username, password) => { 
     dispatch(loginRequest({ username, password })); 
    }, 
    }; 
} 

const mapStateToProps = createStructuredSelector({ 
    data: selectLogin(), 
}); 

// Wrap the component to inject dispatch and state into it 
export default connect(mapStateToProps, mapDispatchToProps)(LoginPage); 

這是我的傳奇處理登錄:

import { getParameter } from 'utils/request'; 
import { pages, oauthClient, storage } from 'config'; 
import { browserHistory } from 'react-router'; 
import { takeLatest } from 'redux-saga'; 
import { take, call, put, fork, race, select, cancel } from 'redux-saga/effects'; 
import { LOCATION_CHANGE } from 'react-router-redux'; 
import auth from 'services/auth'; 
import { selectUsername, selectPassword } from './selectors'; 

// login actions 
import { sendingRequest, clearSuccess, clearError, requestError, changeForm } from './actions'; 
import { 
    LOGIN_REQUEST, 
} from './constants'; 
// app action solicited in LoginPage 
import { 
    logout, 
    setAuthState, 
} from 'containers/App/actions'; 
import { 
    LOGOUT, 
} from 'containers/App/constants'; 

/** 
* Effect to handle authorization 
* @param {string} username    The username of the user 
* @param {string} password    The password of the user 
* @param {object} options    Options 
* @param {boolean} options.isRegistering Is this a register request? 
*/ 
export function* getAuthorize({ username, password, isRegistering }) { 
    try { // eslint-disable-line padded-blocks 

    // We send an action that tells Redux we're sending a request 
    yield put(sendingRequest(true)); 

    // make a first request to generate the cookie seession and include it in the login request 
    yield call(auth.preLogin); 

    // For either log in or registering, we call the proper function in the `auth` 
    // module, which is asynchronous. Because we're using generators, we can work 
    // as if it's synchronous because we pause execution until the call is done 
    // with `yield`! 
    let links; 
    if (isRegistering) { 
     links = yield call(auth.register, username, password); 
    } else { 
     links = yield call(auth.login, username, password); 
    } 
    if (links.err) { 
     throw links.err; 
    } 

    localStorage.setItem(storage.LINKS, JSON.stringify(links._links)); // eslint-disable-line no-underscore-dangle 

    // Now that we are logged in, we are eligible for a code request (see oauth2) 
    const fetchCode = yield call(auth.code, oauthClient.clientId, oauthClient.redirectUri); 
    const responseCodeUrl = yield fetchCode.url; 

    // let's get the token 
    const code = getParameter('code', responseCodeUrl); 

    if (!code) { 
     return false; 
    } 
    const jwt = yield call(auth.token, oauthClient.clientId, oauthClient.clientSecret, code, oauthClient.redirectUri, oauthClient.scopes); 

    if (!jwt) { 
     return false; 
    } 
    // TODO : use sessionStorage and localStorage only if Remember me button was checked (do we do a remember me button) 
    localStorage.setItem(storage.TOKEN, JSON.stringify(jwt)); 

    return jwt; 
    } catch (error) { 
    // If we get an error we send Redux the appropiate action and return 
    yield put(requestError({ id: 'com.domain.api.messages', defaultMessage: error.message })); 

    return false; 
    } finally { 
    // When done, we tell Redux we're not in the middle of a request any more 
    yield put(sendingRequest(false)); 
    } 
} 


/** 
* Log in saga 
*/ 
export function* getLogin() { 
    yield put(clearError()); 
    yield put(clearSuccess()); 
    const username = yield select(selectUsername()); 
    const password = yield select(selectPassword()); 
    // A `LOGOUT` action may happen while the `authorize` effect is going on, which may 
    // lead to a race condition. This is unlikely, but just in case, we call `race` which 
    // returns the 'winner', i.e. the one that finished first 
    const winner = yield race({ 
    auth: call(getAuthorize, { username, password, isRegistering: false }), 
    logout: take(LOGOUT), 
    }); 

    // If `authorize` was the winner... 
    if (winner.auth) { 
    // ...we send Redux appropiate actions 
    yield put(setAuthState(true)); // User is logged in (authorized) 
    yield put(changeForm({ username: '', password: '' })); // Clear form 
    forwardTo(pages.pageDashboard.path); // Go to dashboard page 
    // If `logout` won... 
    } else if (winner.logout) { 
    // ...we send Redux appropiate action 
    yield put(setAuthState(false)); // User is not logged in (not authorized) 
    yield call(logout); // Call `logout` effect 
    forwardTo(pages.pageLogin.path); // Go to root page 
    } 
} 

/** 
* Watches for LOGIN_REQUEST actions and calls getLogin when one comes in. 
* By using `takeLatest` only the result of the latest API call is applied. 
*/ 
export function* getLoginWatcher() { 
    yield fork(takeLatest, LOGIN_REQUEST, getLogin); 
} 

/** 
* Root saga manages watcher lifecycle 
*/ 
export function* loginData() { 
    // Fork watcher so we can continue execution 
    console.log('starting lifecycle'); 
    const watcher = yield fork(getLoginWatcher); // eslint-disable-line no-unused-vars 
    console.log('take location change'); 
    yield take(LOCATION_CHANGE); 
    console.log('canceling', watcher.toString()); 
    yield cancel(watcher); // <=== SEE WHY THIS TRIGGER ERROR "utils.js:202 uncaught at getLogin Generator is already running" 
    console.log('canceled'); 
} 

// Little helper function to abstract going to different pages 
function forwardTo(location) { 
    browserHistory.push(location); 
} 

export default [ 
    loginData, 
]; 

我很樂意解釋爲什麼這個選項不正確

+0

您能分享減速器代碼嗎? –

+0

請顯示'CHANGE_FORM'動作創建者的所有電話。 –

+0

嗯,所以在傳遞給這個組件的'onSubmit'回調中創建'CHANGE_FORM'行爲?你能否也包括它的代碼? –

回答

2

我無法真正解釋爲什麼你的例子不適合你... reselect代碼顯示there is no magic Immutable.js結構。

此代碼完美適用於我(請注意,我刪除了一個「因式分解」選擇器級別,因此沒有selector =() => (state) => ...了;說實話,我不能說它是問題的根源,但它不是必需的代碼):

const { createSelector } = require('reselect'); 
const { fromJS } = require('immutable'); 

const initialState = fromJS({ 
    login: { 
    formState: { 
     username: 'dka', 
     password: '', 
    }, 
    success: false, 
    error: false, 
    isCurrentlySending: false, 
    } 
}); 

const selectLogin = (state) => state.get('login'); 

const selectFormState = createSelector(
    selectLogin, 
    (loginState) => loginState.get('formState') 
); 

const selectUsername = createSelector(
    selectFormState, 
    (formState) => formState.get('username') 
); 

console.log(selectUsername(initialState)); 
+0

感謝您的回答,給你投票的答案,因爲@aifarfa已經有好的分數:] – BigDong

2

你是如何在action creator中設置newFormState載荷的changeForm

case CHANGE_FORM: 
    return state 
    .set('formState', action.newFormState); 

檢查action.newFormState是一個普通對象嗎?如果是的話,你應該明確地設置選定的字段。

// .. 
return state.setIn(['formState', 'username'], action.newFormState.username) 
+1

你在哪裏沒錯。但是這對於很長的形式來說並不是很方便。我剛把它改成'return state.set('formState',fromJS(action.newFormState))' – BigDong

相關問題