Design Converter
Education
Last updated on Sep 15, 2023
Last updated on Aug 29, 2023
Today, we're going to embark on an exciting journey into the heart of React - Component Composition. React application all about components. Big ones, small ones, stateful ones, pure ones - they're the building blocks of any React application. But how do these components interact with each other? How do they share data and functionality? How do we organize them in a way that our code is clean, maintainable, and scalable? The answer lies in understanding Component Composition.
In this blog post, we'll dive deep into the world of composing components in React. We'll start from the basics, understand the philosophy behind component composition, and gradually move towards more advanced concepts like Higher-Order Components (HOCs), Render Props, and the Context API. We'll also explore different composition patterns and discuss how to optimize component composition for better performance and code reusability.
The philosophy behind composing components in React is rooted in the principles of software engineering and functional programming. The idea is to break down complex problems into smaller, manageable pieces. In the context of React, these pieces are the components.
Each component in React has a specific task. It encapsulates the logic required for this task, and it may contain its own state or props. The component's code is responsible for rendering some parts of the UI. When we talk about composing components, we're essentially talking about taking these independent components and combining them to form a new component.
1 // Import React 2 import React from 'react'; 3 4 // Define a Header Component 5 function Header() { 6 return ( 7 <div className="header"> 8 Header 9 </div> 10 ); 11 } 12 13 // Define a Footer Component 14 function Footer() { 15 return ( 16 <div className="footer"> 17 Footer 18 </div> 19 ); 20 } 21 22 // Define a Container Component 23 function App() { 24 return ( 25 <div className="App"> 26 <Header /> 27 <Footer /> 28 </div> 29 ); 30 } 31 32 // Export the App Component 33 export default App; 34
In the above example, the App Component is composed of the Header Component and the Footer Component. Each of these components is responsible for rendering a specific part of the UI. The App Component composes these components to form a new component that represents the entire app.
In React, props are the parameters we pass from a parent component to a child component. They allow us to pass data and event handlers down the component tree. When composing components, we often pass components as props. This is where the children prop comes into play.
The children prop is a special prop in React that allows us to pass components as data to other components. It's like a placeholder for where the child components go.
1 import React from 'react'; 2 3 function Container({ children }) { 4 return ( 5 <div className="container"> 6 {children} 7 </div> 8 ); 9 } 10 11 function App() { 12 return ( 13 <Container> 14 <div>Child Component</div> 15 </Container> 16 ); 17 } 18 19 export default App; 20
In the above example, the Container Component receives a children prop from the App Component. This children prop is a div element, but it could be any valid React element, including custom components.
Higher-Order Components (HOCs) are a pattern derived from React's compositional nature. An HOC is a function that takes a component and returns a new component with additional props or behavior.
1 import React from 'react'; 2 3 // Define a Higher-Order Component 4 function withExtraProp(Component) { 5 return function ExtraPropComponent(props) { 6 return <Component {...props} extraProp="I'm an extra prop!" />; 7 }; 8 } 9 10 function SimpleComponent({ extraProp }) { 11 return <div>{extraProp}</div>; 12 } 13 14 // Create a new component using the HOC 15 const EnhancedComponent = withExtraProp(SimpleComponent); 16 17 function App() { 18 return <EnhancedComponent />; 19 } 20 21 export default App; 22
In the above example, withExtraProp is an HOC. It's a function that takes a component (SimpleComponent) and returns a new component (EnhancedComponent) that renders the original component with an extra prop.
The render props pattern is another powerful technique in React for sharing code between components. A component with a render prop takes a function that returns a React element and calls it instead of implementing its own render logic.
1 import React from 'react'; 2 3 // Define a Component with a Render Prop 4 function DataProvider({ render }) { 5 const data = 'Hello from DataProvider!'; 6 return render(data); 7 } 8 9 function App() { 10 return ( 11 <DataProvider render={data => ( 12 <div>{data}</div> 13 )} /> 14 ); 15 } 16 17 export default App; 18
In the above example, DataProvider is a component that uses a render prop. It takes a function as a prop and calls this function with some data. The App Component uses DataProvider and passes a function that renders the data to a div. This pattern is useful when we want to share behavior across multiple components.
Compound components are a pattern in React where a parent component shares its state with its child components through the Context API. This allows child components to communicate with each other and with the parent component, which can lead to cleaner and more intuitive APIs.
1 import React, { createContext, useState, useContext } from 'react'; 2 3 // Create a Context 4 const CompoundComponentContext = createContext(); 5 6 // Define a Parent Component 7 function ParentComponent({ children }) { 8 const [value, setValue] = useState('Hello from ParentComponent!'); 9 10 return ( 11 <CompoundComponentContext.Provider value={{ value, setValue }}> 12 {children} 13 </CompoundComponentContext.Provider> 14 ); 15 } 16 17 // Define a Child Component 18 function ChildComponent() { 19 const { value } = useContext(CompoundComponentContext); 20 21 return <div>{value}</div>; 22 } 23 24 function App() { 25 return ( 26 <ParentComponent> 27 <ChildComponent /> 28 </ParentComponent> 29 ); 30 } 31 32 export default App; 33
In the above example, ParentComponent shares its state with ChildComponent through the CompoundComponentContext. ChildComponent can access the shared state using the useContext Hook.
The Context API is a feature in React that allows us to share the state and pass it through the component tree without having to pass props down manually at every level. This is particularly useful when composing components.
1 import React, { createContext, useState, useContext } from 'react'; 2 3 // Create a Context 4 const MyContext = createContext(); 5 6 // Define a Provider Component 7 function MyProvider({ children }) { 8 const [value, setValue] = useState('Hello from MyProvider!'); 9 10 return ( 11 <MyContext.Provider value={{ value, setValue }}> 12 {children} 13 </MyContext.Provider> 14 ); 15 } 16 17 // Define a Consumer Component 18 function MyConsumer() { 19 const { value } = useContext(MyContext); 20 21 return <div>{value}</div>; 22 } 23 24 function App() { 25 return ( 26 <MyProvider> 27 <MyConsumer /> 28 </MyProvider> 29 ); 30 } 31 32 export default App; 33
In the above example, MyProvider shares its state with MyConsumer through the MyContext. MyConsumer can access the shared state using the useContext Hook.
Hooks are a feature in React that allows us to use state and other React features without writing a class. They can be used to share logic between components, making it easier to compose components.
1 import React, { useState } from 'react'; 2 3 // Define a Custom Hook 4 function useCustomHook() { 5 const [value, setValue] = useState('Hello from useCustomHook!'); 6 7 return { value, setValue }; 8 } 9 10 function MyComponent() { 11 const { value } = useCustomHook(); 12 13 return <div>{value}</div>; 14 } 15 16 function App() { 17 return <MyComponent />; 18 } 19 20 export default App; 21
In the above example, useCustomHook is a custom Hook that encapsulates some state logic. MyComponent uses this Hook to share the state logic. This allows us to reuse state logic across multiple components, making it easier to compose components.
The containment pattern is one of the most common patterns in React. It involves a parent component that wraps one or more child components. The parent component can pass props to the child components, and the child components can communicate back to the parent through callbacks.
1 import React from 'react'; 2 3 // Define a Parent Component 4 function ParentComponent({ children }) { 5 return <div className="parent">{children}</div>; 6 } 7 8 // Define a Child Component 9 function ChildComponent() { 10 return <div className="child">I'm a child component!</div>; 11 } 12 13 function App() { 14 return ( 15 <ParentComponent> 16 <ChildComponent /> 17 </ParentComponent> 18 ); 19 } 20 21 export default App; 22
In the above example, ParentComponent is a container component that wraps ChildComponent. This is a simple yet powerful pattern that allows us to create complex UIs by composing components.
The specialization pattern is a way of creating a new component by inheriting the props and behavior of a base component and adding or overriding them. This pattern is less common in React due to the preference for composition over inheritance, but it can be useful in certain cases.
1 import React from 'react'; 2 3 // Define a Base Component 4 function BaseComponent({ children }) { 5 return <div className="base">{children}</div>; 6 } 7 8 // Define a Specialized Component 9 function SpecializedComponent(props) { 10 return <BaseComponent {...props} />; 11 } 12 13 function App() { 14 return <SpecializedComponent>I'm a specialized component!</SpecializedComponent>; 15 } 16 17 export default App; 18
In the above example, SpecializedComponent is a new component that inherits the props and behavior of BaseComponent and adds its own props.
Slots are a pattern for component composition that provides flexibility in where child components can be inserted. In React, we can achieve this pattern using the children prop.
1 import React from 'react'; 2 3 // Define a Slot Component 4 function SlotComponent({ leftSlot, rightSlot }) { 5 return ( 6 <div className="slot"> 7 <div className="left-slot">{leftSlot}</div> 8 <div className="right-slot">{rightSlot}</div> 9 </div> 10 ); 11 } 12 13 function App() { 14 return ( 15 <SlotComponent 16 leftSlot={<div>I'm in the left slot!</div>} 17 rightSlot={<div>I'm in the right slot!</div>} 18 /> 19 ); 20 } 21 22 export default App; 23
In the above example, SlotComponent is a component that accepts two props: leftSlot and rightSlot. These props are used as slots where we can insert any valid React elements. This pattern provides flexibility in how we can compose our components.
In the world of React, composition is often favored over inheritance. Here are a few reasons why:
While inheritance can be useful in certain cases, it has some limitations, especially in the context of React:
When composing components in React, it's important to consider the performance implications. Each time a component's state or props change, the component re-renders. If a parent component re-renders, all the child components also re-render. This can lead to unnecessary re-renders and negatively impact performance.
One way to optimize performance in component composition is by using React's shouldComponentUpdate lifecycle method or React.memo for function components. These features allow us to control when a component should re-render, preventing unnecessary re-renders.
1 import React, { memo } from 'react'; 2 3 const ChildComponent = memo(function ChildComponent({ value }) { 4 console.log('ChildComponent re-rendered!'); 5 return <div>{value}</div>; 6 }); 7 8 function App() { 9 const [value, setValue] = useState(0); 10 11 return ( 12 <div> 13 <button onClick={() => setValue(value + 1)}>Increment</button> 14 <ChildComponent value={value} /> 15 </div> 16 ); 17 } 18 19 export default App; 20
In the above example, ChildComponent is wrapped with React.memo. This means that ChildComponent will only re-render if its props change. If the parent App Component re-renders, ChildComponent will not re-render unless the value prop has changed.
Component composition in React promotes code reusability. By breaking down our UI into smaller components, we can reuse these components across our application. This leads to less code duplication and makes our code easier to maintain and test.
For example, we might have a Button Component that we use in many parts of our application. Instead of duplicating the button's code each time we need a button, we can create a single Button Component and reuse it.
1 import React from 'react'; 2 3 function Button({ onClick, children }) { 4 return <button onClick={onClick}>{children}</button>; 5 } 6 7 function App() { 8 return ( 9 <div> 10 <Button onClick={() => console.log('Button 1 clicked!')}>Button 1</Button> 11 <Button onClick={() => console.log('Button 2 clicked!')}>Button 2</Button> 12 </div> 13 ); 14 } 15 16 export default App; 17
In the above example, we define a Button Component and reuse it in the App Component. Each instance of the Button Component can have different props, allowing us to customize the button's behavior and content. This is a simple example of how component composition can promote code reusability in React.
Component composition is a fundamental concept in React that allows us to build complex UIs by combining smaller, independent components. It promotes code reusability, makes our code more readable, and allows us to better manage our application's state and behavior.
We've explored various aspects of component composition, from understanding the basic philosophy behind it, to diving deep into advanced concepts like Higher-Order Components (HOCs), Render Props, and the Context API. We've also looked at different composition patterns and how to optimize component composition for better performance and code reusability.
Understanding and effectively using component composition is crucial for building scalable and maintainable React applications. As we continue to build our React applications, we should strive to break down our UI into smaller components and compose these components to form more complex UIs. This approach will not only make our code more manageable but also improve our application's performance. Ready to put these concepts into action? Go ahead, start composing, and see the magic unfold in your React applications.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.