BigDong BigDong - 9 days ago 6
React JSX Question

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

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

Answer

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