Skip to content

Commit

Permalink
Refactored CSR Example React Code
Browse files Browse the repository at this point in the history
Extracted components from the huge CSR Page to make it more readable.

Had to fix React-Query indicator due to the Cache Hook not updating.
  • Loading branch information
JonParton committed Jul 27, 2020
1 parent 75d57c3 commit cdebdbc
Show file tree
Hide file tree
Showing 5 changed files with 449 additions and 356 deletions.
185 changes: 185 additions & 0 deletions components/ManualDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import React from 'react'
import {
Typography,
makeStyles,
Box,
CardContent,
Theme,
Card,
CardMedia,
} from '@material-ui/core'
import { Skeleton, Alert } from '@material-ui/lab'
import { useRecoilValue, useSetRecoilState } from 'recoil'
import { currentManualNameState, currentPageTitleState } from '../state/atoms'
import { usePersonManual } from '../state/ReactQueryHooks'
import { ReactQueryStatusLabel } from './ReactQueryStatusLabel'

const useStyles = makeStyles((theme: Theme) => ({
errorStyle: {
color: 'red',
fontWeight: 'bold',
},
manualCardRoot: {
maxWidth: '500px',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignContent: 'center',
alignSelf: 'center',
},
cardMedia: {
height: 300,
width: 300,
alignSelf: 'center',
},
alertRoot: {
width: '100%',
'& > * + *': {
marginTop: theme.spacing(2),
},
},
}))

export const ManualDisplay: React.FunctionComponent = () => {
const classes = useStyles()
const currentManualName = useRecoilValue(currentManualNameState)
const personManualQuery = usePersonManual(currentManualName)
const setCurrentPageTitle = useSetRecoilState(currentPageTitleState)

// Work out what we should display in the Main content for this manual.
if (personManualQuery.isLoading) {
setCurrentPageTitle(`${currentManualName}'s Manual`)
return (
// <Skeleton component="div" width="100%" height="100%"></Skeleton>
<Box
width="100%"
justifyContent="center"
alignContent="flex-start"
display="flex"
flexDirection="column"
>
<ReactQueryStatusLabel
style={{
width: 'fit-content',
alignSelf: 'center',
marginBottom: '10px',
}}
queryKey={['manual', currentManualName]}
/>
<Card
className={classes.manualCardRoot}
variant="outlined"
elevation={8}
>
<Box
width="100%"
justifyContent="center"
display="flex"
paddingTop="28px"
>
<Skeleton height={200} width={200} variant="circle"></Skeleton>
</Box>
<CardContent>
<Typography variant="h5" component="h2">
<Skeleton width={80} />
</Typography>
<Typography color="textSecondary">
<Skeleton width={160} />
</Typography>
<Typography variant="body2" component="p">
<Skeleton width={'100%'} />
<Skeleton width={'100%'} />
<Skeleton width={'60%'} />
</Typography>
</CardContent>
</Card>
</Box>
)
} else if (personManualQuery.isError) {
setCurrentPageTitle(`Error`)
return (
<Box>
<ReactQueryStatusLabel
style={{
width: 'fit-content',
alignSelf: 'center',
marginBottom: '10px',
}}
queryKey={['manual', currentManualName]}
/>
<Typography variant="h4" className={classes.errorStyle} gutterBottom>
Error:
</Typography>
<Typography variant="body1" paragraph>
{personManualQuery.error.message}
</Typography>
<Typography variant="body1" paragraph>
If this is happening locally, did you start the azure functions?
</Typography>
</Box>
)
} else if (
personManualQuery.data !== undefined &&
personManualQuery.data.numberOfRecords == 1
) {
setCurrentPageTitle(`${currentManualName}'s Manual`)
return (
<Box className={classes.alertRoot} display="flex" flexDirection="column">
<Alert severity="info">
In loading this manual we also did a shallow navigation to include the
manual name in the URL. This means the URL could be copied and shared!
(Deep Linking) Note: I haven&apos;t found a way to do this with clean
url&apos;s (IE just /[manual])
</Alert>
<Box
width="100%"
justifyContent="center"
alignContent="flex-start"
display="flex"
flexDirection="column"
>
<ReactQueryStatusLabel
style={{
width: 'fit-content',
alignSelf: 'center',
marginBottom: '10px',
}}
queryKey={['manual', currentManualName]}
/>
{personManualQuery.data.manuals.map((manual) => {
return (
<Card
className={classes.manualCardRoot}
variant="outlined"
elevation={8}
key={manual.id}
>
<CardMedia
className={classes.cardMedia}
image={manual.AvatarURL}
title={`${manual.name}'s Avatar`}
/>
<CardContent>
<Typography variant="h5" component="h2">
{manual.name}
</Typography>
<Typography color="textSecondary" paragraph>
Answer to the meaning of life:{' '}
{manual.answerToTheMeaningOfLife}
</Typography>
<Typography variant="body2" component="p">
{manual.description}
</Typography>
</CardContent>
</Card>
)
})}
</Box>
</Box>
)
} else {
return <div>Please Select a manual on the left hand side.</div>
}
}

export default ManualDisplay
145 changes: 145 additions & 0 deletions components/ManualsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React from 'react'
import {
Typography,
Button,
makeStyles,
ListItem,
ListItemAvatar,
ListItemText,
Box,
Avatar,
List,
} from '@material-ui/core'
import BackspaceIcon from '@material-ui/icons/Backspace'
import { Skeleton } from '@material-ui/lab'
import { useRouter } from 'next/router'
import { useSnackbar } from 'notistack'
import { useSetRecoilState, useRecoilValue } from 'recoil'
import { mobileMenuOpenState, currentManualNameState } from '../state/atoms'
import { usePersonManuals } from '../state/ReactQueryHooks'

const useStyles = makeStyles(() => ({
ManualsList: {
width: '250px',
},
errorStyle: {
color: 'red',
fontWeight: 'bold',
},
}))

export const ManualsList: React.FunctionComponent = () => {
const classes = useStyles()
const router = useRouter()
const { enqueueSnackbar } = useSnackbar()
const setMobileMenuOpen = useSetRecoilState(mobileMenuOpenState)
const currentManualName = useRecoilValue(currentManualNameState)
const PersonManualsQuery = usePersonManuals()

if (PersonManualsQuery.isLoading) {
return (
<List className={classes.ManualsList}>
{[1, 2, 3, 4, 5, 6].map((i) => {
return (
<ListItem key={i} divider>
<ListItemAvatar>
<Skeleton
height="30px"
width="30px"
variant="circle"
></Skeleton>
</ListItemAvatar>
<ListItemText>
<Skeleton height="30px" />
</ListItemText>
</ListItem>
)
})}
</List>
)
} else if (PersonManualsQuery.isError) {
return (
<List className={classes.ManualsList}>
<ListItem key="Error">
<Box component="div">
<Typography
variant="h4"
className={classes.errorStyle}
gutterBottom
>
Error:
</Typography>
<Typography variant="body1" paragraph>
Error from the server: &quot;{PersonManualsQuery.error.message}
&quot;
</Typography>
<Typography variant="body1" paragraph>
If this is happening locally, did you start the azure functions?
</Typography>
</Box>
</ListItem>
</List>
)
} else {
if (
PersonManualsQuery.data !== undefined &&
PersonManualsQuery.data.numberOfRecords > 0
) {
return (
<List className={classes.ManualsList}>
<Typography key="title" variant="h5" color="primary" gutterBottom>
Available Manuals:
</Typography>
<Button
variant="contained"
color="primary"
startIcon={<BackspaceIcon />}
onClick={() => {
router.push(`/CSRExample`, undefined, { shallow: true })
enqueueSnackbar('Returned to explanation', { variant: 'info' })
setMobileMenuOpen(false)
}}
disabled={currentManualName.length == 0 ? true : false}
style={{ marginBottom: '10px' }}
>
Clear Selection
</Button>
{PersonManualsQuery.data.manuals.map((manual) => {
return (
<React.Fragment key={manual.id}>
<ListItem
button
component="a"
href="#"
onClick={() => {
router.push(
`/CSRExample?manual=${manual.name}`,
undefined,
{ shallow: true },
)
setMobileMenuOpen(false)
}}
selected={manual.name == currentManualName ? true : false}
divider
>
<ListItemAvatar>
<Avatar src={manual.AvatarURL} />
</ListItemAvatar>
<ListItemText primary={manual.name} />
</ListItem>
</React.Fragment>
)
})}
</List>
)
} else {
return (
<List className={classes.ManualsList}>
<ListItem>No Manuals from the API...</ListItem>
</List>
)
}
}
}

export default ManualsList
23 changes: 22 additions & 1 deletion components/ReactQueryStatusLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,25 @@ import {
} from '@material-ui/core'
import DoneIcon from '@material-ui/icons/Done'
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'
import { useState, useEffect } from 'react'

const useStyles = makeStyles((theme) => ({
tooltip: {
fontSize: theme.typography.fontSize,
},
}))

// A custom hook to allow the Component to listen to updates from a callback function.
function useForceUpdate() {
const [value, setValue] = useState(0) // integer state
return () => setValue((value) => ++value) // update the state to force render
}

type ReactQueryStatusLabelProps = {
queryKey: QueryKeyOrPredicateFn
}

type extendedChipProps = ChipProps & ReactQueryStatusLabelProps

export const ReactQueryStatusLabel: React.FunctionComponent<extendedChipProps> = (
props,
) => {
Expand All @@ -29,6 +35,21 @@ export const ReactQueryStatusLabel: React.FunctionComponent<extendedChipProps> =
const query = queryCache.getQuery(queryKey)
const classes = useStyles()

// For some reason the userQueryCache hook does not update for changes to the cache!
// (Shallow compare maybe?) To get around this we have to pass our own function to
// the subscribe method of the cach and faux some state to cause an update when
// something happens to our query (Feels Hacky but works!).
const forceUpdate = useForceUpdate()
useEffect(() => {
queryCache.subscribe((queryCache, query) => {
if (query) {
if (query.queryKey.toString() == queryKey.toString()) {
forceUpdate()
}
}
})
}, [])

let labelText: string
const lastUpdated: Date = new Date(query.state.updatedAt)

Expand Down
Loading

0 comments on commit cdebdbc

Please sign in to comment.