Render-props pattern in React
The render-props pattern is a technique for sharing props between components. It involves passing a function as a prop to a component, which then renders this function as part of its output.
As an overview, render-props pattern looks like this;
import { ReactNode, FC } from "react";
interface ProfileDisplayPropType {
render: (name: string) => ReactNode;
}
const ProfileDisplay: FC<DisplayNameProps> = (props) => {
const [name] = useState("John Doe");
return (
<div>
<p>Details</p>
{props.children(name)}
</div>
)
}
const App = () => {
return (
<>
<ProfileDisplay
render={name => (<p>{name}</p>)}
/>
</>
)
}
In the context above, you can see that we have a ProfileDisplay component that accepts a render prop. The render prop is a function that receives a name argument which simply returns a ReactNode. Also, a state variable called name is initialized using the useState hook. It is then passed as a prop to the render function in the return statement of the component where it's being rendered.
Next, within the App component, ProfileDisplay is rendered and a JSX function, with access to the name argument, is passed as a value to the render prop. The result of this would be having the p tag rendered to the DOM.
In a more detailed example,
interface PersonType {
[key: string]: string | number;
firstName: string;
lastName: string;
};
interface Person {
person: PersonType;
setPerson: React.Dispatch<React.SetStateAction<PersonType>>;
}
interface ProfileDisplayPropType {
children: (data: Person) => ReactNode;
}
const ProfileDisplay: FC<ProfileDisplayPropType> = (props) => {
const [person, setPerson] = useState({
firstName: "John",
lastName: "Doe",
});
return (
<div>
<p>Details</p>
{props.children({ person, setPerson })}
</div>
);
};
const DisplayFullName = ({
name
}: {name: string}) => {
return (
<div>
<span>Full Name:</span>{" "}
<span>
{firstName} {lastName}
</span>
</div>
);
};
const EditFullName = ({ person, setPerson }: Person) => {
const handleChange = (e: React.FormEvent<HTMLInputElement>) => {
const value = e.currentTarget.value;
const name = e.currentTarget.name;
setPerson((_person) => ({ ..._person, [name]: value }));
};
return (
<form>
<input
name="firstName"
value={person.firstName}
onChange={(e: React.FormEvent<HTMLInputElement>) => handleChange(e)}
/>
<input
name="lastName"
value={person.lastName}
onChange={(e: React.FormEvent<HTMLInputElement>) => handleChange(e)}
/>
</form>
);
};
const App = () => {
return (
<div>
<ProfileDisplay>
{({ person, setPerson }) => {
return (
<>
<EditFullName person={person} setPerson={setPerson} />
<DisplayFullName
name={`${person.firstName} ${person.lastName}`}
/>
</>
);
}}
</ProfileDisplay>
</div>
);
}
In this extended example, the concept around the ProfileDisplay component remains the same. The state name is now 'person' and its value is now an object just so we could accommodate having the first and last names separately.
Next, we have two new components DisplayFullName and EditFullName. DisplayFullName accepts a name string props while EditFullName takes person and its setter, setPerson, as props.
The DisplayFullName component simply displays the name content it receives, while the EditFullName is a form field that can be used to edit these details, hence the reason for passing the setter.
The App component then simply renders the ProfileDisplay component which serves as a wrapper component that provides data and functionality to its children components. It takes a function, which returns a JSX, as a child and calls it with two arguments: person and setPerson. Throught these props, the EditFullName and DisplayFullName components are provided access to the internal state of the ProfileDisplay components. DisplayFullName and EditFullName would simply be able to display and edit the person state, respectively, due to this access.
The benefit of this is that by using the Render Props pattern, the ProfileDisplay component can encapsulate the shared state( person ) and provide it to multiple child components without the need for complex state management. It allows components to be more flexible and reusable by delegating the rendering logic to the consuming components.
Subscribe to my newsletter
Read articles from Oluwatobi Adegoke directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Oluwatobi Adegoke
Oluwatobi Adegoke
I specialize in creating powerful user interfaces using React, Next.js, and React Native for web and mobile applications.