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} />;
}
}