简体   繁体   中英

React.js: Parent state values not passing into child properties + Fetch API data cannot be accessed

I am encountering several issues in a very basic color harmony picker I am developing. I am still a beginner in React and JSX. I initially had it put up on GitHub so the full files are on there, but I moved it over to Codepen instead.

Here is the Codepen

I made a lot of comments so sorry if they're a bit much, but hopefully they help. My problems don't begin until line 41, the displayHarmonies() method of the DataStore class. The values passed to it come from my App (parent) component:

displayHarmonies(color, harmony) {
    //color and harmony pass in dynamically just fine...this.data will not return anything, not even "undefined"
    console.log(color + " is the color and " + harmony + " is the harmony...and dataStore.displayHarmonies says: " + this.data);

    this.registeredWatchers.map((watcher) => {
        let result = "not green"; //result and resultHex will be determined with an underscore statement that will associate the color & harmony choice (primary + foreign key concept) and will return correct harmony color(s)
        let resultHex = "#HEX";

        appState.harmonyColor = result;
        appState.harmonyHex = resultHex;

        //call to app component's onDataChange() method, where new states will be set using the the appState data we just set in lines 49 and 50
        watcher.onDataChange();
    })
}  

As you can see from my first comment, the only part that doesn't log to the console is this.data, which is set in the constructor for the DataStore:

constructor(data) {
    //store that data in the object
    //data is not being received from object instance of dataStore on line 187
    this.data = data;

On line 187 I make an instance of the DataStore and pass it a variable named data . Prior to being used, this variable is initialized and then assigned to parsed JSON data via Fetch API:

let data = [];

//use polyfill for older browsers to do Ajax request
fetch("data/data.json").then((response) => {
//if we actually got something
    if (response.ok) {
        //then return the text we loaded
        return response.text();
    }
}).then((textResponse) => {
    data = JSON.parse(textResponse);
});

If I console out the data in the second fetch .then() method, the JSON comes back just fine. As soon as I try to use the data variable anywhere else in the application, it returns nothing, as shown in the displayHarmonies() method's console.log() . So that's my first issue, but before I wanted to get to that, I wanted to solve the other issue I was having.

After the appState object (initialized prior to the DataStore, under the fetch statement) values get set to the result variables, displayHarmonies() runs watcher.onDataChange() (in the App component/parent) where the harmonyColor and harmonyHex states get assigned to the new appState values:

onDataChange() {
    console.log("onDataChange() in App called");
    this.setState({
        harmonyColor: appState.harmonyColor,
        harmonyHex: appState.harmonyHex
    })
}

If I log these states out to the console, they are the right values, so that's not the problem. I then pass my states to the Display child component to be used as properties:

<Display colorChoice={this.state.currentColor} harmonyChoice={this.state.currentHarmony} harmonyColor={this.state.harmonyColor} harmonyHex={this.state.harmonyHex} /> 

I then set the Display component states in the constructor, assigning them to the props that are being sent to it with each new rendition of the application. I then display the data onto the DOM with the Display component's render method. What's odd is that the application will display the initial states (color: red, harmony: direct, harmonyColor: green, etc.) just fine, but as soon as a change is made, the data on the DOM does not update. The initial data is loaded in the same way though: by passing the parent's states into the child's properties. I have a few console.log() s in place that seem to prove why this should work, however, it does not. So what am I doing wrong?

Thanks, and hope this is not too much for one question!

I tried cloning your repo, but it seems to be nested in another repo. With your current setup, this may work:

In your App component, you can put this lifecycle method to fetch the data, and then set the state with the received data.:

    componentDidMount(){
      fetch("data/data.json").then((response) => {
    //if we actually got something
    if (response.ok) {
        //then return the text we loaded
        return response.text();
    }
}).then((textResponse) => {
  this.setState({
    data : JSON.parse(textResponse);
  })

});
    }

In the return statement, you can render the data store as a child so App can pass the data like this:

return (
    <div className="App">
      <DataStore data={this.state.data} />
      <h1>Color Harmonies</h1>
        {/* assigns this.colorChosen() & this.harmonyChosen() methods as properties to be called in Picker component */}
        <Picker colorChosen={this.colorChosen.bind(this)} harmonyChosen={this.harmonyChosen.bind(this)}/>
        {/* give Display component props that are dynamically set with states */}
        <Display colorChoice={this.state.currentColor} harmonyChoice={this.state.currentHarmony} harmonyColor={this.state.harmonyColor} harmonyHex={this.state.harmonyHex} />
    </div>
);

Then, your data store should receive the data as a prop, so you can use it like this:

displayHarmonies(color, harmony) {
    //color and harmony pass in dynamically just fine...this.data will not return anything, not even "undefined"
    console.log(color + " is the color and " + harmony + " is the harmony...and dataStore.displayHarmonies says: " + this.props.data); //data is received in the properties so you can use it. 
//other code


})

Doing this, you should also be able to remove this.data from the constructor of the DataStore component.

Also in Data store, youll want to to allow it to accept props like this:

constructor(props){
super(props)
}

First a bit to your current code, at the end of the post, I have added an alternative solution, so if this is tl;dr; just skip to the snippet at the end :)

A first remark would be on the data variable that you wish to pass on to your DataStore , nl (I left out some parts, as they are irrelevant to the discussion)

let data = [];

fetch("data/data.json").then(( response ) => {
    data = JSON.parse( response.text() );
});
//... later down the code
var store = new DataStore(data);

Here you are reassigning the data variable inside the then promise chain of your fetch call. Although the assignment will appear to work, the data that now is on store.data will be an empty array, and the global variable will data will now contain the parsed response.text() . You should probably just push in the data you have just parsed (but in my example, I didn't even include the DataStore so this is just for future reference)

In your CodePen, you seem to mixing props & state for your Display component. That is in essence a no-op, you shouldn't mix them unless you really know what you are doing. Also note, that by calling this.setState inside the componentWillReceiveProps life cycle method, the app will automatically re-render more than needed. I am referring to this code:

componentWillReceiveProps(nextProps) {
  this.setState({
    color: nextProps.colorChoice,
    harmony: nextProps.harmonyChoice,
    harmonyColor: nextProps.harmonyColor,
    harmonyHex: nextProps.harmonyHex
  });
}

But you are then rendering like this:

render() {
  return (
    <div>
      {/* these aren't changing even though states are being set */}
      <p><b>Color:</b> {this.state.color}</p>
      <p><b>Harmony:</b> {this.state.harmony}</p>
      <p><b>Harmony Color(s):</b> {this.state.harmonyColor} ({this.state.harmonyHex})</p>
    </div>
  )
}

Here you should remove the componentWillReceiveProps method, and render values from this.props as you are passing these along from your App .

Alternative solution

As mentioned in the comments, your code currently is doing a lot more than it should do to pass state between parent and child components.

One thing you should keep in mind, is that when a component state gets changed, react will re-render the component automatically. When it sees that the virtual DOM has discrepancies with the real DOM it will automatically replace those components.

In that sense, your DataStore is not necessary. Depending on how you want to manage state, the component will react on those changes.

Since your app uses Component State (which is fine for small applications, once you want to move to bigger applications, you will probably want to move on to something like Redux, or MobX), the only thing you need to do, is to make sure that you set the correct components state to trigger the rendering.

As an example, I remade your code in a cleaner way:

 const Choice = ({ header, values, onChange, activeValue }) => { return <ul> <li><h1>{ header }</h1></li> { values.map( (value, key) => <li key={key+value} className={classNames( { active: value === activeValue, item: true } )} onClick={() => onChange( value )}>{ value }</li> ) } </ul> }; const colors = ['red', 'green', 'black', 'blue', 'yellow']; const harmonies = ['direct', 'split', 'analogous']; class App extends React.Component { constructor(...args) { super(...args); this.state = { activeColor: undefined, activeHarmony: undefined }; } onColorChanged( color ) { this.setState({ activeColor: color }); } onHarmonyChanged( harmony ) { this.setState({ activeHarmony: harmony }); } render() { let { activeColor, activeHarmony } = this.state; return <div> <Choice header="Choose color" values={colors} activeValue={activeColor} onChange={(...args) => this.onColorChanged(...args)} /> <Choice header="Choose harmony" values={harmonies} activeValue={activeHarmony} onChange={(...args) => this.onHarmonyChanged(...args)} /> </div>; } } ReactDOM.render( <App />, document.querySelector('#container')); 
 h1 { margin: 0; padding: 0; } ul { list-style-type: none; } .item { cursor: pointer; padding: 5px; } .active { background-color: lightgreen; } 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.6.0/prop-types.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/index.js"></script> <div id="container"></div> 

Now, there are some things in this sample code that might need some explanation. For one, this code has 2 component types, 1 presentational component called Choice which is stateless, and one container component called App which delegates it's state to it's children.

A bit more information about container & presentational components can be found on the blog of Dan Abramov (redux creator)

The essence of the above concept is just this, the App component is responsible for the state, and for sharing it with it's children. So, all state changes need to be made on the App component. As you can see in the render, the App simply passes its state along:

render() {
  let { activeColor, activeHarmony } = this.state;
  return <div>
    <Choice 
      header="Choose color" 
      values={colors} 
      activeValue={activeColor} 
      onChange={(...args) => this.onColorChanged(...args)} />
    <Choice 
      header="Choose harmony" 
      values={harmonies} 
      activeValue={activeHarmony}
      onChange={(...args) => this.onHarmonyChanged(...args)} />
  </div>;
}

The App passes a change handler along to the Choice component that can be called when a selection should occur, this gets forwarded to the App , the state changes, and app re-renders, allowing the Choice component to update it's elements.

const Choice = ({ header, values, onChange, activeValue })

Based on the props passed into it, the Choice component can decide which is the active item at the moment of rendering. As you can see, the props are destructed. header , values , onChange and activeValue are all properties on the props of the component, but to save time, we can assign these values at ones to a variable and use them in the rendering.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM