0

I have a button in a child component for debugging purposes that prints the current state in its parent component. When it prints, that is how I want the state to be. When I hit another button in the same child component, the state changes in the parent, by removing a few properties.

Notice how the Id and SEO_CSEO_Survey_Questions__r properties are now missing.

Child Component

import React, { Component } from 'react';
import { Link } from 'react-router';

class Index extends Component {

  constructor(props){
    super(props)

    this.state = {
      selected: 'created'
    }

    this.updatePreview = this.updatePreview.bind(this);
    this.renderEditArea = this.renderEditArea.bind(this);
  }

  isActive(value) {
      return 'slds-tabs--default__item '+((value===this.state.selected) ?'slds-active':'');
  }

  updatePreview(e){
    const updatedPreview = {
      ...this.props.selectedPreview,
      [e.target.name]: e.target.value
    };
    this.props.update(updatedPreview)
  }

  // determines which type of edit area should display
  // survey settings or question
  renderEditArea() {
    let selected = this.props.selectedPreview;
    let hasNameKey = "Name" in selected;
    if(hasNameKey){
      return (
        <div>
          <input
            onChange={(e) => this.updatePreview(e)}
            name="Name"
            type="text"
            className="slds-input"
            value={selected.Name ? selected.Name : ''}
          />
          <input
            onChange={(e) => this.updatePreview(e)}
            name="SEO__Welcome_Text__c"
            type="text"
            className="slds-input"
            value={selected.SEO__Welcome_Text__c ? selected.SEO__Welcome_Text__c : ''}
          />
        </div>
      )
    }else {
      return (
        <input
          onChange={(e) => this.updatePreview(e)}
          name="SEO__Question__c"
          type="text"
          className="slds-input"
          value={selected.SEO__Question__c ? selected.SEO__Question__c : ''}
        />
      )
    }
  }

  render() {
    return (
      <div className="slds-size--1-of-1 slds-medium-size--4-of-5 slds-large-size--4-of-5">
        <div className="slds-tabs--default">
          <h2 className="slds-text-heading--small slds-p-top--x-small" style={{position: "absolute"}}>
            <button onClick={this.props.addQuestion} className="slds-button slds-button--icon slds-p-left--xx-small" title="add sur">
              <svg className="slds-button__icon slds-button__icon--medium" aria-hidden="true">
                <use xlinkHref={addIcon}></use>
              </svg>
              <span className="slds-assistive-text">Add Question</span>
            </button>
          </h2>
          <ul className="slds-tabs--default__nav" style={{justifyContent: "flex-end"}}>
            <Link to="/"className="slds-button slds-button--neutral">Back</Link>
            <button onClick={this.props.save} className="slds-button slds-button--brand">Save</button>
            <button onClick={this.props.currentState} className="slds-button slds-button--brand">Current State</button>            
          </ul>
        </div>
        <div className="slds-grid slds-wrap slds-grid--pull-padded">
          {this.renderEditArea()}
        </div>
      </div>
    );
  }
}

export default Index;

Parent

import React, { Component } from 'react';
import { getQuestions, addQuestion, deleteQuestion, newSurveyQuestions, updateSurveyQuestions, EMPTY_SURVEY } from './helpers';

import SideBar from './survey/SideBar';
import MainArea from './survey/Index';

class Survey extends Component {

  constructor(props) {
    super(props)

    this.state = {
      survey: [],
      selectedPreview: []
    }

    this.componentWillMount = this.componentWillMount.bind(this);
    this.save = this.save.bind(this);
    this.setSelectedPreview = this.setSelectedPreview.bind(this);
    this.currentState = this.currentState.bind(this);
  }

  // if the url is `/survey/new`, create an empty survey
  // to save for later.
  // else if there is an id in the url, load the survey and questions
  componentWillMount(){
    if(this.props.pathname === "/survey/new") {
      this.setState({
        survey: EMPTY_SURVEY,
        selectedPreview: EMPTY_SURVEY[0]
      })
    } else if (this.props.params.surveyId){
      getQuestions(this.props.params.surveyId).then(survey => {
        // 'survey' contains all the questions
        this.setState({
          survey,
          selectedPreview: survey[0]
        });
      });
    }
  }

  currentState() {
    console.log('clicking Current State');
    console.log(this.state.survey[0]);
  }

  // saves a new survey with associated newly created questions
  // or saves an existing survey with associated questions
  save() {
    console.log('clicking Save');
    console.log(this.state.survey[0]);
    // if the url is set to survey/new
    // save new survey with associated newly created questions
    if(this.props.pathname === "/survey/new") {
      newSurveyQuestions(this.state.survey).then( id => {
        this.context.router.transitionTo(`/survey/id/${id}`);
      })

    // else update survey and questions
    } else {
      updateSurveyQuestions(this.state.survey);
    }
  }

  // sets selectedPreview for the entire component and
  // its children
  setSelectedPreview(selectedPreview) {
    this.setState({selectedPreview});
  }

  render() {
    return (
      <div className="slds-grid slds-wrap slds-grid--pull-padded">
        <SideBar
          survey={this.state.survey}
          setSelectedPreview={this.setSelectedPreview}
          deleteQuestion={this.deleteQuestion}

        />
        <MainArea
          addQuestion={this.addQuestion}
          selectedPreview={this.state.selectedPreview}
          update={this.update}
          save={this.save}
          currentState={this.currentState}
        />
      </div>
    );
  }
}

Survey.contextTypes = {
  router: React.PropTypes.object
}

export default Survey;

help function

export function updateSurveyQuestions(survey) {
  // create proper url for update request
  const requestURL = `${URL + survey[0].attributes.url}`;

  // save questions for later update requests
  const questions = survey[0].SEO__CSEO_Survey_Questions__r.records;

  let s = [...survey];

  // remove properties for proper body format
  delete s[0].Id;
  delete s[0].SEO__CSEO_Survey_Questions__r;
  delete s[0].attributes;

  axios.patch(requestURL, s[0], API_TOKEN);

  questions.forEach( question => {
    // save request url for later
    let requestURL = `${URL + question.attributes.url }`;

    // remove properites for proper body format
    delete question.attributes;
    delete question.Id;

    axios.patch(requestURL, question, API_TOKEN);
  })
}

When I removed all the code in save(), except for the console.log's, it prints as expected.

Tyler Zika
  • 1,144
  • 2
  • 14
  • 25

1 Answers1

1

tl;dr: Your code is working fine. What you are seeing is a consequence of how objects and the console work. Do

console.log(this.state.survey[0].Id);

instead to see that the property does actually exist.

See also Is Chrome's JavaScript console lazy about evaluating arrays?


When I removed all the code in save(), except for the console.log's, it prints as expected.

That seems to suggest that that code is changing the object. E.g. updateSurveyQuestions or newSurveyQuestions might remove these properties.

You have to keep in mind that the output you see in the console is computed at the moment you expand the properties, not at the moment console.log is called. In fact, the little i icon next to Object tells you that. When you hover over it says:

Value below was evaluated just now

Here is a simplified example of what you are experiencing: Expand the object and notice that it is empty even though we removed it after calling console.dir (open your browser's console):

var obj = {foo: 42};
console.dir(obj);
delete obj.foo;

Ideally updateSurveyQuestions or newSurveyQuestions would not mutate the object directly, but rather clone it, make the necessary changes to the clone and then update the components state (if desired). E.g. a simple way to clone the object is via Object.assign:

var copy = Object.assign({}, this.state.survey[0]);
Community
  • 1
  • 1
Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • It looks like `updateSurveyQuestions` and `newSurveyQuestions` are mutating the object directly, which I don't understand because I'm passing a copy of state to it, correct? – Tyler Zika Mar 31 '17 at 16:56
  • *"because I'm passing a copy of state to it"* All I can see is that you are passing `this.state.survey`, which is not a copy. JavaScript represents objects (arrays are objects too) as *references*. They are *not* copied on assignment. – Felix Kling Mar 31 '17 at 16:59
  • I create copy of state like so, `let survey = [...this.state.survey]`, and passed `survey` into my functions. still getting same error. – Tyler Zika Mar 31 '17 at 17:01
  • That only creates an copy of the array itself, but not the objects inside the array. I.e. `survey[0] === this.state.survey[0]`. – Felix Kling Mar 31 '17 at 17:02
  • I know. In my helper functions I work with the array accordingly. – Tyler Zika Mar 31 '17 at 17:04
  • What's strange is my state is being changed as well, even though I'm not setting state in my helper functions. This is breaking my code since the properties `Id` and `SEO_CSEO_Survey_Questions__r` are required in other parts of my web app. – Tyler Zika Mar 31 '17 at 17:13
  • As I have said, you would have to make a *copy* of the object you are going to change. – Felix Kling Mar 31 '17 at 17:13
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/139615/discussion-between-tyler-zika-and-felix-kling). – Tyler Zika Mar 31 '17 at 17:14