Skip to content

Commit

Permalink
Convert state providers to use use-context-selector
Browse files Browse the repository at this point in the history
This user-land library improves performance by removing unnecessary
re-renders to components which use global contexts. By selecting the
state that the components use from the contexts, they'll limit their
re-rendering to only when that state changes.

See reactjs/rfcs#119 for more relevant
information about the performance issues with the context API.
  • Loading branch information
samholmes committed Jan 30, 2024
1 parent 6f30f56 commit ec3edc0
Show file tree
Hide file tree
Showing 13 changed files with 73 additions and 28 deletions.
27 changes: 27 additions & 0 deletions jestSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,30 @@ jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock
for (const log in global.console) {
global.console[log] = jest.fn()
}

jest.mock('use-context-selector', () => {
const contextValues = new Map()
return {
createContext: defaultValue => {
// Create new provider
const Provider = (props, context) => {
contextValues.set(Provider, props.value)
return props.children
}
// Get the value for the provider:
const currentValue = contextValues.get(Provider)
// Set it's default value:
contextValues.set(Provider, currentValue ?? defaultValue)
// Return provider
return {
Provider: Provider,
displayName: 'test'
}
},
useContextSelector: (context, selector) => {
const currentValue = contextValues.get(context.Provider)
const selected = selector(currentValue)
return selected
}
}
})
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,12 @@
"redux-thunk": "^2.3.0",
"rn-id-blurview": "^1.2.1",
"rn-qr-generator": "^1.3.1",
"scheduler": "^0.23.0",
"sha.js": "^2.4.11",
"sprintf-js": "^1.1.1",
"url": "^0.11.0",
"url-parse": "^1.5.2",
"use-context-selector": "^1.4.1",
"yaob": "^0.3.12",
"yavent": "^0.1.3"
},
Expand Down
10 changes: 5 additions & 5 deletions src/components/common/SceneWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element {
const activeUsername = useSelector(state => state.core.account.username)
const isLightAccount = accountId != null && activeUsername == null

const { footerHeight = 0 } = useSceneFooterState()
const footerHeight = useSceneFooterState(state => state.footerHeight ?? 0)

const navigation = useNavigation<NavigationBase>()
const theme = useTheme()
Expand All @@ -138,15 +138,15 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element {
[frame.height, frame.width]
)

const notificationHeight = theme.rem(4)
const headerBarHeight = getDefaultHeaderHeight(frame, false, 0)

// If the scene has scroll, this will be required for tabs and/or header animation
const handleScroll = useSceneScrollHandler(scroll && (hasTabs || hasHeader))

const { renderFooter } = useSceneFooterRenderState()
const renderFooter = useSceneFooterRenderState(state => state.renderFooter)

const renderScene = (keyboardAnimation: Animated.Value | undefined, trackerValue: number): JSX.Element => {
const notificationHeight = theme.rem(4)
const headerBarHeight = getDefaultHeaderHeight(frame, false, 0)

// If function children, the caller handles the insets and overscroll
const isFuncChildren = typeof children === 'function'

Expand Down
2 changes: 1 addition & 1 deletion src/components/navigation/HeaderBackground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { BlurBackground } from '../ui4/BlurBackground'
export const HeaderBackground = (props: any) => {
const theme = useTheme()

const { scrollState } = useSceneScrollContext()
const scrollState = useSceneScrollContext(state => state.scrollState)

return (
<HeaderBackgroundContainerView scrollY={scrollState.scrollY}>
Expand Down
3 changes: 2 additions & 1 deletion src/components/notification/NotificationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ const NotificationViewComponent = (props: Props) => {
const isBackupWarningShown = account.id != null && account.username == null

const { bottom: insetBottom } = useSafeAreaInsets()
const { footerOpenRatio, footerHeight = 0 } = useSceneFooterState()
const footerOpenRatio = useSceneFooterState(state => state.footerOpenRatio)
const footerHeight = useSceneFooterState(state => state.footerHeight ?? 0)

const [autoDetectTokenCards, setAutoDetectTokenCards] = React.useState<React.JSX.Element[]>([])

Expand Down
2 changes: 1 addition & 1 deletion src/components/scenes/WalletListScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function WalletListScene(props: Props) {

const sortOption = useSelector(state => state.ui.settings.walletsSort)

const { setKeepOpen } = useSceneFooterState()
const setKeepOpen = useSceneFooterState(state => state.setKeepOpen)

//
// Handlers
Expand Down
5 changes: 3 additions & 2 deletions src/components/themed/MenuTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ export const MenuTabs = (props: BottomTabBarProps) => {

const { bottom: insetBottom } = useSafeAreaInsets()

const { footerOpenRatio, resetFooterRatio } = useSceneFooterState()
const { renderFooter } = useSceneFooterRenderState()
const footerOpenRatio = useSceneFooterState(state => state.footerOpenRatio)
const resetFooterRatio = useSceneFooterState(state => state.resetFooterRatio)
const renderFooter = useSceneFooterRenderState(state => state.renderFooter)

const { height: keyboardHeight, progress: keyboardProgress } = useReanimatedKeyboardAnimation()
const menuTabHeightAndInsetBottomTermForShiftY = useDerivedValue(() => keyboardProgress.value * (insetBottom + MAX_TAB_BAR_HEIGHT), [insetBottom])
Expand Down
2 changes: 1 addition & 1 deletion src/components/themed/SceneFooterWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface SceneFooterProps {
export const SceneFooterWrapper = (props: SceneFooterProps) => {
const { children, noBackgroundBlur = false, sceneWrapperInfo } = props
const { hasTabs = true, isKeyboardOpen = false } = sceneWrapperInfo ?? {}
const { footerOpenRatio } = useSceneFooterState()
const footerOpenRatio = useSceneFooterState(state => state.footerOpenRatio)

const handleFooterLayout = useLayoutHeightInFooter()
const safeAreaInsets = useSafeAreaInsets()
Expand Down
3 changes: 2 additions & 1 deletion src/components/themed/SearchFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export const SearchFooter = (props: SearchFooterProps) => {

const textInputRef = React.useRef<SimpleTextInputRef>(null)

const { footerOpenRatio, setKeepOpen } = useSceneFooterState()
const footerOpenRatio = useSceneFooterState(state => state.footerOpenRatio)
const setKeepOpen = useSceneFooterState(state => state.setKeepOpen)

const handleSearchChangeText = useHandler((text: string) => {
onChangeText(text)
Expand Down
12 changes: 8 additions & 4 deletions src/state/SceneFooterState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const [SceneFooterRenderProvider, useSceneFooterRenderState] = createStat
* @param deps the dependencies for the render function to trigger re-renders
*/
export const useSceneFooterRender = (renderFn: FooterRender = defaultFooterRender, deps: DependencyList) => {
const { setRenderFooter } = useSceneFooterRenderState()
const setRenderFooter = useSceneFooterRenderState(state => state.setRenderFooter)

// The callback will allow us to trigger a re-render when the deps change
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down Expand Up @@ -127,11 +127,15 @@ export const useSceneFooterRender = (renderFn: FooterRender = defaultFooterRende
* thrashing for the footer state shared values.
*/
export const FooterAccordionEventService = () => {
const { scrollState } = useSceneScrollContext()
const scrollState = useSceneScrollContext(state => state.scrollState)
const { scrollBeginEvent, scrollEndEvent, scrollMomentumBeginEvent, scrollMomentumEndEvent, scrollY } = scrollState

const scrollYStart = useSharedValue<number | undefined>(undefined)
const { footerOpenRatio, footerOpenRatioStart, keepOpen, footerHeight = 1, snapTo } = useSceneFooterState()
const footerOpenRatio = useSceneFooterState(state => state.footerOpenRatio)
const footerOpenRatioStart = useSceneFooterState(state => state.footerOpenRatioStart)
const keepOpen = useSceneFooterState(state => state.keepOpen)
const footerHeight = useSceneFooterState(state => state.footerHeight ?? 1)
const snapTo = useSceneFooterState(state => state.snapTo)

// This factor will convert scroll delta into footer open value delta (a 0 to 1 fraction)
const scrollDeltaToRatioDeltaFactor = 1 / footerHeight
Expand Down Expand Up @@ -236,7 +240,7 @@ export const FooterAccordionEventService = () => {
* @returns layout handler for the component which height you want to measure
*/
export const useLayoutHeightInFooter = (): ((event: LayoutChangeEvent) => void) => {
const { setFooterHeight } = useSceneFooterState()
const setFooterHeight = useSceneFooterState(state => state.setFooterHeight)

const [layoutHeight, setLayoutHeight] = useState<number | undefined>(undefined)

Expand Down
2 changes: 1 addition & 1 deletion src/state/SceneScrollState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export type SceneScrollHandler = (event: NativeSyntheticEvent<NativeScrollEvent>
* the hook by the optional `isEnabled` boolean parameter.
*/
export const useSceneScrollHandler = (isEnabled: boolean = true): SceneScrollHandler => {
const { setScrollState } = useSceneScrollContext()
const setScrollState = useSceneScrollContext(state => state.setScrollState)

const localScrollState: ScrollState = useScrollState()
const isFocused = useIsFocused()
Expand Down
26 changes: 15 additions & 11 deletions src/state/createStateProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import React, { useContext } from 'react'
import React from 'react'
import { createContext, useContextSelector } from 'use-context-selector'

type Selector<State> = <T>(selector: (state: State) => T) => T
/**
* This creates a "state provider" component from a getter function.
* The function passed is a getter function to return the value for the state
* provider's context.
*
* @param getValue the function to return the context value
* @returns The context provider component and a useContextValue hook
* @param getState the function to return the context value (state)
* @returns The context provider component and a useStateSelector hook to select context state
*/
export function createStateProvider<Value>(getValue: () => Value): [React.FunctionComponent<{ children: React.ReactNode }>, () => Value] {
const Context = React.createContext<Value | undefined>(undefined)
export function createStateProvider<State>(getState: () => State): [React.FunctionComponent<{ children: React.ReactNode }>, Selector<State>] {
const Context = createContext<State | undefined>(undefined)
function WithContext({ children }: { children: React.ReactNode }) {
const value = getValue()
const value = getState()
return <Context.Provider value={value}>{children}</Context.Provider>
}

function useContextValue() {
const context = useContext(Context)
if (context == null) throw new Error(`Cannot call useDefinedContext outside of ${Context.displayName}`)
return context
function useStateSelector<T>(selector: (state: State) => T): T {
const state = useContextSelector(Context, state => {
if (state == null) throw new Error(`Cannot call useStateSelector outside of ${Context.displayName}`)
return selector(state)
})
return state
}

return [WithContext, useContextValue]
return [WithContext, useStateSelector]
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -18067,6 +18067,11 @@ url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"

use-context-selector@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/use-context-selector/-/use-context-selector-1.4.1.tgz#eb96279965846b72915d7f899b8e6ef1d768b0ae"
integrity sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA==

use-latest-callback@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51"
Expand Down

0 comments on commit ec3edc0

Please sign in to comment.