3

I keep hitting a wall when trying to get the parent data passed down to the child component.

My view:

<%= react_component 'Items', { data: @items } %>

My Items component makes an ajax call, sets state, and renders Item. Leaving key={this.props.id} out of the Item instance passed into the mapping function makes it so that the component html renders to the page. But add the key in, and I get a console error: Uncaught TypeError: Cannot read property 'id' of undefined

Here's 'Items':

var Items = React.createClass({
    loadItemsFromServer: function() {
        $.ajax({
            url: this.props.url,
            dataType: 'json',
            cache: false,
            success: function(data) {
                this.setState({data: data});
            }.bind(this),
            error: function(xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },
    componentDidMount: function() {
        this.loadItemsFromServer();
    },
    render: function() {
        var itemNodes = this.props.data.map(function() {
            return (
                <Item key={this.props.id} />
            );
        });
        return (
            <div className="ui four column doubling stackable grid">
                {itemNodes}
            </div>
        );
    }
});

My item.js.jsx component just formats each Item:

var Item = React.createClass({
    render: function() {
        return (
            <div className="item-card">
                <div className="image">

                </div>
                <div className="description">
                    <div className="artist">{this.props.artist}</div>
                </div>
            </div>
        );
    }
});

The React dev tools extension shows the props and state data inside Items. The children, however, are empty.

enter image description here

I'm aware of this, but I'm setting key with this.props.id. I'm not sure what I'm missing?

calyxofheld
  • 1,538
  • 3
  • 24
  • 62
  • 1
    In you items component you passing item: item as variable to its child (item), but you are calling props on class which is undefined. Call @props.item.attribute if you want to map (loop) across your items variable. What is your items json? – evgeny Jun 03 '16 at 23:15
  • I corrected my post. I am calling @props.item.attribute. And I don't have an items json yet, since I'm just trying to set it up to render existing database content. – calyxofheld Jun 03 '16 at 23:24
  • 1
    Ok, no I think it should render your items, but you don't have them yet. Seed few items and see if it shows up them from backend. – evgeny Jun 03 '16 at 23:27
  • Just did. They propagate to the backend, and I can see the new item added in the data-react-props. But it's not rendering to the page. – calyxofheld Jun 03 '16 at 23:36
  • i just ran curl 0.0.0.0:3000/items.json and got `[{"id":1,"url":"http://0.0.0.0:3000/items/1.json"},{"id":2,"url":"http://0.0.0.0:3000/items/2.json"},{"id":3,"url":"http://0.0.0.0:3000/items/3.json"},{"id":4,"url":"http://0.0.0.0:3000/items/4.json"},{"id":5,"url":"http://0.0.0.0:3000/items/5.json"},{"id":6,"url":"http://0.0.0.0:3000/items/6.json"},{"id":7,"url":"http://0.0.0.0:3000/items/7.json"},{"id":8,"url":"http://0.0.0.0:3000/items/8.json"},{"id":9,"url":"http://0.0.0.0:3000/items/9.json"}]` – calyxofheld Jun 04 '16 at 00:30
  • 1
    Use console.log in your render to see what you get into your react. I also suggest you to have react dev extension installed (there is one at least for Chrome), so you can see what your route gets currently as object from your backend. If you still don't see any errors in your console, that means you are rendering correctly, your data either not getting in or you are not rendering that object. – evgeny Jun 04 '16 at 00:43
  • check what is the value of `data-react-props` in your html for this react component. – Vipin Verma Jun 05 '16 at 09:38
  • Moreover, you can debug your react class in the chrome/firefox console. mark some breakpoints in getDefaultProps and check what exactly does the this.props contain – Vipin Verma Jun 05 '16 at 09:39
  • @vipin8169 I got the html for each Item to render. `data-react-props` contains the json for each Item. I also installed Chrome's React plug-in, and see two arrays - one for Props, one for State, and each containing the 12 json objects and every database parameter that they *should* contain. React docs say to add a `key` to the rendered Item component in the array, but doing so stops the component from rendering at all. – calyxofheld Jun 06 '16 at 01:28
  • you must add a key to each item rendered, if you are rendering it in loops, and key must be unique. See this gist of mine. this is a react class that i created, and for every dom element that is rendered in a loop I am adding a key component. See the key component within each for loop https://gist.github.com/vipin8169/60e6b84185f1e41d6b76bed950c25bec – Vipin Verma Jun 06 '16 at 05:28
  • passing a unique key='item.id' to div with className="item-card" will solve this. please let me know if this solves your problem. read this for more info - https://facebook.github.io/react/docs/reconciliation.html – Vipin Verma Jun 06 '16 at 06:04

2 Answers2

1

I found a couple of problems with the code you posted, in the Items component

  1. You're rendering this.props.data while in fact this.state.data is the one being updated with the ajax request. You need to render this.state.data but get the initial value from props
  2. The map iterator function takes an argument representing the current array element, use it to access the properties instead of using this which is undefined

The updated code should look like this

var Item = React.createClass({
    render: function() {
        return (
            <div className="item-card">
                <div className="image">

                </div>
                <div className="description">
                    <div className="artist">{this.props.artist}</div>
                </div>
            </div>
        );
    }
});

var Items = React.createClass({
    getInitialState: function() {
        return {
            // for initial state use the array passed as props,
            // or empty array if not passed
            data: this.props.data || []
        };
    },
    loadItemsFromServer: function() {
        var data = [{
            id: 1,
            artist: 'abc'
        }, {
            id: 2,
            artist: 'def'
        }]
        this.setState({
            data: data
        });

    },
    componentDidMount: function() {
        this.loadItemsFromServer();
    },
    render: function() {
        // use this.state.data not this.props.data, 
        // since you are updating the state with the result of the ajax request, 
        // you're not updating the props
        var itemNodes = this.state.data.map(function(item) {
            // the map iterator  function takes an item as a parameter, 
            // which is the current element of the array (this.state.data), 
            // use (item) to access properties, not (this)

            return (
                // use key as item id, and pass all the item properties 
                // to the Item component with ES6 object spread syntax
                <Item key={item.id} {...item} />
            );
        });
        return (
            <div className="ui four column doubling stackable grid">
                {itemNodes}
            </div>
        );
    }
});

And here is a working example http://codepen.io/Gaafar/pen/EyyGPR?editors=0010

gafi
  • 12,113
  • 2
  • 30
  • 32
  • Thank you for this comprehensive answer! This really helps. I've gotten it to work with dummy data like this, but using the data that is actually on my server is where I was totally stumped. Your suggestions worked once I put my ajax call back in, but it looks like the success and error functions were overriding the data microseconds after page load. I removed those functions. Is that a bad thing? – calyxofheld Jun 17 '16 at 02:35
  • Is it important to setState in my ajax call? – calyxofheld Jun 17 '16 at 02:41
  • My understanding is that you want to open the component with some initial data that you pass as props, then load the updated data from the server with an ajax request and display it. If this is the case then you need to set the state with the latest data in the ajax success. It might be failing because the ajax response object has a different structure than the dummy object we use. Add a line to log the ajax response and make sure it has the same structure as the dummy data. – gafi Jun 17 '16 at 12:04
0

There are a couple of problems with your implementation.

First of all, you need to decide: Do you want to render the @items passed to the Items component from your view? Or do you want to load them asynchronous? Because right now I get the impression you are trying to do both...

Render items passed from view

If you want to render the items from your view passed to the component, make sure it's proper json. You might need to call 'as_json' on it.

<%= react_component 'Items', { data: @items.as_json } %>

Then, in your Component, map the items to render the <Item /> components. Here is the second problem, regarding your key. You need to define the item variable to the callback function of your map function, and read the id from it:

var itemNodes = this.props.data.map(function(item) {
  return (
    <Item key={item.id} artist={item.artist} />
  );
});

Note, I also added the author as prop, since you are using it in your <Item /> Component.

You can remove your componentDidMount and loadItemsFromServer functions, since you are not using them.

Load items asynchronous

If you want to load the items asynchronously, like you are trying to do in your loadItemsFromServer function, first of all, pass the url from your view and remove the {data: @items} part, since you will load the items from your component, something like:

<%= react_component 'Items', { url: items_path(format: :json) } %>

If you want to render the asynchronous fetched items, use:

var itemNodes = this.state.data.map(function(item) {
  return (
    <Item key={item.id} artist={item.artist} />
  );loadItemsFromServer
});

Note I changed this.props.map to this.state.map

You can now use your componentDidMount and loadItemsFromServer functions to load the data and save them to state.

Sander Garretsen
  • 1,683
  • 10
  • 19