typescript: using generics in jsx

2021-08-12

 | 

~3 min read

 | 

432 words

I’ve been working on mastering generics for a few years at this point. While I’m making progress, it’s certainly still a work in progress.

This week, I had the opportunity to mark another milestone: learning how to use generics in my JSX.

Imagine a presentational component that’s agnostic to the type of data it receives - but which requires strong typing. This is a perfect use case for generics, but how do you actually declare the type?

Let’s walk through an example. We’ll use a component called <RemoteData/>. It takes the results from a fetch request and then presents them. The benefit of the <RemoteData/> component, however, is that it will have some sane defaults and ensure that we’re always handling the different cases of not started, loading, error, and success.

The end result will be an API like so:

app.tsx
type Posts {/*...*/}

export function App(){
    /*...*/
    return(
        <RemoteData<Posts[]>
            remoteData={posts}
            success={postsSuccess}
        />
    )
}

Note the <RemoteData<Posts[]>>. That’s it. That’s how we declare that Posts[] will be our generic.

On the component side, what does this look like?

RemoteData.tsx
import * as React from "react"
import { Spinner } from "./components"

enum RemoteDataState {
  NotStarted = "Not Started",
  Loading = "Loading",
  Success = "Success",
  Fail = "Fail",
}

type RD<T, E = string> =
  | { state: RemoteDataState.NotStarted }
  | { state: RemoteDataState.Loading }
  | { state: RemoteDataState.Success; data: T }
  | { state: RemoteDataState.Fail; message: E }

interface IRemoteDataProps<T, E> {
  remoteData: RD<T, E>
  notStarted?: () => JSX.Element
  loading?: () => JSX.Element
  success: (data: T) => JSX.Element
  fail?: (message: E) => JSX.Element
}

const noop = () => {
  return null
}

function defaultLoad() {
  return (
    <>
      <Spinner fullWidth />
      <div>Loading...</div>
    </>
  )
}

function defaultFail<E>(message: E) {
  return (
    <>
      <p>{message ?? "Something went wrong. Please try again."}</p>
    </>
  )
}

export const RemoteData = <T, E = string>(props: IRemoteDataProps<T, E>) => {
  const { notStarted, loading, success, fail, remoteData } = props
  switch (remoteData.state) {
    case RemoteDataState.NotStarted:
      return (notStarted ?? noop)()
    case RemoteDataState.Loading:
      return (loading ?? defaultLoad)()
    case RemoteDataState.Success:
      return success(remoteData.data)
    case RemoteDataState.Fail:
      return (fail ?? defaultFail)(remoteData.message)
  }
}

So, we could have defined a second generic (for the error type), but it is defaulted to string. We allow the defaults for notStarted, loading, and fail to be used by not supplying any callback that returns a JSX Element. success on the other hand, is defined (though not shown here).

Just like that, we have defined a functional component that takes a generic and then called it within our app - without relying on any!


Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!