Creating <select> elements in React JS

37,059

A parent can access it's child's refs as long as you set the child as a ref inside the parent, you can access any refs inside that child. So for example something like this.refs.childRef.refs.nestedChildRef. However this is a really bad idea unless you are just reading information (and handling errors such as the refs not existing correctly since the parent doesn't know if the child's ref is set correctly).

But luckily there is a very simple solution. You are right, you will want to create a child component which gets it's own data, rather than a mixin. But how you handle giving the selected item back to the parent is simply pass an onChange function from the parent like you would to an in-built <select> in react.

Here's an example of this parent child relationship:

var MyParent = React.createClass({
    getInitialState: function() {
        return {
            childSelectValue: undefined
        }
    },
    changeHandler: function(e) {
        this.setState({
            childSelectValue: e.target.value
        })
    },
    render: function() {
        return (
            <div>
                <MySelect 
                    url="http://foo.bar"
                    value={this.state.childSelectValue}
                    onChange={this.changeHandler} 
                />
            </div>
        )
    }
});

var MySelect = React.createClass({
    propTypes: {
        url: React.PropTypes.string.isRequired
    },
    getInitialState: function() {
        return {
            options: []
        }
    },
    componentDidMount: function() {
        // get your data
        $.ajax({
            url: this.props.url,
            success: this.successHandler
        })
    },
    successHandler: function(data) {
        // assuming data is an array of {name: "foo", value: "bar"}
        for (var i = 0; i < data.length; i++) {
            var option = data[i];
            this.state.options.push(
                <option key={i} value={option.value}>{option.name}</option>
            );
        }
        this.forceUpdate();
    },
    render: function() {
        return this.transferPropsTo(
            <select>{this.state.options}</select>
        )
    }
});

The above example shows a parent component MyParent including a child component MySelect which it passes a url to, along with any other props that are valid for a standard react <select> element. Using react's built in this.transferPropsTo() function to transfer all props over to the child's <select> element in it's render function.

This means that the parent defining changeHandler as the onChange handler for MySelect passes this function directly to the child's <select> element. So when the select changes, the event gets handled by the parent instead of the child. And by the way this is a prefectly legal and legit way to do things in React - since it's still following the top down philosophy. If you think about it, when you use a normal built-in <select> from react, this is exactly what's going on. onChange is just a property for <select>.

It's also worth noting that if you like, you can "hyjack" the change handler to add your own custom logic. For example what I tend to do is wrap my <input type="text" /> fields with a custom input component that takes any and all valid <input> props, but also takes validator function as a property which allows me to define a function that returns true/false based on the value inputted in to the <input> field. I can do this by defining onChange in the parent, for the wrapper child, but then in the child I define my own onChange internally which checks for this.props.onChange to be defined, and a function, then runs my validator and passes the event chain back up to the parent by calling this.props.onChange(e, valid). Giving the parent the very same event object in it's onChange handler, but also with an added boolean valid argument. Anyway, I digress....

Hope this helps you out :)

Share:
37,059
George Oblapenko
Author by

George Oblapenko

Updated on July 09, 2022

Comments

  • George Oblapenko
    George Oblapenko almost 2 years

    I'm rewriting the UI for my web-app in react.js, and I'm a bit stumped by the following problem.

    I have a page which displays data obtained via an AJAX request, and below that, a form to submit new data is displayed. All good.

    Now, I want to add a <select> element to the form, and fetch the values from a different location (url).

    The current code (without the <select>) looks like this (simplified a bit, but all the working details are the same; it mostly follows the tutorial on the react.js website):

    var tasks_link = $('#tasks_link');
    
    var getDataMixin = {
            loadDataFromServer: function() {
                $.ajax({
                    url: this.props.url,
                    dataType: 'json',
                    success: function(data) {
                        this.setState({data: data});
                    }.bind(this),
                    error: function(xhr, status, err) {
                        console.error(this.props.url, status, err.toString());
                    }.bind(this)
                });
            },
            getInitialState: function() {
                return {data: []};
            },
            componentDidMount: function() {
                this.loadDataFromServer();
            }
        };
    
    var sendDataMixin = {
            handleDataSubmit: function(senddata) {
                $.ajax({
                    url: this.props.url,
                    dataType: 'json',
                    contentType: 'application/json',
                    type: 'POST',
                    data: senddata,
                    success: function(data) {
                        var curr_d = this.state.data;
                        var curr_d_new = curr_d.concat([data]);
                        this.setState({data: curr_d_new});
                    }.bind(this),
                    error: function(xhr, status, err) {
                        console.error(this.props.url, status, err.toString());
                    }.bind(this)
                });
            }
        };
    
    var taskForm = React.createClass({
            handleSubmit: function() {
                var name = this.refs.task_name.getDOMNode().value.trim();
                if (!name) {
                  return false;
                }
                this.props.onTaskSubmit(JSON.stringify({name: name}));
                this.refs.task_name.getDOMNode().value = '';
                return false;
            },
            render: function () {
                return (
                    <form className="well base_well new_task_well" onSubmit={this.handleSubmit}>
                        <div className="form-group">
                            <div className="input-group">
                                <span className="input-group-addon no_radius">Task name</span>
                                <input type="text" className="form-control no_radius" id="add_new_project_input" ref="task_name"/>
                            </div>
                        </div>
                        <button type="button" className="btn btn-default no_radius add_button" id="add_new_task_btn" type="submit">Add task</button>
                    </form>
                );
            }
        });
    
    var taskBox = React.createClass({
            mixins: [getDataMixin, sendDataMixin],
            render: function () {
                return (
                    <div id="project_box" className="taskBox"> <taskList data={this.state.data} />
                        <taskForm onTaskSubmit={this.handleDataSubmit}/> </div>
                );
            }
        });
    
    tasks_link.click(function() {
            React.renderComponent(
                <taskBox url="/api/tasks/" />,
                document.getElementById('content_container')
            );
        });
    

    Now, I can add a select element by adding a getDataMixin to TaskForm, fetching the data and building a list of possible options, but I will need to have forms with many lists, and the approach doesn't seem to scale (due to naming collisions; or I'll need to use something other than mixins).

    So I though of creating a separate React class, which would just have the getDataMixin, receive the API url via the parent setting its props, and render the <select> element; and use this class inside the form. But I have no idea how to access the selected value (since a parent cannot access it's child's refs). So I need another way to pass the selected value "up".

    Or, in case this isn't possible, a nudge in the correct direction – I don't want to end up with a ton of un-reusable code (part of the reason why I switched to react was to use mixins and keep the code to a sane and readable minimum).

  • Josef.B
    Josef.B about 9 years
    Mike, as written your solution won't invoke the render on the select object since you don't use setState(x) function. I modified the code as follows to create a location array, var optionsArray = [];, then update that array, finally after the loop to invoke: this.setState({options:optionsArray});
  • Mike Driver
    Mike Driver about 9 years
    Good catch - thats what I get for not testing code in answers, although personally I prefer to use this.forceUpdate() when directly mutating the state object. Mostly just personal preference though. I've edited my answer to update it.