Search⌘ K
AI Features

Render Props in React

Explore how to implement render props in React to build flexible and reusable components for searching, sorting, and filtering data. Understand how to manage state and props effectively while handling dynamic data input and improving code clarity and type safety using TypeScript generics.

So far, all of our application examples have a single functionality, either searching, sorting, or filtering. But what if we want to have search, sort, and filter abilities acting on our data in combination? Unfortunately, if we combine the code for search, sorting, and filtering, our App.tsx becomes bloated and hard to understand.

TypeScript 3.3.4
function App() {
const [query, setQuery] = useState<string>("");
const [showPeople, setShowPeople] = useState<boolean>(false);
const [widgetSortProperty, setWidgetSortProperty] = useState<
ISorter<IWidget>
>({ property: "title", isDescending: true });
const [widgetFilterProperties, setWidgetFilterProperties] = useState<
Array<IFilter<IWidget>>
>([]);
const [peopleSortProperty, setPeopleSortProperty] = useState<
ISorter<IPerson>
>({ property: "firstName", isDescending: true });
const [peopleFilterProperties, setPeopleFilterProperties] = useState<
Array<IFilter<IPerson>>
>([]);
const buttonText = showPeople ? "Show widgets" : "Show people";
return (
<>
<button
className="btn btn-primary"
onClick={() => setShowPeople(!showPeople)}
>
{buttonText}
</button>
<SearchInput
setSearchQuery={(query) => {
setQuery(query);
}}
/>
{!showPeople && (
<>
<h2>Widgets:</h2>
<Sorters
setProperty={(propertyType) => {
setWidgetSortProperty(propertyType);
}}
object={widgets[0]}
/>
<br />
<Filters
object={widgets[0]}
properties={widgetFilterProperties}
onChangeFilter={(property) => {
const propertyMatch = widgetFilterProperties.some(
(widgetFilterProperty) =>
widgetFilterProperty.property === property.property
);
const fullMatch = widgetFilterProperties.some(
(widgetFilterProperty) =>
widgetFilterProperty.property === property.property &&
widgetFilterProperty.isTruthySelected ===
property.isTruthySelected
);
if (fullMatch) {
setWidgetFilterProperties(
widgetFilterProperties.filter(
(widgetFilterProperty) =>
widgetFilterProperty.property !== property.property
)
);
} else if (propertyMatch) {
setWidgetFilterProperties([
...widgetFilterProperties.filter(
(widgetFilterProperty) =>
widgetFilterProperty.property !== property.property
),
property,
]);
} else {
setWidgetFilterProperties([
...widgetFilterProperties,
property,
]);
}
}}
/>
{widgets
.filter((widget) =>
genericSearch(widget, ["title", "description"], query, false)
)
.filter((widget) => genericFilter(widget, widgetFilterProperties))
.sort((a, b) => genericSort(a, b, widgetSortProperty))
.map((widget) => {
return <WidgetRenderer {...widget} />;
})}
</>
)}
{showPeople && (
<>
<h2>People:</h2>
<Sorters
setProperty={(propertyType) => {
setPeopleSortProperty(propertyType);
}}
object={people[0]}
/>
<Filters
object={people[0]}
properties={peopleFilterProperties}
onChangeFilter={(property) => {
const propertyMatch = peopleFilterProperties.some(
(peopleFilterProperty) =>
peopleFilterProperty.property === property.property
);
const fullMatch = peopleFilterProperties.some(
(peopleFilterProperty) =>
peopleFilterProperty.property === property.property &&
peopleFilterProperty.isTruthySelected ===
property.isTruthySelected
);
if (fullMatch) {
setPeopleFilterProperties(
peopleFilterProperties.filter(
(peopleFilterProperty) =>
peopleFilterProperty.property !== property.property
)
);
} else if (propertyMatch) {
setPeopleFilterProperties([
...peopleFilterProperties.filter(
(peopleFilterProperty) =>
peopleFilterProperty.property !== property.property
),
property,
]);
} else {
setPeopleFilterProperties([
...peopleFilterProperties,
property,
]);
}}}
/>
{people
.filter((person) =>
genericSearch(
person,
["firstName", "lastName", "eyeColor"],
query,
false
)
)
.filter((person) => genericFilter(person, peopleFilterProperties))
.sort((a, b) => genericSort(a, b, peopleSortProperty))
.map((person) => {
return <PeopleRenderer {...person} />;
})}
</>
)}
</>
);
}
export default App;

In addition to being difficult to understand, there is another problem with the code we’ve written so far—at least with using the object prop for sorting and filtering. In our examples, widgets and people are static lists. But, when we load this data in an API, we can’t guarantee we’ll always have an entity within widget[0] and people[0]. Either could be an empty array, or depending on the API conventions, null or undefined entirely. We should design our client code so that it can gracefully handle any of these cases. It would be great if we could supply our data (whether widgets or people) as children of the SearchInput, Filters, or Sorters, then we can act on that data directly in those respective components instead of having to carry many stateful variables in the app component.

To do this, first we’ll add a new data prop to SearchInput:

TypeScript 3.3.4
export interface ISearchInputProps<T> {
data: Array<T>;
setSearchQuery: (searchQuery: string) => void;
}

Move rendering logic into its own prop

Once the new data prop is added, we can move the render logic directly into its own prop. Let’s call the prop renderItem. This process is very similar to the pattern used for FlatList or SectionList in the built-in React Native components. This new prop is a function, taking a single item parameter of type T and returning a ReactNode.

TypeScript 3.3.4
export interface ISearchInputProps<T> {
data: Array<T>;
renderItem: (item: T) => ReactNode;
setSearchQuery: (searchQuery: string) => void;
}

Encapsulating state variables back into SearchInput

Finally, since we have our data and rendering logic encapsulated in these new props, there’s no need to have a setSearchQuery callback as a prop anymore. We can move that to SearchInput's own state variable.

TypeScript 3.3.4
export interface ISearchInputProps<T> {
data: Array<T>;
renderItem: (item: T) => ReactNode;
}
export function SearchInput(props: ISearchInputProps) {
// ...
const [searchQuery, setSearchQuery] = useState("");
// ...
}

With our props and state variables refactored, we can refactor the SearchInput component like this:

TypeScript 3.3.4
export interface ISearchInputProps<T> {
data: Array<T> | undefined;
renderItem: (item: T) => ReactNode;
}
export function SearchInput<T>(
props: ISearchInputProps<T>
) {
const { data, renderItem } = props;
const [searchQuery, setSearchQuery] = useState("");
const setSearchQueryDebounced = useDebounce((event) => {
setSearchQuery(event.target.value);
}, 250);
return (
<>
<label htmlFor="search" className="mt-3">
Search - try me
</label>
<input
id="search"
className="form-control full-width"
type="search"
placeholder="Search..."
aria-label="Search"
onChange={setSearchQueryDebounced}
/>
{data &&
data
.filter((item) =>
genericSearch(item, ["title", "description"], searchQuery, false)
)
.map(renderItem)}
</>
);
}

Final optimizations for SearchInput

If we examine the hardcoded values that remain in the function, we’ll see a few more optimization opportunities: We’ve provided the initial state of the search query as any empty string (""). But for some use cases (like after routing to a page), we could provide an initial search query to search with. Let’s add this string as a new prop called initialSearchQuery. We copied the keys to search with over from the old implementation in App.tsx, but these should also be configurable. Let’s add them as a new prop called searchKeys. Our revised ISearchInputProps looks like this:

TypeScript 3.3.4
export interface ISearchInputProps<T> {
data: Array<T> | undefined;
renderItem: (item: T) => ReactNode;
initialSearchQuery: string;
searchKeys: Array<keyof T>;
}

Now, when we use SearchInput (with the widgets data array for example), it looks like this:

TypeScript 3.3.4
<SearchInput
data={widgets}
renderItem={(item) => <WidgetRenderer {...item}/>}
initialSearchQuery={""}
searchKeys={["title", "description"]}
/>

One benefit of using our new props interface typing is that it will warn us if we use an incorrect renderer. In this example, simply by passing {widgets} as our data prop, TypeScript immediately infers that item in the renderItem function must be of type IWidget. Therefore, if we try to use the PersonRenderer component, for example, we’ll see type errors before we even run our code.

Let’s bring in the widgets and people data, types, and renderers to see how this new code looks and runs.

import * as React from "react";
import { ReactNode, useState } from "react";
import { useDebounce } from "../hooks/useDebounce";
import { genericSearch } from "../utils/genericSearch";

export interface ISearchInputProps<T> {
  data: Array<T> | undefined;
  renderItem: (item: T) => ReactNode;
  initialSearchQuery: string;
  searchKeys: Array<keyof T>;
}

export function SearchInput<T>(props: ISearchInputProps<T>) {
  const { data, renderItem, initialSearchQuery, searchKeys } = props;
  const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
  const setSearchQueryDebounced = useDebounce((event) => {
    setSearchQuery(event.target.value);
  }, 250);

  return (
    <>
      <label htmlFor="search" className="mt-3">
        Search 
      </label>
      <input
        id="search"
        className="form-control full-width"
        type="search"
        placeholder="Search..."
        aria-label="Search"
        onChange={(event) => {
          event.persist()
          setSearchQueryDebounced(event)
        }}
      />
      {data &&
        data
          .filter((item) => genericSearch(item, searchKeys, searchQuery, false))
          .map(renderItem)}
    </>
  );
}
Using our refactored SearchInput component to render and search widgets and people data

The disadvantages of render props

For a single functionality, whether it’s searching, sorting, or filtering, the render props pattern works great. We’ve seen how clean the code can be. Unfortunately, this pattern has the disadvantage of not being able to combine search, sort, or filter functionalities. At best, we can pass our array of data in the data prop and do manipulations on that data inside the implementation of our component. Before we address this issue, we’ll go through both the Sorters and Filters components to show how they can also leverage render props. Then we’ll take a step back and consider ways to combine the search, sort, and filter functionalities into one organizing generic component.