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.
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.