DEV Community

A. Sharif
A. Sharif

Posted on • Edited on

Notes on TypeScript: Render Props

Introduction

These notes should help in better understanding TypeScript and might be helpful when needing to lookup up how to leverage TypeScript in a specific situation. All examples are based on TypeScript 3.2.

More Notes on TypeScript

Notes on TypeScript: Pick, Exclude and Higher Order Components

Render Props

Render Props is a popular pattern for enhancing a React Component with additional functionality. It can be interchanged with a higher order component, and choosing the render props pattern or a higher order component is a matter of flavour and depends on the specific use case.

To get a better understanding of the topic, let's build a component that uses a render prop. In the previous "Notes on TypeScript" we built a component that provided an Input component with onChange and value properties.
We can rewrite this higher order component to a render prop implementation.

class OnChange extends React.Component {
  state = {
    value: this.props.initialValue
  };
  onChange = event => {
    const target = event.target;
    const value = target.type === "checkbox" ? target.checked : target.value;
    this.setState({ value });
  };
  render() {
    return this.props.render({
      value: this.state.value,
      onChange: this.onChange
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Using the refactored OnChange inside your React application:

<OnChange
  initialValue="hello"
  render={onChangeProps => <Input {...props} {...onChangeProps} />}
/>
Enter fullscreen mode Exit fullscreen mode

We can reuse most of the previously defined types.

type InputProps = {
  name: string,
  type: string
};

type OnChangeProps = {
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
  value: string
};

type ExpandedOnChangeProps = {
  initialValue: string | boolean,
  render: (onChangeProps: onChangeProps) => JSX.Element
};

type OnChangeState = {
  value: string
};
Enter fullscreen mode Exit fullscreen mode

Our Input component has not changed, we can also reuse that component for this example.

const Input = ({ value, onChange, type, name }: InputProps & OnChangeProps) => (
  <input type={type} name={name} value={value} onChange={onChange} />
);
Enter fullscreen mode Exit fullscreen mode

So now that we have everything in place, let's see how OnChange would be typed.
Interestingly, there's not very much we need to do, to type the onChange component.

class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState> {
  state = {
    value: this.props.initialValue
  };
  onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const target = event.target;
    this.setState({ value: target.value });
  };
  render() {
    return this.props.render({
      value: this.state.value,
      onChange: this.onChange
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Compared to the higher order component implementation, we only need to define props and state for OnChange, in this case using the already existing ExpandedOnChangeProps and OnChangeState and defining the class component as follows:

class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState>.

We might want to reuse the functionality in multiple places inside our application. By defining a new component, f.e. ControlledInput, we can combine our OnChange and Input and let developers define the initialValue as well as name and type.

type ControlledInputProps = InputProps & { initialValue: string };

const ControlledInput = ({ initialValue, ...props }: ControlledInputProps) => (
  <OnChange
    initialValue={initialValue}
    render={onChangeProps => <Input {...props} {...onChangeProps} />}
  />
);
Enter fullscreen mode Exit fullscreen mode

Now ControlledInput can be used inside another component and TypeScript will complain when either name, type or initialValue is missing.

<ControlledInput initialValue="testdrive" type="text" name="test" />
Enter fullscreen mode Exit fullscreen mode

Advanced

We might want to enable to either pass the render callback via render or children prop. This requires us to make some changes to our OnChange component. If we recall, our ExpandedOnChangeProps has the following shape:

type ExpandedOnChangeProps = {
  initialValue: string | boolean,
  render: (onChangeProps: onChangeProps) => JSX.Element
};
Enter fullscreen mode Exit fullscreen mode

One way to enable passing callbacks as children prop is to change the definition to the following:

type ExpandedOnChangeProps = {
  initialValue: string,
  render?: (onChangeProps: onChangeProps) => JSX.Element,
  children?: (onChangeProps: onChangeProps) => JSX.Element
};
Enter fullscreen mode Exit fullscreen mode

But the above definition has problems, as both or none of the variants could be provided now. What we actually want is to ensure that one of these properties is defined, which is possible defining an explicit RenderProp type:

type RenderProp =
  | { render: (onChangeProps: OnChangeProps) => JSX.Element }
  | { children: (onChangeProps: OnChangeProps) => JSX.Element };
Enter fullscreen mode Exit fullscreen mode

Which means we can rewrite our ExpandedOnChangeProps definition to:

type ExpandedOnChangeProps = {
  initialValue: string
} & RenderProp;
Enter fullscreen mode Exit fullscreen mode

Finally we need to update the render function to handle both possible cases:

class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState> {
  state = {
    value: this.props.initialValue
  };
  onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const target = event.target;
    this.setState({ value: target.value });
  };
  render() {
    if ("render" in this.props) {
      return this.props.render({
        value: this.state.value,
        onChange: this.onChange
      });
    }

    if ("children" in this.props) {
      return this.props.children({
        value: this.state.value,
        onChange: this.onChange
      });
    }

    throw new Error("A children or render prop has to be defined");
  }
}
Enter fullscreen mode Exit fullscreen mode

By using "render" in this.props, we can check if render is defined else check if a children property is defined. In case neither properties are defined we throw an error.
Our previously defined ControlledInput could be rewritten to:

const ControlledInput = ({
  initialValue,
  ...props
}: InputProps & { initialValue: string }) => (
  <OnChange initialValue={initialValue}>
    {onChangeProps => <Input {...props} {...onChangeProps} />}
  </OnChange>
);
Enter fullscreen mode Exit fullscreen mode

We should have a basic understanding of how render props can be typed with TypeScript now.

If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif

Top comments (2)

Collapse
 
dance2die profile image
Sung M. Kim

Thanks for the article A. Sharif.

It seems like this is a part of a series.
Would you be able to mark series posts in the meta data so it's easy to navigate?

series

Collapse
 
busypeoples profile image
A. Sharif

Thanks for the info Sung Kim!