Skip to content

Commit

Permalink
replace class components (HOCs) with React Hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
proddy committed Sep 20, 2021
1 parent 09d8a63 commit a31cf53
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 69 deletions.
12 changes: 6 additions & 6 deletions interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.11.4",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@msgpack/msgpack": "^2.7.0",
"@types/lodash": "^4.14.168",
"@types/node": "^15.0.1",
"@types/react": "^17.0.4",
"@types/react-dom": "^17.0.3",
"@types/lodash": "^4.14.172",
"@types/node": "^12.20.20",
"@types/react": "^17.0.19",
"@types/react-dom": "^17.0.9",
"@types/react-material-ui-form-validator": "^2.1.0",
"@types/react-router": "^5.1.13",
"@types/react-router-dom": "^5.1.7",
Expand All @@ -30,7 +30,7 @@
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"sockette": "^2.0.6",
"typescript": "4.2.4",
"typescript": "4.3.5",
"zlib": "^1.0.5"
},
"scripts": {
Expand Down
22 changes: 5 additions & 17 deletions interface/src/authentication/UnauthenticatedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,13 @@ interface UnauthenticatedRouteProps
| React.ComponentType<any>;
}

type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;

class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
public render() {
const {
authenticationContext,
component: Component,
features,
...rest
} = this.props;
const renderComponent: RenderComponent = (props) => {
if (authenticationContext.me) {
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
}
if (Component) {
return <Component {...props} />;
}
};
return <Route {...rest} render={renderComponent} />;
const { authenticationContext, features, ...rest } = this.props;
if (authenticationContext.me) {
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
}
return <Route {...rest} />;
}
}

Expand Down
56 changes: 56 additions & 0 deletions interface/src/components/FormLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { FC } from 'react';

import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
import { Button, LinearProgress, Typography } from '@material-ui/core';

const useStyles = makeStyles((theme: Theme) =>
createStyles({
loadingSettings: {
margin: theme.spacing(0.5)
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: 'center'
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2)
}
})
);

interface FormLoaderProps {
errorMessage?: string;
loadData: () => void;
}

const FormLoader: FC<FormLoaderProps> = ({ errorMessage, loadData }) => {
const classes = useStyles();
if (errorMessage) {
return (
<div className={classes.loadingSettings}>
<Typography variant="h6" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button
variant="contained"
color="secondary"
className={classes.button}
onClick={loadData}
>
Retry
</Button>
</div>
);
}
return (
<div className={classes.loadingSettings}>
<LinearProgress className={classes.loadingSettingsDetails} />
<Typography variant="h6" className={classes.loadingSettingsDetails}>
Loading&hellip;
</Typography>
</div>
);
};

export default FormLoader;
1 change: 1 addition & 0 deletions interface/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { default as HighlightAvatar } from './HighlightAvatar';
export { default as MenuAppBar } from './MenuAppBar';
export { default as PasswordValidator } from './PasswordValidator';
export { default as RestFormLoader } from './RestFormLoader';
export { default as FormLoader } from './FormLoader';
export { default as SectionContent } from './SectionContent';
export { default as WebSocketFormLoader } from './WebSocketFormLoader';
export { default as ErrorButton } from './ErrorButton';
Expand Down
65 changes: 19 additions & 46 deletions interface/src/features/FeaturesWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,31 @@
import { Component } from 'react';
import { FC } from 'react';

import { Features } from './types';
import { FeaturesContext } from './FeaturesContext';
import FullScreenLoading from '../components/FullScreenLoading';
import ApplicationError from '../components/ApplicationError';
import { FEATURES_ENDPOINT } from '../api';
import { useRest } from '../hooks';

interface FeaturesWrapperState {
features?: Features;
error?: string;
}
import { Features } from './types';
import { FeaturesContext } from './FeaturesContext';

class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
state: FeaturesWrapperState = {};
const FeaturesWrapper: FC = ({ children }) => {
const { data: features, errorMessage: error } = useRest<Features>({
endpoint: FEATURES_ENDPOINT
});

componentDidMount() {
this.fetchFeaturesDetails();
if (features) {
return (
<FeaturesContext.Provider value={{ features }}>
{children}
</FeaturesContext.Provider>
);
}

fetchFeaturesDetails = () => {
fetch(FEATURES_ENDPOINT)
.then((response) => {
if (response.status === 200) {
return response.json();
} else {
throw Error('Unexpected status code: ' + response.status);
}
})
.then((features) => {
this.setState({ features });
})
.catch((error) => {
this.setState({ error: error.message });
});
};

render() {
const { features, error } = this.state;
if (features) {
return (
<FeaturesContext.Provider
value={{
features
}}
>
{this.props.children}
</FeaturesContext.Provider>
);
}
if (error) {
return <ApplicationError error={error} />;
}
return <FullScreenLoading />;
if (error) {
return <ApplicationError error={error} />;
}
}

return <FullScreenLoading />;
};

export default FeaturesWrapper;
2 changes: 2 additions & 0 deletions interface/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as useRest } from './useRest';
export { default as useAuthorizedRest } from './useAuthorizedRest';
12 changes: 12 additions & 0 deletions interface/src/hooks/useAuthorizedRest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { redirectingAuthorizedFetch } from '../authentication';
import useRest, { RestRequestOptions } from './useRest';

const useAuthorizedRest = <D>({
endpoint
}: Omit<RestRequestOptions, 'fetchFunction'>) =>
useRest<D>({
endpoint,
fetchFunction: redirectingAuthorizedFetch
});

export default useAuthorizedRest;
79 changes: 79 additions & 0 deletions interface/src/hooks/useRest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useCallback, useEffect, useState } from 'react';
import { useSnackbar } from 'notistack';

export interface RestRequestOptions {
endpoint: string;
fetchFunction?: typeof fetch;
}

const useRest = <D>({
endpoint,
fetchFunction = fetch
}: RestRequestOptions) => {
const { enqueueSnackbar } = useSnackbar();

const [saving, setSaving] = useState<boolean>(false);
const [data, setData] = useState<D>();
const [errorMessage, setErrorMessage] = useState<string>();

const handleError = useCallback(
(error: any) => {
const errorMessage = error.message || 'Unknown error';
enqueueSnackbar('Problem fetching: ' + errorMessage, {
variant: 'error'
});
setErrorMessage(errorMessage);
},
[enqueueSnackbar]
);

const loadData = useCallback(async () => {
setData(undefined);
setErrorMessage(undefined);
try {
const response = await fetchFunction(endpoint);
if (response.status !== 200) {
throw Error('Invalid status code: ' + response.status);
}
setData(await response.json());
} catch (error) {
handleError(error);
}
}, [handleError, fetchFunction, endpoint]);

const save = useCallback(
async (data: D) => {
setSaving(true);
setErrorMessage(undefined);
try {
const response = await fetchFunction(endpoint, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
if (response.status !== 200) {
throw Error('Invalid status code: ' + response.status);
}
enqueueSnackbar('Update successful.', { variant: 'success' });
setData(await response.json());
} catch (error) {
handleError(error);
} finally {
setSaving(false);
}
},
[enqueueSnackbar, handleError, fetchFunction, endpoint]
);

const saveData = () => data && save(data);

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

return { loadData, saveData, saving, setData, data, errorMessage } as const;
};

export default useRest;
33 changes: 33 additions & 0 deletions interface/src/utils/binding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
type UpdateEntity<S> = (state: (prevState: Readonly<S>) => S) => void;

export const extractEventValue = (
event: React.ChangeEvent<HTMLInputElement>
) => {
switch (event.target.type) {
case 'number':
return event.target.valueAsNumber;
case 'checkbox':
return event.target.checked;
default:
return event.target.value;
}
};

export const updateValue = <S>(updateEntity: UpdateEntity<S>) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
updateEntity((prevState) => ({
...prevState,
[event.target.name]: extractEventValue(event)
}));
};

export const updateBooleanValue = <S>(updateEntity: UpdateEntity<S>) => (
name: string,
value?: boolean
) => {
updateEntity((prevState) => ({
...prevState,
[name]: value
}));
};
1 change: 1 addition & 0 deletions interface/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './binding';

0 comments on commit a31cf53

Please sign in to comment.