Hooks and Render Props to replace HoCs in React

HOC = higher-order component

What md-admin does a lot

  1. Fetch some JSON from the backend passing a key from the search result
  2. If successful, format the JSON prettily
  3. Alternatively, display an error message

HoC 1

withObservableSubscription will maintain a subscription to a backend URL and provide payload and error props

const IntradayPricesImpl = ({ payload, error }) => {
    if (payload) {
        return <IntradayPricesTable prices={payload}/>
    }
    else if (error) {
        <UserMessage error={`Failed to load Interday Price data. <pre>${error.statusText}</pre>`}/>
    }
    else {
        return <div><Loader/></div>
    }
};

export const IntradayPrices = withObservableSubscription(
    ({ ric }, component) => (
        observeApi(`/api/intraday/${ric}`).subscribe(
            data => {
                component.setState({ payload: data, error: null })
            },
            error => {
                component.setState({ payload: null, error: error });
            }
        )
    ),
    (props, nextProps) => props.ric !== nextProps.ric // reload predicate
)(IntradayPricesImpl);

HoC 2

Just wrap withObservableSubscription and cut down the boilerplate

const IntradayPricesImpl = ({ payload, error }) => {
    if (payload) {
        return <IntradayPricesTable prices={payload}/>
    }
    else if (error) {
        return <UserMessage error={`Unable to load Intraday Price data. Reason: ${error.errorText}`}/>
    }
    else {
        return <div><Loader/></div>
    }
};

export const IntradayPrices = apiPage(({ric}) => `/api/intraday/${ric}`)(IntradayPricesImpl);

HoC 3

Pull showing the loader or error message into the HoC, only call the wrapped component on success

const IntradayPricesImpl = ({payload}) => (
    <IntradayPricesTable prices={payload}/>
);

export const IntradayPrices = apiPage(
    ({ric}) => `/api/intraday/${ric}`,
    "Unable to load Intraday Price data"
)(IntradayPricesImpl);

Render prop

No more HOC wrapping - just call an ApiPage component. Pass a function as its content that will produce more content to be spliced in.

export const IntradayPrices = ({ric}) => (
    <ApiPage url={`/api/intraday/${ric}`} errorMessage="Unable to load Intraday Price data">
        {prices => <IntradayPricesTable prices={prices} />}
    </ApiPage>
);

ApiPage implementation using hook

Here useApiData is a custom hook that will provide the payload or error for the backend URL. This component effectively replaces the previous HOC's render method.

export const ApiPage = ({ url, errorMessage = "Error loading data", children }) => {
    const errorMessageFunc = typeof errorMessage === "function" ? errorMessage : (error) => `${errorMessage} Reason: ${error}`;
    const [error, data] = useApiData(url);

    if (data) {
        return children(data);
    }
    else if (error) {
        return <UserMessage error={errorMessageFunc(error)}/>
    }
    else {
        return <div> <Loader/> </div>;
    }
};

Defining the custom hook

The custom hook function useApiData is just a function that composes calls to other hook functions. useState and useEffect are basic hooks provided by React.

export function useApiData(url) {
    const [state, setState] = useState([null, null]); // default state
    useEffect(() => {
        const subscription = ajax.get(url).subscribe({
            response => setState([null, response.data]),
            err => setState([err, null])
        });
        return () => { // cleanup
            setState([null, null]);
            subscription.unsubscribe();
        }
    }, [url]); // reload predicate
    return state;
}

Avoiding prop propagation

The HOC implementation had to blindly forward all props to the wrapped component just to allow outer props to be used when rendering. With a render prop, this isn't necessary.

export const DataScopeFileCodePage = ({match: {params: {fileCode}}}) => (
    <ApiPage url={`/api/datascope/${fileCode}`} errorMessage="Failed to get file code data">
        {instrumentData =>
            <DatascopeFileCodeDisplay
				fileCode={fileCode}
				instrumentData={instrumentData}/>}
    </ApiPage>
);

Here the destructuring had to be either duplicated, or extracted out to an upper-level component, wasting space in the VDOM