|
| 1 | +--- |
| 2 | +templateKey: "blog-post" |
| 3 | +title: "React compound components (Hooks + typescript)" |
| 4 | +date: 2021-05-25 |
| 5 | +featuredpost: false |
| 6 | +description: >- |
| 7 | + This article talks about what are compound components and how to build an accordion component using the concept of compound components. |
| 8 | +keywords: |
| 9 | +- reactjs |
| 10 | +- Compound-components |
| 11 | +- Hooks |
| 12 | +- Typescript |
| 13 | +link: /react-compound-components |
| 14 | +category: |
| 15 | +- Tutorial |
| 16 | +author: Ashwin kumar |
| 17 | +tags: |
| 18 | +- react js |
| 19 | +- reactjs |
| 20 | +- typescript |
| 21 | +- compound components |
| 22 | +- build accordion component using compound components |
| 23 | +--- |
| 24 | + |
| 25 | +**Compound Components** are an advanced and great pattern which you can use to create the meta component. It gives your components more flexibility. It is little difficult to understand the concept initially, But believe me, if you get a hold of it you will love building the components using compound components pattern |
| 26 | + |
| 27 | +## What are compound components? |
| 28 | + |
| 29 | +The compound components are a set of two or more components that work together to accomplish a specific task. The set of components will share an implicit state to communicate between them. |
| 30 | + |
| 31 | +> Think of compound components like the select and option elements in HTML. Apart they don’t do too much, but together they allow you to create the complete experience. — Kent C. Dodds |
| 32 | +
|
| 33 | +```jsx |
| 34 | +<select> |
| 35 | + <option>Option1</option> |
| 36 | + <option>Option2</option> |
| 37 | + <option>Option3</option> |
| 38 | + <option>Option4</option> |
| 39 | +</select> |
| 40 | +``` |
| 41 | + |
| 42 | +In the above example, when you click on an option in the select component, select knows which option you clicked. The select and the option share the state between them and update the selected option state on their own, we don’t need to explicitly configure them. |
| 43 | + |
| 44 | +## Compound Components in Action |
| 45 | + |
| 46 | +In this post, we’ll be building an Accordion component using compound components. |
| 47 | + |
| 48 | +You can check out the [Code sandbox link](https://codesandbox.io/s/interesting-wildflower-wj3iy) for the final demo of the accordion component. |
| 49 | + |
| 50 | +The accordion component will have four components. |
| 51 | + |
| 52 | +1. **Accordion** - The outer wrapper component of the Accordion component. This is the root component of the Accordion component. |
| 53 | +2. **AccordionItem** - The component that allows us to define each accordion item. Each AccordionItem will have its AccordionButton and AccordionPanel components. |
| 54 | +3. **AccordionButton** - The header for the Accordion component.On clicking the accordion button will open the corresponding accordion panel. |
| 55 | +4. **AccordionPanel** - The panel for the accordion. This will hold the content of each accordion item. |
| 56 | + |
| 57 | +We are going to create the above mentioned components one by one and also let's see how we can create the link between them. |
| 58 | +Let's start with Accordion component. Accordion component will wrap all other necessary components and will maintain the state that is to be shared among all the other components |
| 59 | + |
| 60 | +```tsx |
| 61 | +const Accordion: React.FC<{ |
| 62 | + children: ReactNode | ReactNode[]; |
| 63 | + className?: string |
| 64 | +}> = ({ children, className }) => { |
| 65 | + const [activeItem, setActiveItem] = useState(""); |
| 66 | + // function to update the active item |
| 67 | + const changeActiveItem = useCallback( |
| 68 | + (value) => { |
| 69 | + if (activeItem !== value) setActiveItem(value); |
| 70 | + }, |
| 71 | + [setActiveItem, activeItem] |
| 72 | + ); |
| 73 | + |
| 74 | + return <div className={className}>{children}</div> |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +we had created the accordion component, now we need to pass or share the state and the function to update the state to its children. To achieve this, we are going to use the **React Context**. If you are not familiar with React context, please refer [https://reactjs.org/docs/context.html](https://reactjs.org/docs/context.html). Iam not gonna explain about react context here, but i will give you a hint about react context. |
| 79 | + |
| 80 | +> Context provides a way to pass data through the component tree without having to pass props down manually at every level. |
| 81 | +
|
| 82 | +Now let's add context to our accordion component. |
| 83 | + |
| 84 | +```tsx |
| 85 | +import { createContext, useContext } from "react"; |
| 86 | + |
| 87 | +// Creating the context for the Accordion. |
| 88 | +export const AccordionContext = createContext<{ |
| 89 | + activeItem: string; |
| 90 | + changeSelectedItem: (item: string) => void; |
| 91 | +}>({ activeItem: "", changeSelectedItem: () => {} }); |
| 92 | + |
| 93 | +export const useAccordionContext = () => { |
| 94 | + const context = useContext(AccordionContext); |
| 95 | + if (!context) { |
| 96 | + throw new Error("Error in creating the context"); |
| 97 | + } |
| 98 | + return context; |
| 99 | +}; |
| 100 | +``` |
| 101 | + |
| 102 | +After creating the context, we need to provide values to the context, it is done by **Context.Provider** element. The values refers to the data that is to be shared among all the components in the accordion. 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. |
| 103 | + |
| 104 | +```tsx |
| 105 | +const Accordion: React.FC<{ |
| 106 | + children: ReactNode | ReactNode[]; |
| 107 | + className?: string; |
| 108 | +}> = ({ children, className }) => { |
| 109 | + const [activeItem, setActiveItem] = useState(""); |
| 110 | + |
| 111 | + const changeActiveItem = useCallback( |
| 112 | + (value) => { |
| 113 | + if (activeItem !== value) setActiveItem(value); |
| 114 | + }, |
| 115 | + [setActiveItem, activeItem] |
| 116 | + ); |
| 117 | + return ( |
| 118 | + <AccordionContext.Provider |
| 119 | + value={{ activeItem, changeSelectedItem: changeActiveItem }} |
| 120 | + > |
| 121 | + <div className={`accordion ${className}`}>{children}</div> |
| 122 | + </AccordionContext.Provider> |
| 123 | + ); |
| 124 | +}; |
| 125 | + |
| 126 | +export default Accordion; |
| 127 | +``` |
| 128 | + |
| 129 | +In the accordion, we need to share the activeItem and changeSelectedItem to all the other components, so we are passing those two to the value of accordion context provider. Now We had built the root accordion component and also we have provided the values to the context. Let's build the remaining components and we are going to use the values consumed from the context and make the accordion component work as a whole. |
| 130 | + |
| 131 | +```tsx |
| 132 | +// AccordionItem component |
| 133 | +export const AccordionItem: React.FC<{ |
| 134 | + children: ReactNode[]; |
| 135 | + label: string; |
| 136 | +className?: string; |
| 137 | +}> = ({ children, label, className }) => { |
| 138 | + const childrenArray = React.Children.toArray(children); |
| 139 | + |
| 140 | + // label is used to distinguish between each accordion element. |
| 141 | + // Adding the label prop to the children of accordionItem along with other props. |
| 142 | + const accordionItemChildren = childrenArray.map((child) => { |
| 143 | + if (React.isValidElement(child)) { |
| 144 | + return React.cloneElement(child, { |
| 145 | + ...child.props, |
| 146 | + label |
| 147 | + }); |
| 148 | + } |
| 149 | + return null; |
| 150 | + }); |
| 151 | + return <div className={className}>{accordionItemChildren}</div>; |
| 152 | +}; |
| 153 | +``` |
| 154 | + |
| 155 | +```tsx |
| 156 | +// AccordionButton component |
| 157 | +export const AccordionButton: React.FC<{ |
| 158 | + children: ReactNode; |
| 159 | + label?: string; |
| 160 | + className?:string |
| 161 | +}> = ({ label, children, className }) => { |
| 162 | + const { changeSelectedItem } = useAccordionContext(); |
| 163 | + const accordionButtonClickHandler = useCallback(() => { |
| 164 | + changeSelectedItem(label || ""); |
| 165 | + }, [changeSelectedItem, label]); |
| 166 | + |
| 167 | + return ( |
| 168 | + <div onClick={accordionButtonClickHandler} className={`accordion-button ${className}`}> |
| 169 | + {children} |
| 170 | + </div> |
| 171 | + ); |
| 172 | +}; |
| 173 | +``` |
| 174 | + |
| 175 | +```tsx |
| 176 | +// AccordionPanel component |
| 177 | +export const AccordionPanel: React.FC<{ |
| 178 | + children: ReactNode; |
| 179 | + label?: string; |
| 180 | + className?:string |
| 181 | +}> = ({ children, label, className }) => { |
| 182 | + const { activeItem } = useAccordionContext(); |
| 183 | + const panelStyles = [ |
| 184 | + "accordion-panel", |
| 185 | + label === activeItem ? "show-item" : "hide-item", |
| 186 | + className |
| 187 | + ].join(" "); |
| 188 | + |
| 189 | + return <div className={panelStyles}>{children}</div>; |
| 190 | +}; |
| 191 | +``` |
| 192 | + |
| 193 | +We have done with creating all other components. Let's see what we have done. |
| 194 | + |
| 195 | +- In the Accordion-item, label prop is used to differentiate between different accordion items. The label prop will be required in the accordionButton and accordionPanel components, so we are adding label prop to the accordionItem children along with the other props. |
| 196 | +- We are using the **useAccordionContext** in the AccordionPanel and AccordionButton. That is how we get the data that is being provided from the Accordion component. |
| 197 | +- We use changeSelectedItem in the AccordionButton component to update the active item when the button is clicked. |
| 198 | +- We use activeItem in the AccordionPanel component whether show the content or hide the content |
| 199 | + |
| 200 | +Now we had built the accordion component completely, let's see how we can use the Accordion component |
| 201 | + |
| 202 | +```tsx |
| 203 | +import { |
| 204 | + Accordion, |
| 205 | + AccordionButton, |
| 206 | + AccordionItem, |
| 207 | + AccordionPanel |
| 208 | +} from "./components/accordion"; |
| 209 | + |
| 210 | +export default function App() { |
| 211 | + return ( |
| 212 | + <div className="App"> |
| 213 | + <Accordion> |
| 214 | + <AccordionItem label="react"> |
| 215 | + <AccordionButton>React</AccordionButton> |
| 216 | + <AccordionPanel> |
| 217 | + <p> |
| 218 | + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do |
| 219 | + eiusmod tempor incididunt ut labore et dolore magna aliqua. |
| 220 | + </p> |
| 221 | + </AccordionPanel> |
| 222 | + </AccordionItem> |
| 223 | + <AccordionItem label="angular"> |
| 224 | + <AccordionButton>Angular</AccordionButton> |
| 225 | + <AccordionPanel> |
| 226 | + <p> |
| 227 | + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui |
| 228 | + officia deserunt mollit anim id est laborum. |
| 229 | + </p> |
| 230 | + </AccordionPanel> |
| 231 | + </AccordionItem> |
| 232 | + <AccordionItem label="javascipt"> |
| 233 | + <AccordionButton>Javasciprt</AccordionButton> |
| 234 | + <AccordionPanel> |
| 235 | + Duis aute irure dolor in reprehenderit in voluptate velit esse |
| 236 | + cillum dolore eu fugiat nulla pariatur. |
| 237 | + </AccordionPanel> |
| 238 | + </AccordionItem> |
| 239 | + </Accordion> |
| 240 | + </div> |
| 241 | + ); |
| 242 | +} |
| 243 | +``` |
| 244 | +Yayyy!!! We had built the accordion component using the compound components. |
| 245 | + |
| 246 | + We can also build the same accordion component using render props method but there are many limitations to style the inner components (AccordionButton & AccordionPanel) , we need to pass props like renderAccordionButton, buttonClassName for the AccordionButton and we need separate props for AccordionItem and also AccordionPanel. |
| 247 | + |
| 248 | + Look at the accordion component now, it looks clean, you can style each and every component of Accordion using its respective component. In future, if you want to have buttonColour for the AccordionButton component, you can just add that prop to the AccordionButton alone and not to the outer accordion component. |
| 249 | + |
| 250 | + Feel free to try it and check out the component in [code sandbox](https://codesandbox.io/s/interesting-wildflower-wj3iy). I hope you had understood the compound components and how to use them. |
0 commit comments