HOC = higher-order component
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);
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);
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);
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>
);
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>;
}
};
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;
}
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