typescript and react types

It can be confusing to move from javascript to typescript if you are not used to types.

If you want to use more advanced react component creation techniques, it can be even more confusing.

Passing Down “Created” Elements

If you pass in elements, such as a message to display when there is no data, you need to pass in either a react element or someting you pass to JSX/createElement (see Pass in Elements for Your Component).

If you already have an element that you created in the parent element, then you have an element you want to pass. You may just want to pass in something that is renderable. Should you pass in a RectElement or something more general that is renderable? What type should you use?

If you think of the element you want to pass like children, then you can find the definition for what is passed is children by looking at some of the other definitoions. You’ll find ReactNode[].

 //
    // React Nodes
    // http://facebook.github.io/react/docs/glossary.html
    // ----------------------------------------------------------------------

    type ReactText = string | number;
    type ReactChild = ReactElement<any> | ReactText;

    // Should be Array<ReactNode> but type aliases cannot be recursive
    type ReactFragment = {} | Array<ReactChild | any[] | boolean>;
    type ReactNode = ReactChild | ReactFragment | boolean | null | undefined;

Which suggests that a ReactNode is really anything that can be rendered including a ReactChild, which is in fact, a ReactElement. A ReactElement is the output of React.createElement. It can be difficult to find this in the react type definitions because there are several overloads.

So, we should really do:

interface Props {
   noDataMessage: ReactNode
   hasData: boolean
}

function Data(props: Props) {
  return(
    <div>
      ...
      { props.hasData ? 
         (props.noDataMessage || "No data"):
         <Data />
      }
      ...
    </div>
  )
}

Breaking Down Component Creation

You probably break down your component creation or have component-creating-functions you use to create the UI. Something like:

function createDiv(arg: string) {
  return(<div>{arg}</div>)
}

What should the return type be?

This depends on what you want it to be. You are clearly returning a react element so you could use React.Element. There is conveniently named class called JSX.Element that you could use:

namespace JSX {
  interface Element extends React.ReactElement<any> { }
}

So this makes sense:

function createDiv(arg: string): JSX.Element { ...}

But you may also return other things that are renderable. A react element is really a data structure that has a type (string, component class or function), props and a key. If you creating a div as above and know for a fact that you are returning an element, then JSX.Element is applicable.

If your function could return null, then you might be tempted to do JSX.Element | null, which makes sense. But this is saying that your function could return null. If that’s possible for your specific function, you can use this signature. For example, a pure renderable function (presentational component) has a type SFC or StatelessComponent in react. It’s return type is ReactElement<any> | null for exactly that reason.

Generally, a function creating components is going to create one of a few types. A react element, maybe a string, maybe null, maybe an array. If your function allows you to be more specific do it e.g. Array<JSX.Element>.

The most general return value that is safe to use is ReactNode, which includes strings, elements, arrays of strings/elements, etc. That is, a ReactNode is something that react can render.

function createDiv(arg: string): ReactNode { ... }

With ReactNode, the only real decision is whether you have an array of ReactNode’s or a single renderable. Remember, a ReactNode could be an array of JSX.Element’s so depending on your desired function signature and usage, you can return an array. If you create your array inside ReactNode, don’t forget the keys when you create those elements. If you choose to return Array<ReactNode> your caller will need to add the keys and will need to probably use cloneElement to add the key because the element was already created.

If you know for sure that and its important to your API that you have created a HTML element and are not returning just a string (a general react renderable) then you can be more specific

type DOMElement ...
type ReactHTMLElement ...

these extends ReactElement with prop types that are more specific and include HTML attributes. In addition, the “type” value as output from createElement is restricted to a HTML string e.g. “a” or “div”. In other words, if you are return a general, perhaps custom element you need ReactElement. If you are returing a HTML specific element and its important to your API, you can use a DOM or ReactHTMLElement.

If your component has data in a well known format then you may want to provide an option to render the data. In this case, you need a signature that takes some properties and returns an element to display. As described above, you could define:

export interface Renderer<P> {
  (props?: P) => JSX.Element | null 
}

Pass in Elements for Your Component

Let’s say you want to make your component pluggable by passing in the specific element to use in your component. Traditionally this has been done with strings:

interface Component {
   el?: string
   props?: Record<string, any>
   style?: Record<string, any>
}

const DefaultComponent: Component = { el: "div" }

interface Props {
  component?: Component
  className?: string
  ...
}

function foo(props: Props) {
  // ... create a Component using DefaultComponent e.g. ramda.deepMergeRight()
  const cprops = ...
  const TopLevel = cprops.component.el
  return(<TopLevel />) // the JSX parser cannot parse a name with dots in it...
}

But the JSX notation, and hence React.createComponent, can take a string, a class or a function: React.createElement(component, props, ...children). In this API, component can be a string, class or function (pure render).

What type do we use for el above if we can take these three types?

Looking through node_modules/@types/react/index.d.ts suggests:

 type ReactType = string | ComponentType<any>;
    type ComponentType<P = {}> = ComponentClass<P> | StatelessComponent<P>;

so our declaration should be:

interface Component {
   el?: React.ReactType
}

Comments

Popular posts from this blog

quick note on scala.js, react hooks, monix, auth

zio environment and modules pattern: zio, scala.js, react, query management

user experience, scala.js, cats-effect, IO