Skip to content

Commit 92e3b5a

Browse files
committed
Convert state providers to use use-context-selector
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.
1 parent 9af94aa commit 92e3b5a

12 files changed

+72
-27
lines changed

jestSetup.js

+27
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,30 @@ jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock
233233
for (const log in global.console) {
234234
global.console[log] = jest.fn()
235235
}
236+
237+
jest.mock('use-context-selector', () => {
238+
const contextValues = new Map()
239+
return {
240+
createContext: defaultValue => {
241+
// Create new provider
242+
const Provider = (props, context) => {
243+
contextValues.set(Provider, props.value)
244+
return props.children
245+
}
246+
// Get the value for the provider:
247+
const currentValue = contextValues.get(Provider)
248+
// Set it's default value:
249+
contextValues.set(Provider, currentValue ?? defaultValue)
250+
// Return provider
251+
return {
252+
Provider: Provider,
253+
displayName: 'test'
254+
}
255+
},
256+
useContextSelector: (context, selector) => {
257+
const currentValue = contextValues.get(context.Provider)
258+
const selected = selector(currentValue)
259+
return selected
260+
}
261+
}
262+
})

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,12 @@
206206
"redux-thunk": "^2.3.0",
207207
"rn-id-blurview": "^1.2.1",
208208
"rn-qr-generator": "^1.3.1",
209+
"scheduler": "^0.23.0",
209210
"sha.js": "^2.4.11",
210211
"sprintf-js": "^1.1.1",
211212
"url": "^0.11.0",
212213
"url-parse": "^1.5.2",
214+
"use-context-selector": "^1.4.1",
213215
"yaob": "^0.3.12",
214216
"yavent": "^0.1.3"
215217
},

src/components/common/SceneWrapper.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element {
120120
const activeUsername = useSelector(state => state.core.account.username)
121121
const isLightAccount = accountId != null && activeUsername == null
122122

123-
const { footerHeight = 0 } = useSceneFooterState()
123+
const footerHeight = useSceneFooterState(state => state.footerHeight ?? 0)
124124

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

141-
const notificationHeight = theme.rem(4)
142-
const headerBarHeight = getDefaultHeaderHeight(frame, false, 0)
143-
144141
// If the scene has scroll, this will be required for tabs and/or header animation
145142
const handleScroll = useSceneScrollHandler(scroll && (hasTabs || hasHeader))
146143

147-
const { renderFooter } = useSceneFooterRenderState()
144+
const renderFooter = useSceneFooterRenderState(state => state.renderFooter)
148145

149146
const renderScene = (keyboardAnimation: Animated.Value | undefined, trackerValue: number): JSX.Element => {
147+
const notificationHeight = theme.rem(4)
148+
const headerBarHeight = getDefaultHeaderHeight(frame, false, 0)
149+
150150
// If function children, the caller handles the insets and overscroll
151151
const isFuncChildren = typeof children === 'function'
152152

src/components/navigation/HeaderBackground.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { BlurBackground } from '../ui4/BlurBackground'
1313
export const HeaderBackground = (props: any) => {
1414
const theme = useTheme()
1515

16-
const { scrollState } = useSceneScrollContext()
16+
const scrollState = useSceneScrollContext(state => state.scrollState)
1717

1818
return (
1919
<HeaderBackgroundContainerView scrollY={scrollState.scrollY}>

src/components/notification/NotificationView.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ const NotificationViewComponent = (props: Props) => {
3333
const isBackupWarningShown = account.id != null && account.username == null
3434

3535
const { bottom: insetBottom } = useSafeAreaInsets()
36-
const { footerOpenRatio, footerHeight = 0 } = useSceneFooterState()
36+
const footerOpenRatio = useSceneFooterState(state => state.footerOpenRatio)
37+
const footerHeight = useSceneFooterState(state => state.footerHeight ?? 0)
3738

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

src/components/themed/MenuTabs.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ export const MenuTabs = (props: BottomTabBarProps) => {
7070

7171
const { bottom: insetBottom } = useSafeAreaInsets()
7272

73-
const { footerOpenRatio, resetFooterRatio } = useSceneFooterState()
74-
const { renderFooter } = useSceneFooterRenderState()
73+
const footerOpenRatio = useSceneFooterState(state => state.footerOpenRatio)
74+
const resetFooterRatio = useSceneFooterState(state => state.resetFooterRatio)
75+
const renderFooter = useSceneFooterRenderState(state => state.renderFooter)
7576

7677
const { height: keyboardHeight, progress: keyboardProgress } = useReanimatedKeyboardAnimation()
7778
const menuTabHeightAndInsetBottomTermForShiftY = useDerivedValue(() => keyboardProgress.value * (insetBottom + MAX_TAB_BAR_HEIGHT), [insetBottom])

src/components/themed/SceneFooterWrapper.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export interface SceneFooterProps {
2020
export const SceneFooterWrapper = (props: SceneFooterProps) => {
2121
const { children, noBackgroundBlur = false, sceneWrapperInfo } = props
2222
const { hasTabs = true, isKeyboardOpen = false } = sceneWrapperInfo ?? {}
23-
const { footerOpenRatio } = useSceneFooterState()
23+
const footerOpenRatio = useSceneFooterState(state => state.footerOpenRatio)
2424

2525
const handleFooterLayout = useLayoutHeightInFooter()
2626
const safeAreaInsets = useSafeAreaInsets()

src/components/themed/SearchFooter.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export const SearchFooter = (props: SearchFooterProps) => {
2727

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

30-
const { footerOpenRatio, setKeepOpen } = useSceneFooterState()
30+
const footerOpenRatio = useSceneFooterState(state => state.footerOpenRatio)
31+
const setKeepOpen = useSceneFooterState(state => state.setKeepOpen)
3132

3233
const handleSearchChangeText = useHandler((text: string) => {
3334
onChangeText(text)

src/state/SceneFooterState.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export const [SceneFooterRenderProvider, useSceneFooterRenderState] = createStat
8080
* @param deps the dependencies for the render function to trigger re-renders
8181
*/
8282
export const useSceneFooterRender = (renderFn: FooterRender = defaultFooterRender, deps: DependencyList) => {
83-
const { setRenderFooter } = useSceneFooterRenderState()
83+
const setRenderFooter = useSceneFooterRenderState(state => state.setRenderFooter)
8484

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

133133
const scrollYStart = useSharedValue<number | undefined>(undefined)
134-
const { footerOpenRatio, footerOpenRatioStart, keepOpen, footerHeight = 1, snapTo } = useSceneFooterState()
134+
const footerOpenRatio = useSceneFooterState(state => state.footerOpenRatio)
135+
const footerOpenRatioStart = useSceneFooterState(state => state.footerOpenRatioStart)
136+
const keepOpen = useSceneFooterState(state => state.keepOpen)
137+
const footerHeight = useSceneFooterState(state => state.footerHeight ?? 1)
138+
const snapTo = useSceneFooterState(state => state.snapTo)
135139

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

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

src/state/SceneScrollState.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export type SceneScrollHandler = (event: NativeSyntheticEvent<NativeScrollEvent>
5050
* the hook by the optional `isEnabled` boolean parameter.
5151
*/
5252
export const useSceneScrollHandler = (isEnabled: boolean = true): SceneScrollHandler => {
53-
const { setScrollState } = useSceneScrollContext()
53+
const setScrollState = useSceneScrollContext(state => state.setScrollState)
5454

5555
const localScrollState: ScrollState = useScrollState()
5656
const isFocused = useIsFocused()

src/state/createStateProvider.tsx

+15-11
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
1-
import React, { useContext } from 'react'
1+
import React from 'react'
2+
import { createContext, useContextSelector } from 'use-context-selector'
23

4+
type Selector<State> = <T>(selector: (state: State) => T) => T
35
/**
46
* This creates a "state provider" component from a getter function.
57
* The function passed is a getter function to return the value for the state
68
* provider's context.
79
*
8-
* @param getValue the function to return the context value
9-
* @returns The context provider component and a useContextValue hook
10+
* @param getState the function to return the context value (state)
11+
* @returns The context provider component and a useStateSelector hook to select context state
1012
*/
11-
export function createStateProvider<Value>(getValue: () => Value): [React.FunctionComponent<{ children: React.ReactNode }>, () => Value] {
12-
const Context = React.createContext<Value | undefined>(undefined)
13+
export function createStateProvider<State>(getState: () => State): [React.FunctionComponent<{ children: React.ReactNode }>, Selector<State>] {
14+
const Context = createContext<State | undefined>(undefined)
1315
function WithContext({ children }: { children: React.ReactNode }) {
14-
const value = getValue()
16+
const value = getState()
1517
return <Context.Provider value={value}>{children}</Context.Provider>
1618
}
1719

18-
function useContextValue() {
19-
const context = useContext(Context)
20-
if (context == null) throw new Error(`Cannot call useDefinedContext outside of ${Context.displayName}`)
21-
return context
20+
function useStateSelector<T>(selector: (state: State) => T): T {
21+
const state = useContextSelector(Context, state => {
22+
if (state == null) throw new Error(`Cannot call useStateSelector outside of ${Context.displayName}`)
23+
return selector(state)
24+
})
25+
return state
2226
}
2327

24-
return [WithContext, useContextValue]
28+
return [WithContext, useStateSelector]
2529
}

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -18067,6 +18067,11 @@ url@^0.11.0:
1806718067
punycode "1.3.2"
1806818068
querystring "0.2.0"
1806918069

18070+
use-context-selector@^1.4.1:
18071+
version "1.4.1"
18072+
resolved "https://registry.yarnpkg.com/use-context-selector/-/use-context-selector-1.4.1.tgz#eb96279965846b72915d7f899b8e6ef1d768b0ae"
18073+
integrity sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA==
18074+
1807018075
use-latest-callback@^0.1.5:
1807118076
version "0.1.5"
1807218077
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51"

0 commit comments

Comments
 (0)