-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Perf: Split SearchRouter to State and Actions
#79753
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
Changes from 7 commits
fc9d7bf
5352122
fbc3d90
728c413
242e7d0
092408d
b90935c
ef66bc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
mountiny marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; | ||
| import React, {useContext, useEffect, useRef, useState} from 'react'; | ||
| import type {AnimatedTextInputRef} from '@components/RNTextInput'; | ||
| import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; | ||
| import {navigationRef} from '@libs/Navigation/Navigation'; | ||
|
|
@@ -9,8 +9,11 @@ import NAVIGATORS from '@src/NAVIGATORS'; | |
| import SCREENS from '@src/SCREENS'; | ||
| import type ChildrenProps from '@src/types/utils/ChildrenProps'; | ||
|
|
||
| type SearchRouterContext = { | ||
| type SearchRouterStateContextType = { | ||
| isSearchRouterDisplayed: boolean; | ||
| }; | ||
|
|
||
| type SearchRouterActionsContextType = { | ||
| openSearchRouter: () => void; | ||
| closeSearchRouter: () => void; | ||
| toggleSearch: () => void; | ||
|
|
@@ -22,16 +25,17 @@ type HistoryState = { | |
| isSearchModalOpen?: boolean; | ||
| }; | ||
|
|
||
| const defaultSearchContext: SearchRouterContext = { | ||
| isSearchRouterDisplayed: false, | ||
| const defaultSearchRouterActionsContext: SearchRouterActionsContextType = { | ||
| openSearchRouter: () => {}, | ||
| closeSearchRouter: () => {}, | ||
| toggleSearch: () => {}, | ||
| registerSearchPageInput: () => {}, | ||
| unregisterSearchPageInput: () => {}, | ||
| }; | ||
|
|
||
| const Context = React.createContext<SearchRouterContext>(defaultSearchContext); | ||
| const SearchRouterStateContext = React.createContext<SearchRouterStateContextType>({isSearchRouterDisplayed: false}); | ||
|
|
||
| const SearchRouterActionsContext = React.createContext<SearchRouterActionsContextType>(defaultSearchRouterActionsContext); | ||
|
|
||
| const isBrowserWithHistory = typeof window !== 'undefined' && typeof window.history !== 'undefined'; | ||
| const canListenPopState = typeof window !== 'undefined' && typeof window.addEventListener === 'function'; | ||
|
|
@@ -69,87 +73,98 @@ function SearchRouterContextProvider({children}: ChildrenProps) { | |
| return () => window.removeEventListener('popstate', handlePopState); | ||
| }, []); | ||
|
|
||
| const routerContext = useMemo(() => { | ||
| const openSearchRouter = () => { | ||
| if (isBrowserWithHistory) { | ||
| window.history.pushState({isSearchModalOpen: true} satisfies HistoryState, ''); | ||
| } | ||
| close( | ||
| () => { | ||
| setIsSearchRouterDisplayed(true); | ||
| searchRouterDisplayedRef.current = true; | ||
| }, | ||
| false, | ||
| true, | ||
| ); | ||
| }; | ||
| const closeSearchRouter = () => { | ||
| setIsSearchRouterDisplayed(false); | ||
| searchRouterDisplayedRef.current = false; | ||
| if (isBrowserWithHistory) { | ||
| const state = window.history.state as HistoryState | null; | ||
| if (state?.isSearchModalOpen) { | ||
| window.history.replaceState({isSearchModalOpen: false} satisfies HistoryState, ''); | ||
| } | ||
| const openSearchRouter = () => { | ||
| if (isBrowserWithHistory) { | ||
| window.history.pushState({isSearchModalOpen: true} satisfies HistoryState, ''); | ||
| } | ||
| close( | ||
| () => { | ||
| setIsSearchRouterDisplayed(true); | ||
| searchRouterDisplayedRef.current = true; | ||
| }, | ||
| false, | ||
| true, | ||
| ); | ||
| }; | ||
| const closeSearchRouter = () => { | ||
| setIsSearchRouterDisplayed(false); | ||
| searchRouterDisplayedRef.current = false; | ||
| if (isBrowserWithHistory) { | ||
| const state = window.history.state as HistoryState | null; | ||
| if (state?.isSearchModalOpen) { | ||
| window.history.replaceState({isSearchModalOpen: false} satisfies HistoryState, ''); | ||
| } | ||
| }; | ||
|
|
||
| const startSearchRouterOpenSpan = () => { | ||
| startSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, { | ||
| name: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, | ||
| op: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, | ||
| attributes: { | ||
| trigger: 'keyboard', | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| // There are callbacks that live outside of React render-loop and interact with SearchRouter | ||
| // So we need a function that is based on ref to correctly open/close it | ||
| // When user is on `/search` page we focus the Input instead of showing router | ||
| const toggleSearch = () => { | ||
| const searchFullScreenRoutes = navigationRef.getRootState()?.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); | ||
| const lastRoute = searchFullScreenRoutes?.state?.routes?.at(-1); | ||
| const isUserOnSearchPage = isSearchTopmostFullScreenRoute() && lastRoute?.name === SCREENS.SEARCH.ROOT; | ||
|
|
||
| if (isUserOnSearchPage && searchPageInputRef.current) { | ||
| if (searchPageInputRef.current.isFocused()) { | ||
| searchPageInputRef.current.blur(); | ||
| } else { | ||
| startSearchRouterOpenSpan(); | ||
| searchPageInputRef.current.focus(); | ||
| } | ||
| } else if (searchRouterDisplayedRef.current) { | ||
| closeSearchRouter(); | ||
| } | ||
| }; | ||
|
|
||
| const startSearchRouterOpenSpan = () => { | ||
| startSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, { | ||
| name: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, | ||
| op: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, | ||
| attributes: { | ||
| trigger: 'keyboard', | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| // There are callbacks that live outside of React render-loop and interact with SearchRouter | ||
| // So we need a function that is based on ref to correctly open/close it | ||
| // When user is on `/search` page we focus the Input instead of showing router | ||
| const toggleSearch = () => { | ||
| const searchFullScreenRoutes = navigationRef.getRootState()?.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); | ||
| const lastRoute = searchFullScreenRoutes?.state?.routes?.at(-1); | ||
| const isUserOnSearchPage = isSearchTopmostFullScreenRoute() && lastRoute?.name === SCREENS.SEARCH.ROOT; | ||
|
|
||
| if (isUserOnSearchPage && searchPageInputRef.current) { | ||
| if (searchPageInputRef.current.isFocused()) { | ||
| searchPageInputRef.current.blur(); | ||
| } else { | ||
| startSearchRouterOpenSpan(); | ||
| openSearchRouter(); | ||
| searchPageInputRef.current.focus(); | ||
| } | ||
| }; | ||
|
|
||
| const registerSearchPageInput = (ref: AnimatedTextInputRef) => { | ||
| searchPageInputRef.current = ref; | ||
| }; | ||
|
|
||
| const unregisterSearchPageInput = () => { | ||
| searchPageInputRef.current = undefined; | ||
| }; | ||
|
|
||
| return { | ||
| isSearchRouterDisplayed, | ||
| openSearchRouter, | ||
| closeSearchRouter, | ||
| toggleSearch, | ||
| registerSearchPageInput, | ||
| unregisterSearchPageInput, | ||
| }; | ||
| }, [isSearchRouterDisplayed, setIsSearchRouterDisplayed]); | ||
| } else if (searchRouterDisplayedRef.current) { | ||
| closeSearchRouter(); | ||
| } else { | ||
| startSearchRouterOpenSpan(); | ||
| openSearchRouter(); | ||
| } | ||
| }; | ||
|
|
||
| const registerSearchPageInput = (ref: AnimatedTextInputRef) => { | ||
| searchPageInputRef.current = ref; | ||
| }; | ||
|
|
||
| const unregisterSearchPageInput = () => { | ||
| searchPageInputRef.current = undefined; | ||
| }; | ||
|
|
||
| // Because of the React Compiler we don't need to memoize it manually | ||
| // eslint-disable-next-line react/jsx-no-constructed-context-values | ||
|
Comment on lines
+141
to
+142
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it cause lint error without this comment?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it does |
||
| const actionsContextValue = { | ||
| openSearchRouter, | ||
| closeSearchRouter, | ||
| toggleSearch, | ||
| registerSearchPageInput, | ||
| unregisterSearchPageInput, | ||
| }; | ||
|
mountiny marked this conversation as resolved.
|
||
|
|
||
| // Because of the React Compiler we don't need to memoize it manually | ||
| // eslint-disable-next-line react/jsx-no-constructed-context-values | ||
|
Comment on lines
+151
to
+152
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, there is a lint error |
||
| const stateContextValue = {isSearchRouterDisplayed}; | ||
|
|
||
| return ( | ||
| <SearchRouterActionsContext.Provider value={actionsContextValue}> | ||
| <SearchRouterStateContext.Provider value={stateContextValue}>{children}</SearchRouterStateContext.Provider> | ||
| </SearchRouterActionsContext.Provider> | ||
| ); | ||
| } | ||
|
|
||
| return <Context.Provider value={routerContext}>{children}</Context.Provider>; | ||
| function useSearchRouterState() { | ||
| return useContext(SearchRouterStateContext); | ||
| } | ||
|
|
||
| function useSearchRouterContext() { | ||
| return useContext(Context); | ||
| function useSearchRouterActions() { | ||
| return useContext(SearchRouterActionsContext); | ||
| } | ||
|
|
||
| export {SearchRouterContextProvider, useSearchRouterContext}; | ||
| export {SearchRouterContextProvider, useSearchRouterState, useSearchRouterActions}; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Run |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ CONSISTENCY-5 (docs)
ESLint rule disables without justification can mask underlying issues and reduce code quality. Clear documentation ensures team members understand exceptions, promoting better maintainability.
Suggested fix:
Add a comment explaining why the
react-hooks/refsrule needs to be disabled here. For example:// eslint-disable-next-line react-hooks/refs -- <explanation of why this is needed>There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add comment as bot says
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is unrelated to my PR and I haven't touched the code below it, but because of the es-lint failure I decided to disable it to improve visibility of the issue
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aimane-chnaif Just to let you know instead of a comment I moved the function to follow the Rules of React so it can be compiled, because it turned out it's an easy one