In React, there are several techniques for reusing components, data, and logic, such as HOCs and Custom Hooks. This time, let's take a look at the render props pattern.
What is the Render Props Pattern?
Quite simply, Render Props is a pattern where you pass a function that returns JSX as a prop to a component, delegating what to render to the parent (external). Here is a simple example. (Note that the prop name doesn't necessarily have to be render.)
// (1) Component providing the shell
const Box = ({ render }) => {
return (
<div style={{ border: '2px solid blue', padding: '20px', borderRadius: '8px' }}>
{render()}
</div>
);
};
// (2) Usage: "I want to put text"
<Box render={() => <span>This is a simple text.</span>} />
// (3) Usage: "I want to put an image"
<Box render={() => <img src="logo.png" alt="Logo" />} />As you can see, it's a pattern of receiving and calling a function as a prop that returns JSX. It's so simple that it might not be immediately clear why this pattern is useful or how it can be used.
Render Props Passing Data
Let's take it a step further. For example, a child component might manage certain data, but the parent may need to display that data differently.
// (1) Component that holds data
const UserProvider = ({ render }) => {
const user = { name: "Sanghyeon", role: "Developer" };
// Pass the data through the render function
return <div>{render(user)}</div>;
};
// (2) Usage A: When you want to see the name in a large font
<UserProvider render={(user) => <h1>{user.name}</h1>} />
// (3) Usage B: When you want to see both the role and name
<UserProvider render={(user) => <p>{user.name} ({user.role})</p>} />In this way, we can freely handle and render the data held by the component from the outside (parent).
Utilizing the children prop
In the above examples, we used a prop named render, but it is more common to utilize the children prop. Using this approach makes it look like a standard component wrapping its children, which improves readability.
// (1) Execute children as a function instead of a render prop
const UserProvider = ({ children }) => {
const user = { name: "Sanghyeon", role: "Developer" };
return <div>{children(user)}</div>;
};
// (2) Usage: The structure becomes much more intuitive.
<UserProvider>
{(user) => (
<div>
<h1>{user.name}</h1>
<p>{user.role}</p>
</div>
)}
</UserProvider>Replacement with Hooks
However, the current code can also be sufficiently replaced with Hooks. If you're only dealing with data, using Hooks might be cleaner and simpler.
// Code converted to a Hook
const useUser = () => {
const user = { name: "Sanghyeon", role: "Developer" };
return user;
};
// (1) Usage A: Large name
function LargeName() {
const user = useUser();
return <h1>{user.name}</h1>;
}
// (2) Usage B: Name and role
function NameAndRole() {
const user = useUser();
return <p>{user.name} ({user.role})</p>;
}In modern React development, many Render Props patterns have been replaced by Hooks. However, there is an area where Hooks struggle. That is "when logic must be coupled with a specific DOM structure."
Practical Use Case
Let's implement infinite scroll. While there are various ways to implement infinite scroll, let's say we use IntersectionObserver and a dummy component to detect the end of the scroll.
function PostListContainer() {
const { items, fetchNextPage, hasMore, isLoading } = useInfiniteScrollHook();
const observerRef = useRef(null); // Managed directly by the user
return (
<div>
<PostList items={items} />
// (1) Inconvenience: UI handling based on data (isLoading) must be done manually every time
{isLoading && <MyCustomSpinner />}
// (2) Inconvenience: You must remember to manually insert the 'dummy div' for the logic
{hasMore && <div ref={observerRef} style={{ height: '20px' }} />}
</div>
);
}Hooks only provide the logic; the developer must manually handle the ref and DOM connection. If this logic is repeated, it becomes quite a tedious task.
Refactoring with Render Props
// (1) Render Props Component
function InfiniteScroll({ onLoadMore, hasMore, children }) {
const [isLoading, setIsLoading] = useState(false);
const observerRef = useRef(null);
useEffect(() => {
if (!hasMore || isLoading) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsLoading(true);
onLoadMore().finally(() => setIsLoading(false));
}
});
if (observerRef.current) observer.observe(observerRef.current);
return () => observer.disconnect();
}, [hasMore, onLoadMore, isLoading]);
return (
<div>
// Pass the current loading state to the child function
{children(isLoading)}
// The component takes responsibility for the essential dummy element
{hasMore && <div ref={observerRef} style={{ height: '20px' }} />}
</div>
);
}
// (2) Usage: Decide the UI by receiving the internal state isLoading
<InfiniteScroll hasMore={hasMore} onLoadMore={fetchNextPage}>
{(isLoading) => (
<>
<PostList items={items} />
// The parent decides whether to put the loading bar right below the list or as a small spinner in the top right
{isLoading && <MyCustomSpinner />}
</>
)}
</InfiniteScroll>By using the Render Props pattern, you can provide logic and the essential DOM structure as a set. The user no longer needs to worry about observerRef, and they gain control over how to display isLoading by receiving it via the children prop.
Pros and Cons
Pros
- Complete Encapsulation of Logic and Structure: When specific DOM elements (like a dummy div) are mandatory for the logic to work, as in the
InfiniteScrollexample, you can hide them inside the component to maximize reusability. - Delegation of Control: The component manages the data, but the parent using it determines 100% "how it is displayed." This allows for very flexible UI compositions.
Cons
- Render Prop Hell (Nesting issues): If you nest multiple Render Props, the depth of the code increases, which can sharply decrease readability.
- Verbose Code compared to Hooks: For simple logic sharing, it requires more code and can look more complex than using Hooks.
Wrapping Up
Hooks are sufficient for simply reusing data or logic. However, if you want to encapsulate both logic and DOM structure, try using the Render Props pattern.
If you're still not sure how to utilize it, check out this collection of libraries using the Render Props pattern for some inspiration!
References