import Callout from "../../../components/Callout"; import StackBlitz from "../../../components/StackBlitz";
Create multiple components that work together to perform a single task
With the Compound Pattern, we can create multiple components that work together to perform one single task.
Let's say for example that we have a Search input component. When a user clicks on the search input, we show a SearchPopup component that shows some popular locations.
To create this behavior, we can create a FlyOut compound component.
This FlyOut component is an example of a compound component, as it also exposes some sub-components that all work together to toggle and render the FlyOut component.
import React from "react";
import { FlyOut } from "./FlyOut";
export default function SearchInput() {
return (
<FlyOut>
<FlyOut.Input placeholder="Enter an address, city, or ZIP code" />
<FlyOut.List>
<FlyOut.ListItem value="San Francisco, CA">
San Francisco, CA
</FlyOut.ListItem>
<FlyOut.ListItem value="Seattle, WA">Seattle, WA</FlyOut.ListItem>
<FlyOut.ListItem value="Austin, TX">Austin, TX</FlyOut.ListItem>
<FlyOut.ListItem value="Miami, FL">Miami, FL</FlyOut.ListItem>
<FlyOut.ListItem value="Boulder, CO">Boulder, CO</FlyOut.ListItem>
</FlyOut.List>
</FlyOut>
);
}The FlyOut compound component is a stateful component - which means we don't have to add the stateful logic to the SearchInput component.
We can implement the Compound pattern using either a Provider, or React.Children.map.
The FlyOut compound component consists of:
FlyoutContextto keep track of the visbility state ofFlyOutInputto toggle theFlyOut'sListcomponent's visibilityListto render theFlyOut'sListItemssListItemthat gets rendered within theList.
const FlyOutContext = React.createContext();
export function FlyOut(props) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState("");
const toggle = React.useCallback(() => setOpen((state) => !state), []);
return (
<FlyOutContext.Provider value={{ open, toggle, value, setValue }}>
<div>{props.children}</div>
</FlyOutContext.Provider>
);
}
function Input(props) {
const { value, toggle } = React.useContext(FlyOutContext);
return <input onFocus={toggle} onBlur={toggle} value={value} {...props} />;
}
function List({ children }) {
const { open } = React.useContext(FlyOutContext);
return open && <ul>{children}</ul>;
}
function ListItem({ children, value }) {
const { setValue } = React.useContext(FlyOutContext);
return <li onMouseDown={() => setValue(value)}>{children}</li>;
}
FlyOut.Input = Input;
FlyOut.List = List;
FlyOut.ListItem = ListItem;Although we didn't have to name our compound component's sub-components FlyOut.<ComponentName>, it's an easy way to identify compound components, and only requires a single import.
Another way to implement the Compound pattern, is to use React.Children.map in combination with React.cloneElement. Instead of having to use the Context API like in the previous example, we now have access to these two values through props.
export function FlyOut(props) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState("");
const toggle = React.useCallback(() => setOpen((state) => !state), []);
return (
<div>
{React.Children.map(props.children, (child) =>
React.cloneElement(child, { open, toggle, value, setValue })
)}
</div>
);
}
function Input(props) {
const { value, toggle } = props;
return <input onFocus={toggle} onBlur={toggle} value={value} {...props} />;
}
function List({ children, open }) {
return open && <ul>{children}</ul>;
}
function ListItem({ children, value, setValue }) {
return <li onMouseDown={() => setValue(value)}>{children}</li>;
}
FlyOut.Input = Input;
FlyOut.List = List;
FlyOut.ListItem = ListItem;All children components are cloned, and passed the value of open, toggle, value and setValue.
State management: Compound components manage their own internal state, which they share among the several child components. When implementing a compound component, we don't have to worry about managing the state ourselves. Single import: When importing a compound component, we don't have to explicitly import the child components that are available on that component. Nested components: When using `React.Children.map`, only direct children of the parent component will have access to the open and toggle props, meaning we can't wrap any of these components in another component.
function FlyoutMenu() {
return (
<FlyOut>
{/* This breaks, since the direct child of FlyOut is now a div */}
<div>
<FlyOut.Input />
<FlyOut.List>
<FlyOut.ListItem>San Francisco, CA</FlyOut.ListItem>
<FlyOut.ListItem>Seattle, WA</FlyOut.ListItem>
</FlyOut.List>
</div>
</FlyOut>
);
}Create a FlyOut component that gets rendered by the Input component. This component should contain:
FlyOut.Inputto render the input fieldFlyOut.Listto render the list itemsFlyOut.ListItemto render the list items