Skip to content

Commit

Permalink
perf: use zustand for shared portal area
Browse files Browse the repository at this point in the history
  • Loading branch information
lordspace74 committed Mar 6, 2023
1 parent 2e9e86b commit 663668b
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 9 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"@gorhom/portal": "^1.0.14",
"nanoid": "^4.0.0",
"ts-pattern": "^4.0.5",
"urql": ">=2"
"urql": ">=2",
"zustand": "^4.3.6"
}
}
13 changes: 5 additions & 8 deletions src/example-app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,22 @@
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { StatusBar } from 'expo-status-bar'
import React, {
useCallback, useContext, useMemo, useState,
} from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { Button, Text } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { ActivityIndicator, Switch } from 'react-native-paper'
import Animated, {
CurvedTransition,
} from 'react-native-reanimated'
import Animated, { CurvedTransition } from 'react-native-reanimated'
import { SafeAreaProvider } from 'react-native-safe-area-context'

import NativePortal from '../components/NativePortal'
import DefaultSnackbarComponent from '../components/SnackbarComponent'
import SnackbarPresentationView from '../components/SnackbarPresentationView'
import { SharedPortalAreaContext, SharedPortalAreaProvider, SharedPortalPresentationArea } from '../contexts/SharedPortalArea'
import { SnackbarProvider } from '../contexts/Snackbar'
import { StringsProvider } from '../contexts/Strings'
import useAddSnackbar from '../hooks/useAddSnackbar'
import useAlert from '../hooks/useAlert'
import useConfirm from '../hooks/useConfirm'
import useSharedPortalArea, { SharedPortalAreaProvider, SharedPortalPresentationArea } from '../hooks/useSharedPortalArea'
import Column from '../primitives/Column'
import Row from '../primitives/Row'

Expand All @@ -39,7 +35,8 @@ const CustomSnackbarComponent: React.FC<SnackbarComponentProps> = (props) => (
)

const Body: React.FC = () => {
const { insets, size } = useContext(SharedPortalAreaContext)
const insets = useSharedPortalArea((state) => state.insets)
const size = useSharedPortalArea((state) => state.size)
const [hasCustomSnackbar, setHasCustomSnackbar] = useState(false)
const [confirmationDialogResponse, setConfirmationDialogResponse] = useState<boolean>()
const addSnackbar = useAddSnackbar()
Expand Down
149 changes: 149 additions & 0 deletions src/hooks/useSharedPortalArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { PortalProvider } from '@gorhom/portal'
import { nanoid } from 'nanoid'
import React, { useCallback, useEffect } from 'react'
import { Dimensions } from 'react-native'
import Animated, { CurvedTransition } from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { create } from 'zustand'

import NativePortal from '../components/NativePortal'

import type { PropsWithChildren } from 'react'
import type {
LayoutChangeEvent, LayoutRectangle, ViewStyle, StyleProp, Insets,
} from 'react-native'

type InsetsWithId = Required<Insets> & { readonly id: string }

interface SharedPortalAreaStore {
readonly allCustomInsets: readonly InsetsWithId[]
readonly defaultInsets: Required<Insets>
readonly setDefaultInsets: (defaultInsets: Required<Insets>) => void
readonly insets: Required<Insets>
readonly size: LayoutRectangle
readonly pushInset: (insets: InsetsWithId) => void
readonly removeInset: (id: string) => void
readonly setSize: (size: LayoutRectangle) => void
}

function calculateInset(allCustomInsets: SharedPortalAreaStore['allCustomInsets'], defaultInsets: SharedPortalAreaStore['defaultInsets']) {
// eslint-disable-next-line unicorn/prefer-at
const lastInset = allCustomInsets[allCustomInsets.length - 1]
return lastInset ? { ...defaultInsets, ...lastInset } : defaultInsets
}

const useSharedPortalArea = create<SharedPortalAreaStore>((set, get) => ({
allCustomInsets: [] as readonly InsetsWithId[],
defaultInsets: DEFAULT_INSETS,
setDefaultInsets: (defaultInsets: Required<Insets>) => set(() => ({
defaultInsets,
insets: calculateInset(get().allCustomInsets, defaultInsets),
})),
insets: DEFAULT_INSETS,
size: {
x: 0,
y: 0,
width: Dimensions.get('window').width,
height: 0,
},
pushInset: (insets: InsetsWithId) => set((state) => {
const allCustomInsets = [...state.allCustomInsets, insets]

return {
allCustomInsets,
insets: calculateInset(allCustomInsets, state.defaultInsets),
}
}),
removeInset: (id: string) => set((state) => {
const allCustomInsets = state.allCustomInsets.filter(({ id: prevId }) => prevId !== id)

return {
allCustomInsets,
insets: calculateInset(allCustomInsets, state.defaultInsets),
}
}),
setSize: (size: LayoutRectangle) => set(() => ({ size })),
}))

const DEFAULT_INSETS = {
top: 0, bottom: 0, left: 0, right: 0,
}

export const SharedPortalAreaProvider: React.FC<PropsWithChildren<{readonly insets?: Insets}>> = ({ children, insets }) => {
const setDefaultInsets = useSharedPortalArea((state) => state.setDefaultInsets)

useEffect(() => {
setDefaultInsets(insets ? { ...DEFAULT_INSETS, ...insets } : DEFAULT_INSETS)
}, [insets, setDefaultInsets])

return (
<PortalProvider>
{children}
</PortalProvider>
)
}

// explicitely set all insets
export const useUpdateSharedPortalAreaInsets = (insets: Required<Insets>, enable = true) => {
const pushInset = useSharedPortalArea((state) => state.pushInset)
const removeInset = useSharedPortalArea((state) => state.removeInset)

useEffect(() => {
if (enable) {
const id = nanoid()
pushInset({ ...insets, id })
return () => removeInset(id)
}
return () => {}
}, [
enable, insets, pushInset, removeInset,
])
}

// Set insets, but with safe area as default
export const useUpdateSharedPortalSafeAreaInsets = (insets: Insets, enable = true) => {
const safeAreaInsets = useSafeAreaInsets()
const pushInset = useSharedPortalArea((state) => state.pushInset)
const removeInset = useSharedPortalArea((state) => state.removeInset)

useEffect(() => {
if (enable) {
const id = nanoid()
pushInset({ ...safeAreaInsets, ...insets, id })
return () => removeInset(id)
}
return () => {}
}, [
safeAreaInsets, insets, enable, pushInset, removeInset,
])
}

type SharedPortalPresentationAreaProps = PropsWithChildren<{ readonly style?: StyleProp<ViewStyle>, readonly colorize?: boolean }>

export const SharedPortalPresentationArea: React.FC<SharedPortalPresentationAreaProps> = ({
children,
style,
colorize,
}) => {
const insets = useSharedPortalArea((state) => state.insets)
const setSize = useSharedPortalArea((state) => state.setSize)

const onLayout = useCallback((event: LayoutChangeEvent) => {
setSize(event.nativeEvent.layout)
}, [setSize])

return (
<NativePortal insets={insets} colorize={colorize}>
<Animated.View
layout={CurvedTransition.duration(500)}
onLayout={onLayout}
style={style}
pointerEvents='box-none'
>
{ children }
</Animated.View>
</NativePortal>
)
}

export default useSharedPortalArea
14 changes: 14 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -18199,6 +18199,11 @@ use-latest-callback@^0.1.5:
dependencies:
object-assign "^4.1.1"

[email protected]:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==

use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
Expand Down Expand Up @@ -18413,8 +18418,10 @@ watchpack@^1.6.1, watchpack@^1.7.4:
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453"
integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==
dependencies:
chokidar "^3.4.1"
graceful-fs "^4.1.2"
neo-async "^2.5.0"
watchpack-chokidar2 "^2.0.1"
optionalDependencies:
chokidar "^3.4.1"
watchpack-chokidar2 "^2.0.1"
Expand Down Expand Up @@ -19105,6 +19112,13 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==

zustand@^4.3.6:
version "4.3.6"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.6.tgz#ce7804eb75361af0461a2d0536b65461ec5de86f"
integrity sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==
dependencies:
use-sync-external-store "1.2.0"

zwitch@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
Expand Down

0 comments on commit 663668b

Please sign in to comment.