Immutable, why does my nested object using fromJS are not immutable when I use reselect

我们两清 提交于 2020-01-17 03:19:08

问题


I have a bug with immutablejs and reselect.

I have the following redux store in my reactjs application :

/*
 * 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;

According to immutableJS documentation, fromJS create immutable object for nested object.

This is why logging this next line works fine :

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

However, this doesn't work anymore when I try to use it with reselect :

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

At first after reading the documentation, I though this was the correct answer for the selectUsername :

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

This is my LoginForm where I handle the changeForm actions:

/**
 * 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;

Here is the , it is including the and is included in

/**
 * 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;

Here is my

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

This is my sagas handling the login :

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,
];

I would gladly appreciate an explanation on why this doesn't select correctly


回答1:


I cannot really explain why your example is not working for you... reselect code shows there is no magic for Immutable.js structure.

This code works for me perfectly (notice I removed one level of "factoring" selectors, so there is no selector = () => (state) => ... anymore; to be honest I can't say it's the root of your problem, but it's not necessary code neither):

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



回答2:


how did you set newFormState payload in action creator changeForm?

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

check if action.newFormState is a plain object? if so you should explicitly set the selected fields.

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


来源:https://stackoverflow.com/questions/40860219/immutable-why-does-my-nested-object-using-fromjs-are-not-immutable-when-i-use-r

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!