Intro
Today’s post will sort of continuation of previous one - useFetchData hook - which allowed us to make a http request with cancellation and various states (if you would like to remind or get familiar here’s the link). Those states were ongoing request flag (isFetching), error - message or null if there is no error and we can have information about if there is no data by checking if data property is not null or by checking the type as FetchReturnType is generic allowing us to type the response.
Our new shiny toy would be Higher Order Component - withFetchResult. We will use it to handle all those states by early returning some components. In fact it will utilise 2 design patterns - Higher Order Component and Switch Component. Let’s get to it.
Higher Order Components
For sure you know (or don’t realise that you know) higher order functions - functions that take as a parameter another function. HOC’s are very similar - there are functions that accept component (components are just functions) as a parameter! As a result they can either intercept props or wrap input component with some UI. They don’t modify internal component rather build around it.
This comes with lots of benefits as we can encapsulate repetitive logic handling into such function and extend existing components with additional features without modifing them. Also it fits nicely into React’s composition over inheritance approach.
Ofcourse there are some cons - it is not the easiest thing to type, makes code a little bit harder to read and understand and the first glance of an eye. Also there is some overhead with refs forwarding and it makes testing and debugging more difficult.
It seems that it has more downsides than positives but as with any other tool - if you use it wisely it can bring you a lot of benefits and satisfaction.
HOC’s vs hooks
Sounds similar, both allows to encapsulate and reuse some logic but whats the difference actually?
Firstly, HOC’s are mainly used to add some UI around your component, eventually return another one. Hooks cannot return any UI.
HOC’s “work” around component, they don’t interfere with internal logic. Hooks are pieces of internal logic, used inside of components.
Finally HOC’s as a pattern can be used with both functional and class-based (yes, that was a thing some time ago) components but hooks are exclusive to functional ones.
Switch Component
This is another fancy name for pattern that for sure you used before. Have you ever early returned UI based on some flag or state? That’s it.
The Code.
As a responsive and mature developers, let’s start from forming our requriements. We would like to:
- Display loader to indicate user that request is pending,
- Show error message if something goes wrong,
- Show prompt if there is no returned data
- Render actual data
- Having a possibility to display custom components handling all this states instead of default ones
- Have it reusable
- and fully typed
Seems a lot but soon enough you will realize that it’s pretty simple, let’s roll!
Firstly, lets create a generic component component that will accept properties of our utility type as props. We will start this with props typing
1// utility type definition as a reminder
2export interface FetchReturnType<Data> {
3 data: Data | null;
4 isFetching: boolean;
5 error: string | null;
6}
7
8// our component props typing as an extending interface - why so, we will get to it in a minute
9interface FetchResultComponentProps<Data> extends FetchReturnType<Data>{}
Here's how our generic component might look, displaying data as pre-formatted text:
1function FetchResultComponent<Data>({ data }: FetchResultComponentProps<Data>) {
2 return <pre>{JSON.stringify(data, null, 2)}</pre>;
3}
However, our goal is to handle various states, so we'll employ the Switch Component pattern to return different elements based on the provided state. For simplicity's sake:
1function FetchResultComponent<Data>({ data, isFetching, error }: FetchResultComponentProps<Data>) {
2 if (isFetching) {
3 return <div>Loading...</div>;
4 }
5 if (error) {
6 return <div>Error</div>;
7 }
8 if (!data) {
9 return <div>No results :(</div>;
10 }
11 return <pre>{JSON.stringify(data, null, 2)}</pre>;
12}
With this, we've covered the core functionality: displaying data and handling all states. Points 1-4: check!
Now, let's enhance the developer experience by allowing to render different components for each state.
We'll extract dummy default components and add optional props for Loader, Error, and NoResults components to the FetchResultComponentProps interface. We'll also set default values to our extracted components:
1function Loader() {
2 return <div>Loading...</div>;
3}
4
5// A simple component to display errors
6function Error({ error }: { error: string }) {
7 return <div>{error}</div>;
8}
9
10function NoData() {
11 return <div>No data :(</div>;
12}
13
14interface FetchResultComponentProps<Data> extends FetchReturnType<Data> {
15 LoaderComponent?: React.FunctionComponent;
16 ErrorComponent?: React.FunctionComponent<{ error: string }>;
17 NoResultsComponent?: React.FunctionComponent;
18}
19
20function FetchResultComponent<Data>({
21 data,
22 isFetching,
23 error,
24 LoaderComponent = Loader,
25 ErrorComponent = Error,
26 NoResultsComponent = NoData,
27}: FetchResultComponentProps<Data>) {
28 if (isFetching) {
29 return <LoaderComponent />;
30 }
31 if (error) {
32 return <ErrorComponent error={error} />;
33 }
34 if (!data) {
35 return <NoResultsComponent />;
36 }
37 return <pre>{JSON.stringify(data, null, 2)}</pre>;
38}
This is shaping up nicely! We've made the component flexible and customisable.
Now, let's move to our HOC. The HOC will encapsulate all state handling and accept a data-displaying component as a parameter, like so:
1const ResultComponent = withFetchResult(DataDisplayingComponent);
Here's how we define withFetchResult:
1function withFetchResult<DataDisplayingComponentProps extends object, Data>(
2 DataDisplayingComponent: React.FunctionComponent<DataDisplayingComponentProps>
3): React.FunctionComponent<FetchResultComponentProps<Data> & WithFetchResultProps<Data>> {
4 return function InnerComponent({
5 isFetching,
6 error,
7 data,
8 LoaderComponent = Loader,
9 ErrorComponent = Error,
10 NoResultsComponent = NoData,
11 ...props
12 }) {
13 if (isFetching) {
14 return <LoaderComponent />;
15 }
16 if (error) {
17 return <ErrorComponent error={error} />;
18 }
19 if (!data) {
20 return <NoResultsComponent />;
21 }
22 return (
23 <DataDisplayingComponent
24 data={data}
25 {...(props as DataDisplayingComponentProps)}
26 />
27 );
28 };
29}
Finally, here's how to use our HOC:
1// Example data interface
2type FetchedData = Array<{
3 displayName: string;
4 email: string;
5 isVerified: boolean;
6}>;
7
8interface DataDisplayingComponentProps extends FetchReturnType<FetchedData> {
9 data: FetchedData;
10 variant: "primary" | "secondary";
11};
12
13function DataDisplayingComponent({ data, variant }: DataDisplayingComponentProps) {
14 // Rendering logic goes here
15}
16
17export const ComponentWithFetchedData = withFetchResult(DataDisplayingComponent);
18
19function DataContainer() {
20 const { data, isFetching, error } = useFetchData('some endpoint url');
21
22 return (
23 <ComponentWithFetchedData
24 variant="primary"
25 data={data}
26 isFetching={isFetching}
27 error={error}
28 />
29 );
30}
That's a wrap! We've created a versatile HOC that can handle various states and is customisable, offering a great developer experience.