When creating a React project, you might need to share some state between components of your application. React Context API offers a mechanism for passing data through the component tree without relying on props (also known as props drilling).
I have seen teams implement a lightweight global store for their React application using React Context as an alternative to using a state management library such as Redux.
This article shows how you can build reusable Accordion
compound components that rely on React Context to share state and control the visibility of collapsible content.
You can find the completed project on GitHub.
You can see the Accordion
in action:
The building blocks of the Accordion
are CollapseButton
and ExpandButton,
which toggle the visibility of the Collapsible
content when clicked.
In the main App file we use the Accordion
as follows:
<Accordion>
<h1>Accordion</h1>
<Accordion.CollapseButton>Show less</Accordion.CollapseButton>
<p>
In pharetra nibh massa, ...
<Accordion.Collapsible>
Vestibulum feugiat ...
</Accordion.Collapsible>
</p>
<Accordion.ExpandButton>Show more...</Accordion.ExpandButton>
<Accordion.CollapseButton>Show less</Accordion.CollapseButton>
</Accordion>;
Since the Accordion
exposes compound components, we get the flexibility to place the ExpandButton,
CollapseButton,
and Collapsible
content in any order or configuration we need. From my experience, creating compound components that enable this kind of composability has the benefits of reducing the number of props, complexity, and potential bugs introduced. Thus, it is generally a good practice.
Let's take a look at the implementation of Accordion
:
const Accordion: FunctionComponent & AccordionComposition = ({ children }) => {
const [isExpanded, setIsExpanded] = useState(false);
const expand = () => setIsExpanded(true);
const collapse = () => setIsExpanded(false);
return (
<AccordionProvider value={{ isExpanded, expand, collapse }}>
{children}
</AccordionProvider>
);
};
If you are wondering about the typings in the first line
const Accordion: FunctionComponent & AccordionComposition
, you can check out my previous article React Compound Components Typings. It explains how you can define and export Compound Components using TypeScript.
The Accordion
component is responsible for storing the isExpanded
state, and defining means through which the state can be updated (via expand,
collapse
functions). As you will see further in this article, the Collapsible
, ExpandButton
and CollapseButton
need to know and modify this state.
This is where React Context comes into play and shines!
The AccordionProvider
is an important part of the React Context mechanism because it gives children in the tree access to a value or the shared state. For example, the value is an object with three properties: isExpanded
, expand
and collapse
. The type of this object is defined as follows:
interface AccordionContextValue {
isExpanded: boolean;
expand: () => void;
collapse: () => void;
}
To understand how AccordionProvider
is defined we must first understand how the AccordionContext
is created. We rely on createContext
function imported from React
.
import { createContext } from "react";
The createContext
function accepts a default value as an argument. In this case, I chose the undefined
value. We then specify the type of the value passed through the context which can either be AccordionContextValue
or the undefined
value.
const AccordionContext = createContext<AccordionContextValue | undefined>(undefined);
The AccordionContext
exposes a Provider
which we can export as AccordionProvider
export const AccordionProvider = AccordionContext.Provider;
According to React Documentation:
Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes.
The Provider component accepts a value prop to be passed to consuming components that are descendants of this Provider. One Provider can be connected to many consumers. Providers can be nested to override values deeper within the tree.
All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes.
To consume the value provided through AccordionProvider
we can use useContext
hook:
import { useContext } from "react";
const context = useContext(AccordionContext);
It is a good idea to check whether we have an undefined context since it is an indication that Collapsible
, ExpandButton
or CollapseButton
were used outside of an Accordion
parent. Such a usage is unintended. The useAccordionContext
custom hook abstracts away this logic and allows reusing it in all places we need:
export const useAccordionContext = () => {
const context = useContext(AccordionContext);
if (!context) {
throw new Error(
"Accordion.Collapsible, Accordion.ExpandButton and Accordion.CollapseButton must be used within an Accordion"
);
}
return context;
};
We can then destructure the isExpanded
value from useAccordionContext
and conditionally render children
of the Collapsible
component:
const Collapsible: FunctionComponent = ({ children }) => {
const { isExpanded } = useAccordionContext();
if (isExpanded) {
return <>{children}</>;
} else {
return null;
}
};
In the ExpandButton
component, we are also using the expand
function when someone clicks the button
to toggle the visibility of the collapsible content. Besides, we control when ExpandButton
is rendered depending on whether isExpanded
is false.
const ExpandButton: FunctionComponent = ({ children }) => {
const { isExpanded, expand } = useAccordionContext();
if (!isExpanded) {
return (
<button className={styles.button} onClick={expand}>
{children}
</button>
);
} else {
return null;
}
};
Similarly, in the CollapseButton
we are using the collapse
function and also controlling when CollapseButton
is rendered depending on isExpanded
.
const CollapseButton: FunctionComponent = ({ children }) => {
const { isExpanded, collapse } = useAccordionContext();
if (isExpanded) {
return (
<button className={styles.button} onClick={collapse}>
{children}
</button>
);
} else {
return null;
}
};
To conclude, React Context API is a powerful tool for passing state through the tree of children. We looked at an example of how to build Accordion
compound components that share and modify some state and how we can abstract that mechanism away for consumers of those components.
Thanks for reading!