Recently I have been working my way through the Advanced React section of Coursera's Meta Front-End Developer Professional Certificate, and at some point we are tasked with creating a radio button list in the following manner:
src/App.js
import "./App.css";
import { RadioGroup, RadioOption } from "./Radio";
import { useState } from "react";
function App() {
const [selected, setSelected] = useState("");
return (
<div className="App">
<h2>How did you hear about Little Lemon?</h2>
<RadioGroup onChange={setSelected} selected={selected}>
<RadioOption value="social_media">Social Media</RadioOption>
<RadioOption value="friends">Friends</RadioOption>
<RadioOption value="advertising">Advertising</RadioOption>
<RadioOption value="other">Other</RadioOption>
</RadioGroup>
<button disabled={!selected}>Submit</button>
</div>
);
}
export default App;
src/Radio/index.js
import * as React from "react";
export const RadioGroup = ({ onChange, selected, children }) => {
const RadioOptions = React.Children.map(children, (child) => {
return React.cloneElement(child, {
onChange,
checked: child.props.value === selected,
});
});
return <div className="RadioGroup">{RadioOptions}</div>;
};
export const RadioOption = ({ value, checked, onChange, children }) => {
return (
<div className="RadioOption">
<input
id={value}
type="radio"
name={value}
value={value}
checked={checked}
onChange={(e) => {
onChange(e.target.value);
}}
/>
<label htmlFor={value}>{children}</label>
</div>
);
};
Explanation:
Most of the code seems pretty straightforward, except for when the legacy APIs React.Children and React.cloneElement (both of whom the usage is currently discouraged by React Docs themselves) are used. However, since these are the two most important functionalities of the code (and indeed the APIs that Meta intended to teach us in the course), I will go through what I believe they are doing, in an effort to help any future coursemates who might stumble upon the same piece of code, and in the end my question itself will be much clearer. To any more experienced coders who might be reading this: please, I invite you to correct any mistakes I make on my attempt at an explanation.
We start by creating a
RadioGroupelement whose children are variousRadioOptionelements. Since a component receives its children from its constructor, I presume that a component's children must be created before the component itself in order for them to be available for their injection inside of their parent component. This means that in this example, theRadioOptionelements are created before the initialization of theRadioGroupelement (which only makes sense, given that in the initialization of theRadioGroupelement we already have achildrenobject over which to iterate).From that point, we jump over to the creation of the
RadioOptionelements insrc/Radio/index.js. Since only thevalueattribute has been provided on their initialization insrc/App.js, what seems to happen is that only the attributes who do not depend on either thecheckedor theonChangeparameters for their initialization are properly initialized, for to the properties that receive parameters that have not been provided,undefinedis assigned, and React ignores all the properties to whomundefinedis assigned. This means that by the end of this step, eachRadioOptionelement will be a<div>with: an<input>tag with the attributesid,type,nameandvalue; and a<label>tag with the attributehtmlFor.Once this is all done, we are ready to create our
RadioGroupelement. We start by creating a copy of itschildrenobject, which is composed of all the previously createdRadioOptionelements, applying a transformation to each of its elements, and saving the copied and subsequently modified array to aconstcalledRadioOptions. Now, what interests me is how such transformation is done:Inside of the callback for the
.map()method, we clone eachRadioOptionelement and append to it two properties which it previously did not have, namely,onChangeandchecked.By the end of this process, the state setter function
setSelected, that was passed intoRadioGroupthrough means of its propertyonChange, has made its way from the parameter of theRadioGroupconstructor and into theonChangeproperty of each individualRadioOptionthrough usage of theReact.cloneElement. This means that the codeonChange={(e) => {onChange(e.target.value); }}is effectively renderedonChange={(e) => {setSelected(e.target.value); }}on the final object.
And this is what I have been able to decipher so far.
Question 1:
I do not understand the following, though:
Looking online I have found many of examples like such (this one comes from the documentation itself):
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage">
Hello
</Row>,
{ isHighlighted: true },
'Goodbye'
);
console.log(clonedElement);
This is supposed to log the following result: <Row title="Cabbage" isHighlighted={true}>Goodbye</Row>.
As can be seen, the isHighlighted property was added even though there was no coded specification for how to deal with this value such as there was in our RadioOption component to deal with the checked and onChange values. Nevertheless, it was successfully appended to the tag's clone.
This made me realize: React.cloneElement can be used to append a property to a component independently of such a property's prior implementation.
However, it is also the case that in the code for the radio buttons list above, React.cloneElement took into account the behaviour specified in the component's body when appending the property it had been provided with.
So where do we draw the line? How do I know exactly when it is going to behave in a way and when in another? Can anyone walk me through the inner workings of this method more in-depth?
Question 2:
Moreover, when we look at the example just above, we see that the passing of { isHighlighted: true } as an argument to React.cloneElement was enough to create, in the final object, a property isHighlighted with the value true.
However, in our code, we need the line checked={checked} inside of the <input>tag for the code to work properly. I believe that it might be because in the documentation's example the appending was done to a simple <Row> tag, whilst in our code the appending takes place one level deeper than our RadioOption's <div>, on the <input> tag inside of that.
Is that correct? And at last: how could I make it so that I could mimic the working of the documentation's example code, i.e., passing everything that should be appended inside the cloneElement() call and not relying in passing it as an argument to the constructor of the RadioOption component?
Thank you very much in advance.