Skip to content

Create componentWithErrorBoundary HOC for more graceful failure #11394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@audius/harmony'
import { useDispatch } from 'react-redux'

import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'
import { useSelector } from 'utils/reducer'

const { setArtistPick, unsetArtistPick } = tracksSocialActions
Expand All @@ -40,7 +41,7 @@ const messagesMap = {
}
}

export const ArtistPickModal = () => {
const ArtistPickModalContent = () => {
const {
isOpen,
onClose,
Expand Down Expand Up @@ -86,3 +87,10 @@ export const ArtistPickModal = () => {
</Modal>
)
}

export const ArtistPickModal = componentWithErrorBoundary(
ArtistPickModalContent,
{
name: 'ArtistPickModal'
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useDispatch, useSelector } from 'react-redux'
import { make, useRecord } from 'common/store/analytics/actions'
import { ArtistPopover } from 'components/artist/ArtistPopover'
import DynamicImage from 'components/dynamic-image/DynamicImage'
import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'
import LoadingSpinner from 'components/loading-spinner/LoadingSpinner'
import { MountPlacement } from 'components/types'
import UserBadges from 'components/user-badges/UserBadges'
Expand Down Expand Up @@ -119,7 +120,7 @@ const ArtistPopoverWrapper = ({
)
}

export const ArtistRecommendations = forwardRef<
const ArtistRecommendationsContent = forwardRef<
HTMLDivElement,
ArtistRecommendationsProps
>((props, ref) => {
Expand Down Expand Up @@ -266,3 +267,10 @@ export const ArtistRecommendations = forwardRef<
</div>
)
})

export const ArtistRecommendations = componentWithErrorBoundary(
ArtistRecommendationsContent,
{
name: 'ArtistRecommendations'
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useRef } from 'react'
// eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web
import { useSpring, animated } from 'react-spring'

import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'

import {
ArtistRecommendations,
ArtistRecommendationsProps
Expand All @@ -21,7 +23,7 @@ const fast = {
friction: 40
}

export const ArtistRecommendationsDropdown = (
const ArtistRecommendationsDropdownContent = (
props: ArtistRecommendationsDropdownProps
) => {
const { isVisible } = props
Expand All @@ -48,3 +50,10 @@ export const ArtistRecommendationsDropdown = (
</animated.div>
)
}

export const ArtistRecommendationsDropdown = componentWithErrorBoundary(
ArtistRecommendationsDropdownContent,
{
name: 'ArtistRecommendationsDropdown'
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cacheUsersSelectors } from '@audius/common/store'
import { Popup } from '@audius/harmony'
import { useSelector } from 'react-redux'

import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'
import { useMainContentRef } from 'pages/MainContentContext'
import { AppState } from 'store/types'
import zIndex from 'utils/zIndex'
Expand All @@ -20,7 +21,7 @@ type Props = {
onClose: () => void
}

export const ArtistRecommendationsPopup = (props: Props) => {
const ArtistRecommendationsPopupContent = (props: Props) => {
const { anchorRef, artistId, isVisible, onClose } = props
const mainContentRef = useMainContentRef()

Expand Down Expand Up @@ -58,3 +59,10 @@ export const ArtistRecommendationsPopup = (props: Props) => {
</Popup>
)
}

export const ArtistRecommendationsPopup = componentWithErrorBoundary(
ArtistRecommendationsPopupContent,
{
name: 'ArtistRecommendationsPopup'
}
)
7 changes: 6 additions & 1 deletion packages/web/src/components/artist/ArtistCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { profilePageActions, usersSocialActions } from '@audius/common/store'
import { FollowButton } from '@audius/harmony'
import { useDispatch } from 'react-redux'

import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'
import Stats, { StatProps } from 'components/stats/Stats'

import styles from './ArtistCard.module.css'
Expand All @@ -18,7 +19,7 @@ type ArtistCardProps = {
onNavigateAway: () => void
}

export const ArtistCard = (props: ArtistCardProps) => {
export const ArtistCardContent = (props: ArtistCardProps) => {
const { artist, onNavigateAway } = props
const {
user_id,
Expand Down Expand Up @@ -108,3 +109,7 @@ export const ArtistCard = (props: ArtistCardProps) => {
</div>
)
}

export const ArtistCard = componentWithErrorBoundary(ArtistCardContent, {
name: 'ArtistCard'
})
7 changes: 6 additions & 1 deletion packages/web/src/components/artist/ArtistPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Popover from 'antd/lib/popover'
import cn from 'classnames'

import { useSelector } from 'common/hooks/useSelector'
import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'
import { MountPlacement } from 'components/types'

import { ArtistCard } from './ArtistCard'
Expand Down Expand Up @@ -43,7 +44,7 @@ type ArtistPopoverProps = {
className?: string
}

export const ArtistPopover = ({
const ArtistPopoverContent = ({
handle,
children,
placement = Placement.RightBottom,
Expand Down Expand Up @@ -110,3 +111,7 @@ export const ArtistPopover = ({
</Component>
)
}

export const ArtistPopover = componentWithErrorBoundary(ArtistPopoverContent, {
name: 'ArtistPopover'
})
11 changes: 10 additions & 1 deletion packages/web/src/components/artist/ArtistSupporting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MAX_ARTIST_HOVER_TOP_SUPPORTING } from '@audius/common/utils'
import { IconTipping as IconTip } from '@audius/harmony'
import { useDispatch } from 'react-redux'

import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'
import { UserProfilePictureList } from 'components/notification/Notification/components/UserProfilePictureList'
import {
setUsers,
Expand All @@ -31,7 +32,8 @@ type ArtistSupportingProps = {
artist: User
onNavigateAway?: () => void
}
export const ArtistSupporting = (props: ArtistSupportingProps) => {

const ArtistSupportingContent = (props: ArtistSupportingProps) => {
const { artist, onNavigateAway } = props
const { user_id, supporting_count } = artist
const dispatch = useDispatch()
Expand Down Expand Up @@ -92,3 +94,10 @@ export const ArtistSupporting = (props: ArtistSupportingProps) => {
<div className={styles.emptyContainer} />
) : null
}

export const ArtistSupporting = componentWithErrorBoundary(
ArtistSupportingContent,
{
name: 'ArtistSupporting'
}
)
8 changes: 7 additions & 1 deletion packages/web/src/components/comments/ArtistPick.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { IconHeart, IconPin, IconText } from '@audius/harmony'

import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'

const pinIcon = { icon: IconPin }
const heartIcon = { icon: IconHeart, color: 'active' }

Expand All @@ -8,7 +10,7 @@ type ArtistPickProps = {
isLiked?: boolean
}

export const ArtistPick = ({
const ArtistPickContent = ({
isPinned = false,
isLiked = false
}: ArtistPickProps) => {
Expand All @@ -24,3 +26,7 @@ export const ArtistPick = ({
</IconText>
)
}

export const ArtistPick = componentWithErrorBoundary(ArtistPickContent, {
name: 'ArtistPick'
})
12 changes: 10 additions & 2 deletions packages/web/src/components/dynamic-image/DynamicImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Box } from '@audius/harmony'
import cn from 'classnames'

import transparentPlaceholderImg from 'assets/img/1x1-transparent.png'
import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'
import Skeleton from 'components/skeleton/Skeleton'

import styles from './DynamicImage.module.css'
Expand Down Expand Up @@ -95,7 +96,7 @@ const fadeIn = (
/**
* A dynamic image that transitions between changes to the `image` prop.
*/
const DynamicImage = ({
const DynamicImageContent = ({
image,
isUrl = true,
wrapperClassName,
Expand Down Expand Up @@ -202,4 +203,11 @@ const DynamicImage = ({
)
}

export default memo(DynamicImage)
const DynamicImageWithErrorBoundary = componentWithErrorBoundary(
memo(DynamicImageContent),
{
name: 'DynamicImage'
}
)

export default DynamicImageWithErrorBoundary
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { ReactNode, useCallback } from 'react'

import { ErrorLevel } from '@audius/common/models'
import {
ErrorBoundary,
ErrorBoundaryProps,
FallbackProps
} from 'react-error-boundary'
import { useDispatch } from 'react-redux'

import { handleError as handleErrorAction } from 'store/errors/actions'

interface ComponentWithErrorBoundaryOptions {
fallback?: ReactNode | null
/**
* Debugging name (for logs, error reporting)
*/
name?: string
}

type HandleError = NonNullable<ErrorBoundaryProps['onError']>

export function componentWithErrorBoundary<P extends object>(
WrappedComponent: React.ComponentType<P>,
options: ComponentWithErrorBoundaryOptions = {}
) {
const {
fallback = null,
name = WrappedComponent.displayName || WrappedComponent.name || 'Unnamed'
} = options

const ComponentWithErrorBoundaryWrapper = (props: P) => {
const dispatch = useDispatch()

const handleError: HandleError = useCallback(
(error, errorInfo) => {
console.error(`ComponentErrorBoundary (${name}):`, error, errorInfo)
dispatch(
handleErrorAction({
name: `ComponentErrorBoundary: ${name}`,
message: error.message,
shouldRedirect: false,
additionalInfo: errorInfo,
level: ErrorLevel.Error
})
)
},
[dispatch]
)

const fallbackRender = useCallback((_: FallbackProps) => {
return React.isValidElement(fallback) ? fallback : <>{fallback}</>
}, [])

return (
<ErrorBoundary fallbackRender={fallbackRender} onError={handleError}>
<WrappedComponent {...props} />
</ErrorBoundary>
)
}

ComponentWithErrorBoundaryWrapper.displayName = `WithErrorBoundary(${name})`
if (WrappedComponent.displayName) {
ComponentWithErrorBoundaryWrapper.displayName += `|${WrappedComponent.displayName}`
}

return ComponentWithErrorBoundaryWrapper
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useHover } from 'react-use'

import { make } from 'common/store/analytics/actions'
import { Avatar } from 'components/avatar/Avatar'
import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'
import Skeleton from 'components/skeleton/Skeleton'
import { useCoverPhoto } from 'hooks/useCoverPhoto'
import { useMedia } from 'hooks/useMedia'
Expand All @@ -34,7 +35,7 @@ type FollowArtistTileProps = {
user: UserMetadata
} & HTMLProps<HTMLInputElement>

export const FollowArtistCard = (props: FollowArtistTileProps) => {
const FollowArtistCardContent = (props: FollowArtistTileProps) => {
const {
user: { name, user_id, is_verified, track_count, follower_count }
} = props
Expand Down Expand Up @@ -216,6 +217,13 @@ export const FollowArtistCard = (props: FollowArtistTileProps) => {
)
}

export const FollowArtistCard = componentWithErrorBoundary(
FollowArtistCardContent,
{
name: 'FollowArtistCard'
}
)

export const FollowArtistTileSkeleton = () => {
const { isMobile } = useMedia()

Expand Down
7 changes: 6 additions & 1 deletion packages/web/src/components/nav/desktop/LeftNavLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useDispatch } from 'react-redux'
import { NavLink, useLocation } from 'react-router-dom'

import { make } from 'common/store/analytics/actions'
import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'
import {
RestrictionType,
useRequiresAccountOnClick
Expand All @@ -18,7 +19,7 @@ export type LeftNavLinkProps = Omit<NavItemProps, 'isSelected'> & {
exact?: boolean
}

export const LeftNavLink = (props: LeftNavLinkProps) => {
const LeftNavLinkContent = (props: LeftNavLinkProps) => {
const {
to,
disabled,
Expand Down Expand Up @@ -71,3 +72,7 @@ export const LeftNavLink = (props: LeftNavLinkProps) => {
</NavLink>
)
}

export const LeftNavLink = componentWithErrorBoundary(LeftNavLinkContent, {
name: 'LeftNavLink'
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { useCallback } from 'react'

import { ExpandableNavItem, ExpandableNavItemProps } from '@audius/harmony'

import { componentWithErrorBoundary } from 'components/error-wrapper/componentWithErrorBoundary'
import { RestrictionType, useRequiresAccountFn } from 'hooks/useRequiresAccount'

type Props = Omit<ExpandableNavItemProps, 'onClick'> & {
restriction?: RestrictionType
}

export const RestrictedExpandableNavItem = ({
const RestrictedExpandableNavItemContent = ({
restriction = 'none',
disabled,
...props
Expand All @@ -25,3 +26,10 @@ export const RestrictedExpandableNavItem = ({
<ExpandableNavItem onClick={handleClick} disabled={disabled} {...props} />
)
}

export const RestrictedExpandableNavItem = componentWithErrorBoundary(
RestrictedExpandableNavItemContent,
{
name: 'RestrictedExpandableNavItem'
}
)
Loading