Skip to content

Commit

Permalink
Fix widget actions flickering due to getting redefined constantly.
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso committed Sep 7, 2023
1 parent b2a2061 commit 40a017d
Show file tree
Hide file tree
Showing 8 changed files with 354 additions and 330 deletions.
16 changes: 12 additions & 4 deletions packages/stateful/actions/react/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,20 @@ export const DaoActionsProvider = ({ children }: ActionsProviderProps) => {
const loadingWidgets = useWidgets({
suspendWhileLoading: true,
})
const widgetActionCategoryMakers = loadingWidgets.loading
? []
: loadingWidgets.data.flatMap(
const loadedWidgets = loadingWidgets.loading ? undefined : loadingWidgets.data
// Memoize this so we don't reconstruct the action makers on every render. The
// React components often need to access data from the widget values object so
// they are defined in the maker functions. If the maker function is called on
// every render, the components will get redefined and will flicker and not be
// editable because they're constantly re-rendering.
const widgetActionCategoryMakers = useMemo(
() =>
loadedWidgets?.flatMap(
({ widget, daoWidget }) =>
widget.getActionCategoryMakers?.(daoWidget.values || {}) ?? []
)
) ?? [],
[loadedWidgets]
)

// Make action categories.
const categories = makeActionCategoriesWithLabel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,32 @@ const useDefaults: UseDefaults<CreatePostData> = () => ({
},
})

const Component: ActionComponent = (props) => {
const { watch } = useFormContext<CreatePostData>()
const tokenId = watch((props.fieldNamePrefix + 'tokenId') as 'tokenId')
const tokenUri = watch((props.fieldNamePrefix + 'tokenUri') as 'tokenUri')
const uploaded = watch((props.fieldNamePrefix + 'uploaded') as 'uploaded')

const postLoading = useCachedLoading(
uploaded && tokenId && tokenUri
? postSelector({
id: tokenId,
metadataUri: tokenUri,
})
: undefined,
undefined
)

return (
<CreatePostComponent
{...props}
options={{
postLoading,
}}
/>
)
}

export const makeCreatePostActionMaker =
({ contract }: PressData): ActionMaker<CreatePostData> =>
({ t, context, address }) => {
Expand Down Expand Up @@ -89,33 +115,6 @@ export const makeCreatePostActionMaker =
[]
)

// Memoize to prevent unnecessary re-renders.
const Component: ActionComponent = useCallback((props) => {
const { watch } = useFormContext<CreatePostData>()
const tokenId = watch((props.fieldNamePrefix + 'tokenId') as 'tokenId')
const tokenUri = watch((props.fieldNamePrefix + 'tokenUri') as 'tokenUri')
const uploaded = watch((props.fieldNamePrefix + 'uploaded') as 'uploaded')

const postLoading = useCachedLoading(
uploaded && tokenId && tokenUri
? postSelector({
id: tokenId,
metadataUri: tokenUri,
})
: undefined,
undefined
)

return (
<CreatePostComponent
{...props}
options={{
postLoading,
}}
/>
)
}, [])

return {
key: ActionKey.CreatePost,
Icon: MemoEmoji,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@dao-dao/types/actions'
import { makeWasmMessage, objectMatchesStructure } from '@dao-dao/utils'

import { useActionOptions } from '../../../../../actions'
import { postSelector, postsSelector } from '../../state'
import { PressData } from '../../types'
import { DeletePostComponent, DeletePostData } from './Component'
Expand All @@ -22,9 +23,52 @@ const useDefaults: UseDefaults<DeletePostData> = () => ({
id: '',
})

export const makeDeletePostActionMaker =
({ contract }: PressData): ActionMaker<DeletePostData> =>
({ t, context, chain: { chain_id: chainId } }) => {
export const makeDeletePostActionMaker = ({
contract,
}: PressData): ActionMaker<DeletePostData> => {
// Make outside of the maker function returned below so it doesn't get
// redefined and thus remounted on every render.
const Component: ActionComponent = (props) => {
const {
chain: { chain_id: chainId },
} = useActionOptions()

const { watch } = useFormContext()
const id = watch((props.fieldNamePrefix + 'id') as 'id')

const postsLoading = useCachedLoading(
postsSelector({
contractAddress: contract,
chainId,
}),
[]
)

// Once created, manually load metadata; it won't be retrievable from
// the contract if it was successfully removed since the token was
// burned.
const postLoading = useCachedLoading(
!props.isCreating
? postSelector({
id,
metadataUri: `ipfs://${id}/metadata.json`,
})
: constSelector(undefined),
undefined
)

return (
<DeletePostComponent
{...props}
options={{
postsLoading,
postLoading,
}}
/>
)
}

return ({ t, context }) => {
// Only available in DAO context.
if (context.type !== ActionContextType.Dao) {
return null
Expand Down Expand Up @@ -75,46 +119,6 @@ export const makeDeletePostActionMaker =
[]
)

// Memoize to prevent unnecessary re-renders.
const Component: ActionComponent = useCallback(
(props) => {
const { watch } = useFormContext()
const id = watch((props.fieldNamePrefix + 'id') as 'id')

const postsLoading = useCachedLoading(
postsSelector({
contractAddress: contract,
chainId,
}),
[]
)

// Once created, manually load metadata; it won't be retrievable from
// the contract if it was successfully removed since the token was
// burned.
const postLoading = useCachedLoading(
!props.isCreating
? postSelector({
id,
metadataUri: `ipfs://${id}/metadata.json`,
})
: constSelector(undefined),
undefined
)

return (
<DeletePostComponent
{...props}
options={{
postsLoading,
postLoading,
}}
/>
)
},
[chainId]
)

return {
key: ActionKey.DeletePost,
Icon: TrashEmoji,
Expand All @@ -126,3 +130,4 @@ export const makeDeletePostActionMaker =
useDecodedCosmosMsg,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '@dao-dao/types/actions'
import { makeWasmMessage, objectMatchesStructure } from '@dao-dao/utils'

import { useActionOptions } from '../../../../../actions'
import { postSelector, postsSelector } from '../../state'
import { PressData } from '../../types'
import { UpdatePostComponent, UpdatePostData } from './Component'
Expand All @@ -28,9 +29,51 @@ const useDefaults: UseDefaults<UpdatePostData> = () => ({
},
})

export const makeUpdatePostActionMaker =
({ contract }: PressData): ActionMaker<UpdatePostData> =>
({ t, context, address, chain: { chain_id: chainId } }) => {
export const makeUpdatePostActionMaker = ({
contract,
}: PressData): ActionMaker<UpdatePostData> => {
// Make outside of the maker function returned below so it doesn't get
// redefined and thus remounted on every render.
const Component: ActionComponent = (props) => {
const {
chain: { chain_id: chainId },
} = useActionOptions()

const { watch } = useFormContext<UpdatePostData>()
const tokenId = watch((props.fieldNamePrefix + 'tokenId') as 'tokenId')
const tokenUri = watch((props.fieldNamePrefix + 'tokenUri') as 'tokenUri')
const uploaded = watch((props.fieldNamePrefix + 'uploaded') as 'uploaded')

const postLoading = useCachedLoading(
uploaded && tokenId && tokenUri
? postSelector({
id: tokenId,
metadataUri: tokenUri,
})
: undefined,
undefined
)

const postsLoading = useCachedLoading(
postsSelector({
contractAddress: contract,
chainId,
}),
[]
)

return (
<UpdatePostComponent
{...props}
options={{
postLoading,
postsLoading,
}}
/>
)
}

return ({ t, context, address }) => {
// Only available in DAO context.
if (context.type !== ActionContextType.Dao) {
return null
Expand Down Expand Up @@ -89,49 +132,6 @@ export const makeUpdatePostActionMaker =
[]
)

// Memoize to prevent unnecessary re-renders.
const Component: ActionComponent = useCallback(
(props) => {
const { watch } = useFormContext<UpdatePostData>()
const tokenId = watch((props.fieldNamePrefix + 'tokenId') as 'tokenId')
const tokenUri = watch(
(props.fieldNamePrefix + 'tokenUri') as 'tokenUri'
)
const uploaded = watch(
(props.fieldNamePrefix + 'uploaded') as 'uploaded'
)

const postLoading = useCachedLoading(
uploaded && tokenId && tokenUri
? postSelector({
id: tokenId,
metadataUri: tokenUri,
})
: undefined,
undefined
)

const postsLoading = useCachedLoading(
postsSelector({
contractAddress: contract,
chainId,
}),
[]
)

return (
<UpdatePostComponent
{...props}
options={{
postLoading,
postsLoading,
}}
/>
)
},
[chainId]
)

return {
key: ActionKey.UpdatePost,
Icon: PencilEmoji,
Expand All @@ -143,3 +143,4 @@ export const makeUpdatePostActionMaker =
useDecodedCosmosMsg,
}
}
}
31 changes: 18 additions & 13 deletions packages/stateful/widgets/widgets/Press/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,22 @@ export const PressWidget: Widget<PressData> = {
visibilityContext: WidgetVisibilityContext.Always,
Renderer,
Editor,
getActionCategoryMakers: (data) => [
({ t }) => ({
key: ActionCategoryKey.Press,
label: t('actionCategory.pressLabel'),
description: t('actionCategory.pressDescription'),
keywords: ['publish', 'article', 'news', 'announcement'],
actionMakers: [
makeCreatePostActionMaker(data),
makeUpdatePostActionMaker(data),
makeDeletePostActionMaker(data),
],
}),
],
getActionCategoryMakers: (data) => {
// Make makers in outer function so they're not remade on every render.
const actionMakers = [
makeCreatePostActionMaker(data),
makeUpdatePostActionMaker(data),
makeDeletePostActionMaker(data),
]

return [
({ t }) => ({
key: ActionCategoryKey.Press,
label: t('actionCategory.pressLabel'),
description: t('actionCategory.pressDescription'),
keywords: ['publish', 'article', 'news', 'announcement'],
actionMakers,
}),
]
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ export const CancelVesting: ActionComponent<CancelVestingOptions> = ({
const cancellableVestingContracts = vestingInfos.loading
? undefined
: vestingInfos.data.filter(
({ owner, vested }) => owner === address && vested !== '0'
({ owner, vested, vest: { status } }) =>
owner === address &&
vested !== '0' &&
!(typeof status === 'object' && 'canceled' in status)
)

return (
Expand Down
Loading

0 comments on commit 40a017d

Please sign in to comment.