Alexander Kireyev
Alexander Kireyev

Reputation: 10825

Error while splitting Reducer

All below code was in 1 file at the start of refactoring and worked well. I simplified code a little.

My reducers folder:

index.js:

import { combineReducers } from 'redux'
import address from './address'
import questions from './questions'

export default combineReducers({
  address,
  questions
});

initialState.js:

import { uniqueID } from '../utils/index';

const defaultQuestion = {
  title: 'What is the address of the property?',
  id: 0,
  question_type: 'address'
};

export const initialState = {
  questions: [defaultQuestion],
  sessionID: uniqueID(),
  session: {},
  currentQuestion: defaultQuestion,
  currentAnswer: '',
  addressSelectd: false,
  amount: 0,
  address: {
    address: {},
    isPendind: false,
    isRejected: false,
    isFulfilled: false,
    message: '',
  }
};

address.js:

import {
  ON_SELECT_ADDRESS,
  SAVE_ADDRESS_PENDING,
  SAVE_ADDRESS_FULFILLED,
  SAVE_ADDRESS_REJECTED,
} from '../constants/Constants';
import { initialState } from './initialState'
import { nextQuestion } from './questions'

export default function reduce(state = initialState, action) {
  switch (action.type) {
  case ON_SELECT_ADDRESS:
    return {...state,
      currentAnswer: action.payload,
      addressSelectd: true
    };

  case SAVE_ADDRESS_PENDING:
    return {...state,
      address: {
        isPendind: true,
      },
    };

  case SAVE_ADDRESS_FULFILLED:
    return {...state,
      address: {
        isPendind: false,
        isRejected: false,
        isFulfilled: true,
        address: action.payload.address,
      },
      amount: action.payload.amount,
      currentAnswer: '',
      currentQuestion: nextQuestion(state),
    };

  case SAVE_ADDRESS_REJECTED:
    // if (action.payload == 'incorrect_address')
    return {...state,
      currentAnswer: '',
      address: {
        address: {},
        isPendind: false,
        isFulfilled: false,
        isRejected: true,
        message: 'Please find valid address',
      },
    };

  default:
    return state;
  }
}

questions.js:

import {
  ON_CHANGE_ANSWER,
  ON_CHANGE_QUESTION,
  GET_QUESTIONS,
  CREATE_SESSION,
  SAVE_ANSWER,
  SAVE_CURRENT_ANSWER,
  ON_FINISH,
} from '../constants/Constants';
import { initialState } from './initialState'
import { isNullOrUndefined } from 'util';

    export const nextQuestion = (state) => {
      let nextId = state.currentQuestion.direct_question_id;
      if (isNullOrUndefined(nextId)) {
        if (state.currentAnswer === 'yes') {
          nextId = state.currentQuestion.yes_question_id;
        } else if (state.currentAnswer === 'no') {
          nextId = state.currentQuestion.no_question_id;
        }
      }
      return state.questions.find((q) => {
        return q.id === nextId;
      });
    }

export default function reduce(state = initialState, action) {
  switch (action.type) {
  case ON_CHANGE_ANSWER:
    return {...state,
      currentAnswer: action.payload
    };

  case ON_CHANGE_QUESTION:
    return {...state,
      currentQuestion: action.payload
    };

  case GET_QUESTIONS:
    return {...state,
      questions: action.payload,
      currentQuestion: action.payload[0]
    };

  case CREATE_SESSION:
    return {...state,
      session: action.payload,
    };

  case SAVE_CURRENT_ANSWER:
    return {...state,
      currentAnswer: action.payload,
    };

  case SAVE_ANSWER:
    return {...state,
      currentAnswer: '',
      currentQuestion: nextQuestion(state),
    };

  case ON_FINISH:
    return initialState;

  default:
    return state;
  }
}

I have a bunch of errors in Chrome console, like:

Warning: Failed prop type: Invalid prop `questions` of type `object` supplied to `MyApp`, expected `array`.
Warning: Failed prop type: The prop `currentAnswer` is marked as required in `MyApp`, but its value is `undefined`.

But only for questions reducer. And If I add console.log in initialState file, I saw it only 1 time ( I suppose should show 2 times)

Seems questions reducer had not been added to root reducer.

configureStore:

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import reducers from '../reducers/index';
import { createLogger } from 'redux-logger';
import DevTools from '../web/containers/DevTools';

const createDevStoreWithMiddleware = compose(
  applyMiddleware(thunk),
  applyMiddleware(createLogger()),
  DevTools.instrument()
)(createStore);

export default function configureStore() {
  const store = createDevStoreWithMiddleware(reducers);

  return store;
}

Updated:

App.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import AddressSearch from '../components/AddressSearch';
import FinalScreen from '../components/FinalScreen';
import {
  onChangeAnswer,
  onChangeQuestion,
  getQuestions,
  saveAnswer,
  createSession,
  onFinish
} from '../../actions/actions';

class MyApp extends Component {

  static propTypes = {
    questions: PropTypes.array.isRequired,
    sessionID: PropTypes.string.isRequired,
    session: PropTypes.object.isRequired,
    currentQuestion: PropTypes.object.isRequired,
    currentAnswer: PropTypes.string.isRequired,
    address: PropTypes.object.isRequired,
    amount: PropTypes.number,
  };

  componentDidMount() {
    this.props.actions.getQuestions();
    this.props.actions.createSession();
  }

  onReset() {
    this.props.actions.onFinish();
    this.componentDidMount();
  }

  nextQuestion(text) {
    if (text.length > 0) {
      this.props.actions.saveAnswer(text);
    }
  }

  renderAnswers() {
    const props = this.props;
    if (props.currentQuestion.question_type === 'address') {
      return <AddressSearch
        currentAnswer={props.currentAnswer}
        message={props.address.message}
        />;
    } else if (props.currentQuestion.question_type === 'text') {
      return [
        <input
          className="question-input"
          value={props.currentAnswer}
          onChange={(event) => props.actions.onChangeAnswer(event.target.value)}
        />,
        <button
          className="main-button"
          onClick={() => this.nextQuestion(props.currentAnswer)}>
            NEXT
        </button>
      ];
    } else if (props.currentQuestion.question_type === 'bool') {
      return [
        <button
          className="yes-no-button"
          onClick={() => this.nextQuestion('yes')}>
            YES
        </button>,
        <button
          className="yes-no-button"
          onClick={() => this.nextQuestion('no')}>
            NO
        </button>
      ];
    } else if (props.currentQuestion.question_type === 'screen') {
      return (
        <button
          className="main-button"
          onClick={() => this.onReset()}>
            Back
        </button>
      );
    }
  }

  containerInner() {
    if (this.props.currentQuestion.question_type === 'success') {
      return <FinalScreen amount={this.props.amount} />;
    } else {
      return [
        <div key={0} className="question">
          {this.props.currentQuestion.title}
        </div>,
        <div key={1} className="answer">
          {this.renderAnswers()}
        </div>
      ];
    }
  }

  render() {
    return (
      <div className="react-native-web">
        {this.containerInner()}
      </div>
      );
    }
  }

const mapStateToProps = (state) => {
  return state;
};

const mapDispatchToProps = (dispatch) => {
  return {
    actions: {
      getQuestions: () => dispatch(getQuestions()),
      createSession: () => dispatch(createSession()),
      saveAnswer: (text) => dispatch(saveAnswer(text)),
      onChangeAnswer: (text) => dispatch(onChangeAnswer(text)),
      onChangeQuestion: (obj) => dispatch(onChangeQuestion(obj)),
      onFinish: () => dispatch(onFinish()),
    }
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(MyApp);

Upvotes: 1

Views: 85

Answers (1)

yadav_vi
yadav_vi

Reputation: 1297

In the mapStateToProps,

const mapStateToProps = (state) => {
  return state;
};

the state should be split according to its reducers. This is because of combineReducers.

So, when you want to get the actual state when using combineReducers you will have to do something like this -

const mapStateToProps = (state) => {
  return state.address; // or state.question
};

If you want to send all the data of the state (i.e. belonging to both reducers), you can do something like this -

const mapStateToProps = (state) => {
  return Object.assign({}, state.address, state.question);
};

or you will have to handle it in the reducer code.

NOTE: I haven't tried it, you will have to be careful while doing this because since a separate object is being created, it might cause problems with updating.


EDIT: Some thought about the implementation.

PS: I think the reducer design isn't correct. What I mean is both the address as well as questions reducer have the same initial state. So when you do a combineReducer(), the store.getState() (i.e. the store state) becomes something like this -

state = {
  address: {
    questions: [{
      title: 'What is the address of the property?',
      id: 0,
      question_type: 'address'
    }],
    sessionID: 1234,
    session: {},
    currentQuestion: defaultQuestion,
    currentAnswer: '',
    addressSelectd: false,
    amount: 0,
    address: {
      address: {},
      isPendind: false,
      isRejected: false,
      isFulfilled: false,
      message: '',
    }
  },
  questions: {
    questions: [{
      title: 'What is the address of the property?',
      id: 0,
      question_type: 'address'
    }],
    sessionID: 1234,
    session: {},
    currentQuestion: defaultQuestion,
    currentAnswer: '',
    addressSelectd: false,
    amount: 0,
    address: {
      address: {},
      isPendind: false,
      isRejected: false,
      isFulfilled: false,
      message: '',
    }
  }
};

rather than this -

state = {
  questions: [{
    title: 'What is the address of the property?',
    id: 0,
    question_type: 'address'
  }],
  sessionID: 1234,
  session: {},
  currentQuestion: defaultQuestion,
  currentAnswer: '',
  addressSelectd: false,
  amount: 0,
  address: {
    address: {},
    isPendind: false,
    isRejected: false,
    isFulfilled: false,
    message: '',
  }
}

I would strongly advice you to move the common state things (like currentAnswer and currentQuestion) into a separate reducer.


Edit 2: I just verified it with the following code that Object.assign() isn't the correct thing to do.

var address = {
  questions: [{
    title: 'What is the address of the property?',
    id: 0,
    question_type: 'address'
  }],
  sessionID: 12345,
  session: {},
  currentQuestion: defaultQuestion,
  currentAnswer: '',
  addressSelectd: false,
  amount: 0,
  address: {
    address: {},
    isPendind: false,
    isRejected: false,
    isFulfilled: false,
    message: ''
  }
};

var questions = {
  questions: [{
    title: 'What is the address of the property?',
    id: 0,
    question_type: 'address'
  }],
  sessionID: 1234,
  session: {},
  currentQuestion: defaultQuestion,
  currentAnswer: '',
  addressSelectd: false,
  amount: 0,
  address: {
    address: {},
    isPendind: false,
    isRejected: false,
    isFulfilled: false,
    message: ''
  }
};

var result = Object.assign({}, address, questions);
console.log(result);

The output is -

{
  "questions": [
    {
      "title": "What is the address of the property?",
      "id": 0,
      "question_type": "address"
    }
  ],
  "sessionID": 1234,
  "session": {},
  "currentQuestion": {
    "title": "What is the address of the property?",
    "id": 0,
    "question_type": "address"
  },
  "currentAnswer": "",
  "addressSelectd": false,
  "amount": 0,
  "address": {
    "address": {},
    "isPendind": false,
    "isRejected": false,
    "isFulfilled": false,
    "message": ""
  }
}

Here, the address has sessionID: 12345, whereas questions has sessionID: 1234, but the result has sessionID: 1234.

Thus the Object.assign() replaces the values set by address with the values of question. This is why it seems to work.

The proper way would be to redesign the reducer such that it has common state in a new reducer.

Upvotes: 1

Related Questions