Home > Back-end >  How to use the React HOC pattern with functional components and Typescript
How to use the React HOC pattern with functional components and Typescript

Time:10-23

Is there a way to build the react docs example for HOCs with functional components?

Something which somehow would look like this:

import { ComponentType, useCallback, useEffect, useState } from 'react';

type Props = { wrapped: ComponentType<{ data: string | null }> };

const WithSubscription = ({ wrapped: Wrapped }: Props) => {
  const [subscriptionData, setSubscriptionData] = useState<null | string>(null);

  const subscribe = useCallback(() => {
    console.log('subscribing');
    setSubscriptionData('data from database');
  }, []);

  useEffect(() => {
    subscribe();
  }, [subscribe]);

  return <Wrapped data={subscriptionData} />;
};

export default WithSubscription

CodePudding user response:

Perhaps it's easier if you convert the existing example to TypeScript first and then modify it. You are trying to change up the example while also adding types. So let's address one thing at a time.

Here is the withSubscription HOC with proper types.

import React from 'react';

type ChangeListener = () => void; 

interface DataSourceType {
  addChangeListener: (listener: ChangeListener) => void;
  removeChangeListener: (listener: ChangeListener) => void;
  // and some other methods which might be used by our selectData
}

declare const DataSource: DataSourceType;

// This function takes a component...
function withSubscription<DataType, OtherProps extends Record<string, unknown>>(
  WrappedComponent: React.ComponentType<OtherProps & { data: DataType }>,
  selectData: (source: DataSourceType, props: OtherProps) => DataType
) {
  // ...and returns another component...
  return class extends React.Component<OtherProps, { data: DataType }> {
    constructor(props: OtherProps) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ... that takes care of the subscription...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... and renders the wrapped component with the fresh data!
      // Notice that we pass through any additional props
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

Note: I'm not using an as assertions here, but you might not get the best type inference when using the HOC. You may want to change up the generics and use Props extends { data: unknown } but then you will need to make an as assertion inside the HOC.

Now it's a lot easier to convert this to a function component, because most of the types stay the same.

For right now let's just ignore a glaringly obvious issue, which is using the entire props object as a dependency of the useEffect. I'm not even sure if that works?

But your HOC would become basically this:

import React, { useState, useEffect } from 'react';

type ChangeListener = () => void;

interface DataSourceType {
  addChangeListener: (listener: ChangeListener) => void;
  removeChangeListener: (listener: ChangeListener) => void;
  // and some other methods which might be used by our selectData
}

declare const DataSource: DataSourceType;

// This function takes a component...
function withSubscription<DataType, OtherProps extends Record<string, unknown>>(
  WrappedComponent: React.ComponentType<OtherProps & { data: DataType }>,
  selectData: (source: DataSourceType, props: OtherProps) => DataType
) {
  // ...and returns another component...
  return (props: OtherProps) => {
    const [data, setData] = useState(selectData(DataSource, props));

    useEffect(() => {
      const handleChange = () => {
        setData(selectData(DataSource, props));
      }
      DataSource.addChangeListener(handleChange);

      return () => {
        DataSource.removeChangeListener(handleChange);
      }
    }, [selectData, props]);

    // ... and renders the wrapped component with the fresh data!
    // Notice that we pass through any additional props
    return <WrappedComponent data={data} {...props} />;
  }
}

There are ways to get around the dependency on props by using refs, but it looks like your subscription does not use the props of the component. So it that case it's easy as we just drop that argument from the selectData function.

import React, { useState, useEffect } from 'react';

type ChangeListener = () => void;

interface DataSourceType {
  addChangeListener: (listener: ChangeListener) => void;
  removeChangeListener: (listener: ChangeListener) => void;
  // and some other methods which might be used by our selectData
}

declare const DataSource: DataSourceType;

// This function takes a component...
function withSubscription<DataType, OtherProps extends Record<string, unknown>>(
  WrappedComponent: React.ComponentType<OtherProps & { data: DataType }>,
  selectData: (source: DataSourceType) => DataType
) {
  // ...and returns another component...
  return (props: OtherProps) => {
    const [data, setData] = useState(selectData(DataSource));

    useEffect(() => {
      const handleChange = () => {
        setData(selectData(DataSource));
      }
      DataSource.addChangeListener(handleChange);

      return () => {
        DataSource.removeChangeListener(handleChange);
      }
    }, [selectData]);

    // ... and renders the wrapped component with the fresh data!
    // Notice that we pass through any additional props
    return <WrappedComponent data={data} {...props} />;
  }
}
  • Related