user3259232 user3259232 - 1 month ago 11
React JSX Question

Learning React.js

I am trying to learn React. I already have a good grasp of javascript. I am trying to learn by creating a small app that is basically a task manager. In my case it's for grocery related items. I have a fiddle created here. Could you please take a look at how I composed the react code and let me know if this is the best approach to building with components/classes? You can see that I have nested components. I am not sure if there is a better way of doing this.

Finally, I wan't a new "add-item-row" created every time a user clicks on the big blue Add button. Right now one is showing be default but I don't want any showing by default. I want one created (add-item-row, div) only when a user clicks on the Add button.

Here is the fiddle.

https://jsfiddle.net/j0mpsbh9/4/

<div id="app" class="container">
<script type="text/babel">
var AddItemWrapper = React.createClass({
render: function() {
return (
<div>
<div className="row">
<AppTitle />
<AddItemForm />
</div>
<AddItemRow />
</div>
);
}
});

var AppTitle = React.createClass({
render: function() {
return (
<div>
<h1>Grocery List</h1>
</div>
);
}
});

var AddItemForm =React.createClass({
render: function() {
return (
<div>
<div className="col-sm-6 col-lg-6">
<div className="form-group">
<label htmlFor="enter-grocery-item" className="sr-only">Enter Grocery Item</label>
<input type="text" className="form-control" id="enter-grocery-item" placeholder="Enter Grocery Item" />
</div>
</div>
<div className="col-sm-6 col-lg-6">
<button type="button" id="add" className="btn btn-block btn-info">Add <span className="glyphicons circle_plus"></span></button>
</div>
</div>
);
}
});

var AddItemRow =React.createClass({
render: function() {
return (
<div className="add-item-row">
<div className="row">
<div className="col-sm-12 grocery-items">
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="grocery-item" className="sr-only">Grocery Item</label>
<input type="text" className="form-control" id="grocery-item" placeholder="" />
</div>
</div>
<div className="col-sm-6 center">
<button type="button" className="btn btn-blockx btn-warning"><span className="glyphicons pencil"></span></button>
<button type="button" className="btn btn-blockx btn-lgx btn-danger"><span className="glyphicons remove"></span></button>
<button type="button" className="btn btn-blockx btn-lgx btn-success"><span className="glyphicons thumbs_up"></span></button>
</div>
</div>
</div>
</div>
);
}
});

ReactDOM.render(
<AddItemWrapper />,
document.getElementById('app')
);
</script>
</div>

JCD JCD
Answer

You have a good start here! Your component hierarchy is organized in a sensible way. However you are missing any kind of interactivity or internal state.

The main way you make React components interactive is by using state plus event callbacks which modify said state. "State" is pretty self explanatory - it describes values inherent to how the components looks and behaves, but which change over time. Every time a React component's state is altered (with this.setState()) that component will re-render (literally by re-running the render() function) to reflect the changes.

First let's edit your AddItemWrapper class to keep track of some internal state when it is first mounted. We know that you want to have multiple rows of data, so let's give it an empty array to store future information about rows:

getInitialState(){
    return {rows: []};
},

Now instead of rendering a single AddItemRow directly, we'll render a dynamic set of rows that is based on the current component state. Array.map() is perfect for this and a common use case in React:

{this.state.rows.map(function(ea, i){
    return <AddItemRow initialItemName={ea} key={ea + "-" + i} />
})}

Basically what that does is take every entry in the array AddItemWrapper.state.rows array and renders it as an AddItemRow component. We give it two properties, initialItemName and key. "initialItemName" will just tell the child component what its name was when it was first added, and "key" is a unique string that allows React to differentiate components from their siblings.

Now we've set up AddItemWrapper to properly render rows based on its internal state. Next we have to modify AddItemForm so that it will react to user input and trigger new rows being added.

In AddItemForm, first we need to add a "ref" to the input text box. This is so that React can identify and read data from this HTML element after it is rendered:

<input ref={function(el){this.inputElement = el;}.bind(this)} ... />

Then give the button element a callback that will trigger when it's clicked:

<button onClick={this.handleClick} ... />

Finally write the callback handler itself:

handleClick(){
    this.props.onAdd(this.inputElement.value);
    this.inputElement.value = "";
}

Notice how this callback is calling this.props.onAdd()? That means we need to pass in a callback function from the parent (AddItemWrapper) to this component to use. This is how we communicate between parents and children in React: pass a function from a parent to a child which will be triggered from within the child, but will effect the parent.

In AddItemWrapper we make sure AddItemForm has access to the callback function:

AddItemForm onAdd={this.onAdd} />

And then we write the callback function itself:

onAdd(newItem){
    var newRows = this.state.rows.slice();
    newRows.push(newItem);
    this.setState({rows: newRows});
}

Notice how we're copying the old array held in state (using Array.slice()), push a new item into the new array, and then update state with the new array? Never mutate state directly; ALWAYS copy it, modify the copy, and then update state with the new copy.

Almost done. We've created a way for AddItemWrapper to render its rows, and a way for AddItemForm to create new rows. Now we edit AddItemRow to render in a way that maintains its own internal state too.

First make sure it initializes its own state when it's mounted. We'll have it keep track of a string value, which initially is the same as what the user entered into the text box when they pressed "Add", but because it's kept in AddItemRow.state it can be modified later by the user:

getInitialState() {
    return {itemName: this.props.initialItemName}
}

Now that the row name is kept in the component state, we can render it in the HTML like this:

<input value={this.state.itemName} ... />

Here's what it looks like when you put it all together!

There are obviously more features that you would want to add, such as letting the user edit, move, or delete a row entry. I'll leave those exercises up to you. I highly recommend you read through all of the official documentation as well as do a few tutorials to get your head in the game. It's obvious that you have a bit of experience under your belt given what you had so far, but getting the hang of how state, render(), and callbacks work takes some practice. Good luck!