Merge pull request #4188 from overleaf/ae-memo

Improve React performance by memoizing components and values

GitOrigin-RevId: 805278b8b7ac04c3dc4b078fa53cc0e3770d261b
This commit is contained in:
Alf Eaton
2021-06-22 02:07:08 +00:00
committed by Copybot
parent d64172cfff
commit eebeffc1c5
11 changed files with 216 additions and 157 deletions
@@ -13,7 +13,7 @@ import withErrorBoundary from '../../../infrastructure/error-boundary'
import { FetchError } from '../../../infrastructure/fetch-json' import { FetchError } from '../../../infrastructure/fetch-json'
import { useChatContext } from '../context/chat-context' import { useChatContext } from '../context/chat-context'
function ChatPane() { const ChatPane = React.memo(function ChatPane() {
const { t } = useTranslation() const { t } = useTranslation()
const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool }) const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool })
@@ -81,7 +81,7 @@ function ChatPane() {
/> />
</aside> </aside>
) )
} })
function LoadingSpinner() { function LoadingSpinner() {
const { t } = useTranslation() const { t } = useTranslation()
@@ -264,19 +264,34 @@ export function ChatProvider({ children }) {
markMessagesAsRead, markMessagesAsRead,
]) ])
const value = { const value = useMemo(
status: state.status, () => ({
messages: state.messages, status: state.status,
initialMessagesLoaded: state.initialMessagesLoaded, messages: state.messages,
atEnd: state.atEnd, initialMessagesLoaded: state.initialMessagesLoaded,
unreadMessageCount: state.unreadMessageCount, atEnd: state.atEnd,
loadInitialMessages, unreadMessageCount: state.unreadMessageCount,
loadMoreMessages, loadInitialMessages,
reset, loadMoreMessages,
sendMessage, reset,
markMessagesAsRead, sendMessage,
error: state.error, markMessagesAsRead,
} error: state.error,
}),
[
loadInitialMessages,
loadMoreMessages,
markMessagesAsRead,
reset,
sendMessage,
state.atEnd,
state.error,
state.initialMessagesLoaded,
state.messages,
state.status,
state.unreadMessageCount,
]
)
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider> return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>
} }
@@ -35,99 +35,104 @@ const chatContextPropTypes = {
unreadMessageCount: PropTypes.number.isRequired, unreadMessageCount: PropTypes.number.isRequired,
} }
function EditorNavigationToolbarRoot({ const EditorNavigationToolbarRoot = React.memo(
onlineUsersArray, function EditorNavigationToolbarRoot({
openDoc, onlineUsersArray,
openShareProjectModal, openDoc,
}) { openShareProjectModal,
const { user } = useApplicationContext(applicationContextPropTypes) }) {
const { user } = useApplicationContext(applicationContextPropTypes)
const { const {
cobranding, cobranding,
loading, loading,
isRestrictedTokenMember, isRestrictedTokenMember,
projectName, projectName,
renameProject, renameProject,
isProjectOwner, isProjectOwner,
} = useEditorContext(editorContextPropTypes) } = useEditorContext(editorContextPropTypes)
const { const {
chatIsOpen, chatIsOpen,
setChatIsOpen, setChatIsOpen,
reviewPanelOpen, reviewPanelOpen,
setReviewPanelOpen, setReviewPanelOpen,
view, view,
setView, setView,
setLeftMenuShown, setLeftMenuShown,
pdfLayout, pdfLayout,
} = useLayoutContext(layoutContextPropTypes) } = useLayoutContext(layoutContextPropTypes)
const { markMessagesAsRead, unreadMessageCount } = useChatContext( const { markMessagesAsRead, unreadMessageCount } = useChatContext(
chatContextPropTypes chatContextPropTypes
) )
const toggleChatOpen = useCallback(() => { const toggleChatOpen = useCallback(() => {
if (!chatIsOpen) { if (!chatIsOpen) {
markMessagesAsRead() markMessagesAsRead()
} }
setChatIsOpen(value => !value) setChatIsOpen(value => !value)
}, [chatIsOpen, setChatIsOpen, markMessagesAsRead]) }, [chatIsOpen, setChatIsOpen, markMessagesAsRead])
const toggleReviewPanelOpen = useCallback( const toggleReviewPanelOpen = useCallback(
() => setReviewPanelOpen(value => !value), () => setReviewPanelOpen(value => !value),
[setReviewPanelOpen] [setReviewPanelOpen]
) )
const toggleHistoryOpen = useCallback(() => { const toggleHistoryOpen = useCallback(() => {
setView(view === 'history' ? 'editor' : 'history') setView(view === 'history' ? 'editor' : 'history')
}, [view, setView]) }, [view, setView])
const togglePdfView = useCallback(() => { const togglePdfView = useCallback(() => {
setView(view === 'pdf' ? 'editor' : 'pdf') setView(view === 'pdf' ? 'editor' : 'pdf')
}, [view, setView]) }, [view, setView])
const openShareModal = useCallback(() => { const openShareModal = useCallback(() => {
openShareProjectModal(isProjectOwner) openShareProjectModal(isProjectOwner)
}, [openShareProjectModal, isProjectOwner]) }, [openShareProjectModal, isProjectOwner])
const onShowLeftMenuClick = useCallback( const onShowLeftMenuClick = useCallback(
() => setLeftMenuShown(value => !value), () => setLeftMenuShown(value => !value),
[setLeftMenuShown] [setLeftMenuShown]
) )
function goToUser(user) { const goToUser = useCallback(
if (user.doc && typeof user.row === 'number') { user => {
openDoc(user.doc, { gotoLine: user.row + 1 }) if (user.doc && typeof user.row === 'number') {
} openDoc(user.doc, { gotoLine: user.row + 1 })
}
},
[openDoc]
)
// using {display: 'none'} as 1:1 migration from Angular's ng-hide. Using
// `loading ? null : <ToolbarHeader/>` causes UI glitches
return (
<ToolbarHeader
style={loading ? { display: 'none' } : {}}
cobranding={cobranding}
onShowLeftMenuClick={onShowLeftMenuClick}
chatIsOpen={chatIsOpen}
unreadMessageCount={unreadMessageCount}
toggleChatOpen={toggleChatOpen}
reviewPanelOpen={reviewPanelOpen}
toggleReviewPanelOpen={toggleReviewPanelOpen}
historyIsOpen={view === 'history'}
toggleHistoryOpen={toggleHistoryOpen}
onlineUsers={onlineUsersArray}
goToUser={goToUser}
isRestrictedTokenMember={isRestrictedTokenMember}
isAnonymousUser={user == null}
projectName={projectName}
renameProject={renameProject}
openShareModal={openShareModal}
pdfViewIsOpen={view === 'pdf'}
pdfButtonIsVisible={pdfLayout === 'flat'}
togglePdfView={togglePdfView}
/>
)
} }
)
// using {display: 'none'} as 1:1 migration from Angular's ng-hide. Using
// `loading ? null : <ToolbarHeader/>` causes UI glitches
return (
<ToolbarHeader
style={loading ? { display: 'none' } : {}}
cobranding={cobranding}
onShowLeftMenuClick={onShowLeftMenuClick}
chatIsOpen={chatIsOpen}
unreadMessageCount={unreadMessageCount}
toggleChatOpen={toggleChatOpen}
reviewPanelOpen={reviewPanelOpen}
toggleReviewPanelOpen={toggleReviewPanelOpen}
historyIsOpen={view === 'history'}
toggleHistoryOpen={toggleHistoryOpen}
onlineUsers={onlineUsersArray}
goToUser={goToUser}
isRestrictedTokenMember={isRestrictedTokenMember}
isAnonymousUser={user == null}
projectName={projectName}
renameProject={renameProject}
openShareModal={openShareModal}
pdfViewIsOpen={view === 'pdf'}
pdfButtonIsVisible={pdfLayout === 'flat'}
togglePdfView={togglePdfView}
/>
)
}
EditorNavigationToolbarRoot.propTypes = { EditorNavigationToolbarRoot.propTypes = {
onlineUsersArray: PropTypes.array.isRequired, onlineUsersArray: PropTypes.array.isRequired,
@@ -15,7 +15,7 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.mac
const [publishModalModules] = importOverleafModules('publishModal') const [publishModalModules] = importOverleafModules('publishModal')
const PublishButton = publishModalModules?.import.default const PublishButton = publishModalModules?.import.default
function ToolbarHeader({ const ToolbarHeader = React.memo(function ToolbarHeader({
cobranding, cobranding,
onShowLeftMenuClick, onShowLeftMenuClick,
chatIsOpen, chatIsOpen,
@@ -90,7 +90,7 @@ function ToolbarHeader({
</div> </div>
</header> </header>
) )
} })
ToolbarHeader.propTypes = { ToolbarHeader.propTypes = {
onShowLeftMenuClick: PropTypes.func.isRequired, onShowLeftMenuClick: PropTypes.func.isRequired,
@@ -18,7 +18,7 @@ import { useDroppable } from '../contexts/file-tree-draggable'
import { useFileTreeSocketListener } from '../hooks/file-tree-socket-listener' import { useFileTreeSocketListener } from '../hooks/file-tree-socket-listener'
import FileTreeModalCreateFile from './modals/file-tree-modal-create-file' import FileTreeModalCreateFile from './modals/file-tree-modal-create-file'
function FileTreeRoot({ const FileTreeRoot = React.memo(function FileTreeRoot({
projectId, projectId,
rootFolder, rootFolder,
rootDocId, rootDocId,
@@ -64,7 +64,7 @@ function FileTreeRoot({
<FileTreeModalError /> <FileTreeModalError />
</FileTreeContext> </FileTreeContext>
) )
} })
function FileTreeRootFolder() { function FileTreeRootFolder() {
useFileTreeSocketListener() useFileTreeSocketListener()
@@ -9,7 +9,7 @@ import localStorage from '../../../infrastructure/local-storage'
import withErrorBoundary from '../../../infrastructure/error-boundary' import withErrorBoundary from '../../../infrastructure/error-boundary'
import { useEditorContext } from '../../../shared/context/editor-context' import { useEditorContext } from '../../../shared/context/editor-context'
function OutlinePane({ const OutlinePane = React.memo(function OutlinePane({
isTexFile, isTexFile,
outline, outline,
jumpToLine, jumpToLine,
@@ -73,7 +73,7 @@ function OutlinePane({
) : null} ) : null}
</div> </div>
) )
} })
OutlinePane.propTypes = { OutlinePane.propTypes = {
isTexFile: PropTypes.bool.isRequired, isTexFile: PropTypes.bool.isRequired,
@@ -5,7 +5,7 @@ import PreviewLogsPane from './preview-logs-pane'
import PreviewFirstErrorPopUp from './preview-first-error-pop-up' import PreviewFirstErrorPopUp from './preview-first-error-pop-up'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
function PreviewPane({ const PreviewPane = React.memo(function PreviewPane({
compilerState, compilerState,
onClearCache, onClearCache,
onRecompile, onRecompile,
@@ -139,7 +139,7 @@ function PreviewPane({
) : null} ) : null}
</> </>
) )
} })
PreviewPane.propTypes = { PreviewPane.propTypes = {
compilerState: PropTypes.shape({ compilerState: PropTypes.shape({
@@ -79,7 +79,7 @@ export function useProjectContext() {
return context return context
} }
export default function ShareProjectModal({ const ShareProjectModal = React.memo(function ShareProjectModal({
handleHide, handleHide,
show, show,
animation = true, animation = true,
@@ -161,10 +161,12 @@ export default function ShareProjectModal({
</ProjectContext.Provider> </ProjectContext.Provider>
</ShareProjectContext.Provider> </ShareProjectContext.Provider>
) )
} })
ShareProjectModal.propTypes = { ShareProjectModal.propTypes = {
animation: PropTypes.bool, animation: PropTypes.bool,
handleHide: PropTypes.func.isRequired, handleHide: PropTypes.func.isRequired,
isAdmin: PropTypes.bool.isRequired, isAdmin: PropTypes.bool.isRequired,
show: PropTypes.bool.isRequired, show: PropTypes.bool.isRequired,
} }
export default ShareProjectModal
@@ -1,4 +1,4 @@
import React, { createContext, useContext } from 'react' import React, { createContext, useContext, useMemo } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
export const ApplicationContext = createContext() export const ApplicationContext = createContext()
@@ -15,16 +15,20 @@ ApplicationContext.Provider.propTypes = {
} }
export function ApplicationProvider({ children }) { export function ApplicationProvider({ children }) {
const applicationContextValue = { const value = useMemo(() => {
gitBridgePublicBaseUrl: window.gitBridgePublicBaseUrl, const value = {
} gitBridgePublicBaseUrl: window.gitBridgePublicBaseUrl,
}
if (window.user.id) { if (window.user.id) {
applicationContextValue.user = window.user value.user = window.user
} }
return value
}, [])
return ( return (
<ApplicationContext.Provider value={applicationContextValue}> <ApplicationContext.Provider value={value}>
{children} {children}
</ApplicationContext.Provider> </ApplicationContext.Provider>
) )
@@ -1,4 +1,10 @@
import React, { createContext, useCallback, useContext, useEffect } from 'react' import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
} from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import useScopeValue from './util/scope-value-hook' import useScopeValue from './util/scope-value-hook'
import useBrowserWindow from '../hooks/use-browser-window' import useBrowserWindow from '../hooks/use-browser-window'
@@ -34,19 +40,23 @@ EditorContext.Provider.propTypes = {
export function EditorProvider({ children, settings }) { export function EditorProvider({ children, settings }) {
const ide = useIdeContext() const ide = useIdeContext()
const cobranding = window.brandVariation const cobranding = useMemo(
? { () =>
logoImgUrl: window.brandVariation.logo_url, window.brandVariation
brandVariationName: window.brandVariation.name, ? {
brandVariationId: window.brandVariation.id, logoImgUrl: window.brandVariation.logo_url,
brandId: window.brandVariation.brand_id, brandVariationName: window.brandVariation.name,
brandVariationHomeUrl: window.brandVariation.home_url, brandVariationId: window.brandVariation.id,
publishGuideHtml: window.brandVariation.publish_guide_html, brandId: window.brandVariation.brand_id,
partner: window.brandVariation.partner, brandVariationHomeUrl: window.brandVariation.home_url,
brandedMenu: window.brandVariation.branded_menu, publishGuideHtml: window.brandVariation.publish_guide_html,
submitBtnHtml: window.brandVariation.submit_button_html, partner: window.brandVariation.partner,
} brandedMenu: window.brandVariation.branded_menu,
: undefined submitBtnHtml: window.brandVariation.submit_button_html,
}
: undefined,
[]
)
const [loading] = useScopeValue('state.loading') const [loading] = useScopeValue('state.loading')
const [projectRootDocId] = useScopeValue('project.rootDoc_id') const [projectRootDocId] = useScopeValue('project.rootDoc_id')
@@ -87,23 +97,33 @@ export function EditorProvider({ children, settings }) {
) )
}, [projectName, setTitle]) }, [projectName, setTitle])
const editorContextValue = { const value = useMemo(
cobranding, () => ({
hasPremiumCompile: compileGroup === 'priority', cobranding,
loading, hasPremiumCompile: compileGroup === 'priority',
projectId: window.project_id, loading,
projectRootDocId, projectId: window.project_id,
projectName: projectName || '', // initially might be empty in Angular projectRootDocId,
renameProject, projectName: projectName || '', // initially might be empty in Angular
isProjectOwner: ownerId === window.user.id, renameProject,
isRestrictedTokenMember: window.isRestrictedTokenMember, isProjectOwner: ownerId === window.user.id,
rootFolder, isRestrictedTokenMember: window.isRestrictedTokenMember,
} rootFolder,
}),
[
cobranding,
compileGroup,
loading,
ownerId,
projectName,
projectRootDocId,
renameProject,
rootFolder,
]
)
return ( return (
<EditorContext.Provider value={editorContextValue}> <EditorContext.Provider value={value}>{children}</EditorContext.Provider>
{children}
</EditorContext.Provider>
) )
} }
@@ -1,4 +1,4 @@
import React, { createContext, useContext, useCallback } from 'react' import React, { createContext, useContext, useCallback, useMemo } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import useScopeValue from './util/scope-value-hook' import useScopeValue from './util/scope-value-hook'
import { useIdeContext } from './ide-context' import { useIdeContext } from './ide-context'
@@ -43,19 +43,32 @@ export function LayoutProvider({ children }) {
'ui.reviewPanelOpen' 'ui.reviewPanelOpen'
) )
const [leftMenuShown, setLeftMenuShown] = useScopeValue('ui.leftMenuShown') const [leftMenuShown, setLeftMenuShown] = useScopeValue('ui.leftMenuShown')
const [pdfLayout] = useScopeValue('ui.pdfLayout') const [pdfLayout] = useScopeValue('ui.pdfLayout', $scope)
const value = { const value = useMemo(
view, () => ({
setView, view,
chatIsOpen, setView,
setChatIsOpen, chatIsOpen,
reviewPanelOpen, setChatIsOpen,
setReviewPanelOpen, reviewPanelOpen,
leftMenuShown, setReviewPanelOpen,
setLeftMenuShown, leftMenuShown,
pdfLayout, setLeftMenuShown,
} pdfLayout,
}),
[
chatIsOpen,
leftMenuShown,
pdfLayout,
reviewPanelOpen,
setChatIsOpen,
setLeftMenuShown,
setReviewPanelOpen,
setView,
view,
]
)
return ( return (
<LayoutContext.Provider value={value}>{children}</LayoutContext.Provider> <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>