Using React’s Context API to Simply Components
Featuring: Radio Buttons and a Radio Group
In this article I’ll demonstrate rendering a list of items (<Radio />
) and how we can useContext()
to simplify the props we are required to pass to a <Radio />
component.
What is React’s Context API?
React’s Context API offers a different way to pass props to child components. Context provides a way to pass data through the component tree without having to pass props down manually at every level.
In a typical React application, data is passed top-down (parent to child) via props, but such usage can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.
If you’d like to learn more about React’s Context API, I recommend visiting the React Docs.
Simplifying the Component
We must first define a component and see how it’s used before we can identify any existing problems and how useContext
could solve them. For this article we’re going to define a very basic <Radio />
component which renders a single input[type="radio"]
element.
const Radio = ({ value, checked, name, onChange }) => (
<input
type="radio"
checked={checked}
onChange={onChange}
name={name}
value={value}
/>
);
Now let’s take a look at how we could use the <Radio />
component to build a simple page with three <Radio />
buttons: small, medium, and large.
Note: we’re going to improve and rewrite <ExampleComponent />
at the end of this article.
const ExampleComponent = () => {
const [value, setValue] = useState('small'); const handleChange = (event) => {
setValue(event.target.value);
}; const handleSubmit = (event) => {
event.preventDefault();
console.log(event.target.size.value);
}; return (
<div>
<h1>Example Component</h1>
<form onSubmit={handleSubmit}>
<label>
<Radio
checked={value === 'small'}
onChange={handleChange}
name="size"
value="small"
/>
<span>Small</span>
</label>
<label>
<Radio
checked={value === 'medium'}
onChange={handleChange}
name="size"
value="medium"
/>
<span>Medium</span>
</label>
<label>
<Radio
checked={value === 'large'}
onChange={handleChange}
name="size"
value="large"
/>
<span>Large</span>
</label>
<button type="submit">Submit</button>
</form>
</div>
);
};
As you look at the code above, notice the props
being passed to each of the <Radio />
components. Specifically, the checked
, onChange
, and name
props. Those three props are almost all the same for each of the three components:
name="size"
onChange={onChange}
checked={value === T}
where T is different for each<Radio />
Wouldn’t it be nice if we didn’t have to pass all these props directly to the <Radio />
component? … Yes, it would be and that’s exactly what we’re going to do in the sections below.
Step 1: Creating a New Component
Let’s add a new component to the library and call it <RadioGroup />
. The Radio Group will make rendering a list of <Radio />
components easier. The Radio Group will take three additional props: onChange
, name
, and value
. In the sections below we will expose these three props to the <Radio />
component using the Context API.
const RadioGroup = ({ children, onChange, name, value }) => {
return (
<div>{children}</div>
);
}
Step 2: Adding `Context` to the RadioGroup
The parent component <RadioGroup />
which renders the children can use React’s Context API to store the onChange
, name
, and value
prop. Then all children beneath the <RadioGroup />
component will have access to these three props. Therefore, each <Radio />
component will no longer need an explicit checked
, name
, nor value
prop passed to them.
const RadioGroupContext = createContext();const RadioGroup = ({ children, onChange, name, value }) => {
return (
<RadioGroupContext.Provider value={{ onChange, name, value }}>
<div>{children}</div>
</RadioGroupContext.Provider>
);
};
Step 3: Updating the Radio Component with `useContext`
Anytime we define a Context.Provider
like RadioGroupContext.Provider
we want to also define a Context.Consumer
which subscribes to context changes and has access to the values set by the Provider. Since we’re using React Hooks we’ll leverage the useContext()
hook instead of using Context.Consumer
.
function useRadioGroup() {
return useContext(RadioGroupContext);
}const Radio = ({
value,
checked: checkedProp,
name: nameProp,
onChange: onChangeProp,
}) => {
// Subscribe to the context provider
const radioGroup = useRadioGroup(); const onChange = (event) => {
if (onChangeProp) {
onChangeProp(event);
}
if (radioGroup && radioGroup.onChange) {
radioGroup.onChange(event);
}
}; let name = nameProp;
let checked = checkedProp;
if (radioGroup) {
if (typeof checked === 'undefined') {
checked = radioGroup.value === value;
}
if (typeof name === 'undefined') {
name = radioGroup.name;
}
} return (
<input
type="radio"
checked={checked}
onChange={onChange}
name={name}
value={value}
/>
);
}
You’ll notice the <Radio />
component has quite a bit more code than the initial implementation had, but that’s a tradeoff we’ve decided to make: more code in the implementation of <Radio />
for a more simplified external component usage.
Step 4: Simplifying the Example Component
Now let’s rewrite the <ExampleComponent />
(which we initially defined at the top of this article) and this time we’ll use the new <RadioGroup />
component.
const ExampleComponent = () => {
const [value, setValue] = useState('small'); const handleChange = (event) => {
setValue(event.target.value);
}; const handleSubmit = (event) => {
event.preventDefault();
console.log(event.target.size.value);
}; return (
<div>
<h1>Example Component</h1>
<form onSubmit={handleSubmit}>
<RadioGroup
onChange={handleChange}
name="size"
value={value}
>
<label>
<Radio value="small" />
<span>Small</span>
</label>
<label>
<Radio value="medium" />
<span>Medium</span>
</label>
<label>
<Radio value="large" />
<span>Large</span>
</label>
</RadioGroup>
<button type="submit">Submit</button>
</form>
</div>
);
};
Conclusion: What’s the Difference between ‘No RadioGroup’ and ‘RadioGroup’
If we only examine the code required to use the <Radio />
component, then we can see quite clearly that using the <Radio />
requires much less code when using the <RadioGroup />
component.
Before: the code required to render a Radio without using the RadioGroup component
<label>
<Radio
checked={value === 'medium'}
onChange={handleChange}
name="size"
value="medium"
/>
<span>Medium</span>
</label>
After: the code required to render a Radio when using the RadioGroup component
<label>
<Radio value="medium" />
<span>Medium</span>
</label>