As a way to help me learn ReactJS, I am setting up something that really should be easy, but it turned out to be somewhat complicated for me.
I want to configure some managed checkbox groups in ReactJS. In an HTML field, a "field" actually consists of several input type = "flag" elements that share the NAME property. As far as I understand, this is just a user interface element that should match the compositional nature of ReactJS.
I have two ReactJS components:
Firstly, CheckboxField is intended for each individual entry in the group of flags - for example, each input element = "checkbox" HTML element.
Secondly, the CheckboxFieldGroup for each group of checkbox entries is each element of HTML elements that have a common NAME property. The CheckboxFieldGroup component creates several CheckboxField components based on the original details that are passed to it.
State is managed in the CheckboxFieldGroup component, and not at a separate CheckboxField level. From what I read, you should manage the state as the highest level that makes sense. And for me it makes sense to have it at the CheckboxFieldGroup level.
When the CheckboxFieldGroup is first launched, its initial state is created as an array from its original props as well as the array. The rendering method (actually the renderChoices method) passes through its state array and passes all the properties of the state element up to the CheckboxField component as the last attribute. When the user indicates / deselects one of the flags, this event is passed through a callback to the handleChange method of CheckboxFieldGroup, its owner. This method determines which of the checkboxes was changed by polling its id property, and then makes the corresponding change to the correct member of the CheckboxFieldGroup state array through a call to setState (). This leads to the fact that CheckboxFieldGroup is automatically redrawn, while a new array of states is passed to the individual components of CheckboxField, so everything is in sync.
var CheckboxField = React.createClass({ propTypes: { values: React.PropTypes.object.isRequired }, getDefaultProps: function () { return { values: { label: "Place holder text" } }; }, render: function() { return ( <label htlmFor={this.props.values.id}> <input type="checkbox" name={this.props.values.name} id={this.props.values.id} value={this.props.values.value} checked={this.props.values.checked} onChange={this.handleChange} /> {this.props.values.label} <br /> </label> ); }, handleChange: function(event) { // Should use this to set parent state via a callback func. Then the // change to the parent state will generate new props to be passed down // to the children in the render(). this.props.callBackOnChange(this, event.target.checked); } }); var CheckboxFieldGroup = React.createClass({ propTypes: { defaultValues: React.PropTypes.object.isRequired }, getInitialState: function () { // default props passed in to CheckboxFieldGroup (this componenent) will be used to set up the state. State // is stored in this component, and *not* in the child CheckboxField components. The state store in this // component will, in turn, generate the props for the child CheckboxField components. When the latter // are updated (ie clicked) by the user, then the event will call the handleChange() function in // this component. That will generate update this component state, which in turn will generate // new props for the child CheckboxField components, which will cause those components to re-render! var that = this; var initStateArray = this.props.defaultValues.valuesArray.map(function(choice, i) { var tempObj = { name: that.props.defaultValues.name, value: choice.value, label: choice.label, id: _.uniqueId("choice"), checked: choice.checked }; return tempObj; }); return {valuesArray: initStateArray}; }, renderChoices: function() { var that = this; // Could also use .bind(this) on our map() function but that requires IE9+. return this.state.valuesArray.map(function(choice, i) { return CheckboxField({ values: { name: that.props.defaultValues.name, value: choice.label, label: choice.label, id: choice.id, checked: choice.checked }, callBackOnChange: that.handleChange }); }); }, render: function () { return ( <form> {this.renderChoices()} </form> ); }, handleChange: function(componentChanged, newState) { // Callback function passed from CheckboxFieldGroup (this component) to each of the // CheckboxField child components. (See renderChoices func). var idx = -1; var stateMemberToChange = _.find(this.state.valuesArray, function(obj, num) { idx = num; return obj.id === componentChanged.props.values.id; }); // Threw an error when I tried to update and indiviudal member of the state array/object. So, take a copy // of the state, update the copy and do a setState() on the whole thing. Using setState() rather than // replaceState() should be more efficient here. var newStateValuesArray = this.state.valuesArray; newStateValuesArray[idx].checked = newState; this.setState({valuesArray: newStateValuesArray}); // Automatically triggers render() !! }, getCheckedValues: function() { // Get an array of state objects that are checked var checkedObjArray = []; checkedObjArray = _.filter(this.state.valuesArray, function(obj){ return obj.checked; }); // Get an array of value properties for the checked objects var checkedArray = _.map(checkedObjArray, function(obj){ return obj.value; }); console.log("CheckboxFieldGroup.getCheckedValues() = " + checkedArray); }, componentDidMount: function() { this.getCheckedValues(); }, componentDidUpdate: function() { this.getCheckedValues(); } }); var defaults = { name : "mikeyCheck", valuesArray : [{ label : "My Checkbox Field", value: "MyCheckboxField", checked : false }, { label : "My Other Checkbox Field", value : "MyOtherCheckboxField", checked : false }, { label : "Yet Another Checkbox Field", value : "YetAnotherCheckboxField", checked : true },{ label : "Yes, it a fourth checkbox field", value : "YesItsAFourthCheckboxField", checked : false }] }; React.renderComponent(<CheckboxFieldGroup defaultValues={defaults} />, document.getElementById("main"));
All of this works great, and here's the JSFiddle of his work .
But I feel like I made a few mistakes here.
- There seems to be a lot of code to achieve something so simple. Is my whole approach wrong?
- The state of my CheckboxFieldGroup seems to contain a lot of things that might not need to be there, for example. it contains the name, value, label, identifier and is checked when in fact it is only the last named that will ever be changed (by the user). So should this be the only one that is able in others in the details? However, I need the id property to be in a state, so that the CheckboxFieldGroup.handleChange () method can determine which flag has really changed. Or is there a better / easier way to do this?
- When I update the state of the CheckboxFieldGroup component, again in the handleChange () method, I could not find a way to directly update the part of the state that I needed, i.e. the checked property of an element of the state array that corresponds to a flag that was simply checked / unqualified. What I ended up with was taking the entire copy of the state array into another variable, updating my one property there, and then replacing the whole state with a new array. Isn't that a wasteful way to do this, although I use setState () rather than replaceState ()?
Many thanks for your help. And yes, I have Google and I looked through the documentation. I also bought and read the book Developing a React Edge, which is now number one in area one at the moment!