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-21 11:02:38 +01:00 committed by Copybot
parent d64172cfff
commit eebeffc1c5
11 changed files with 216 additions and 157 deletions

View file

@ -13,7 +13,7 @@ import withErrorBoundary from '../../../infrastructure/error-boundary'
import { FetchError } from '../../../infrastructure/fetch-json'
import { useChatContext } from '../context/chat-context'
function ChatPane() {
const ChatPane = React.memo(function ChatPane() {
const { t } = useTranslation()
const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool })
@ -81,7 +81,7 @@ function ChatPane() {
/>
</aside>
)
}
})
function LoadingSpinner() {
const { t } = useTranslation()

View file

@ -264,19 +264,34 @@ export function ChatProvider({ children }) {
markMessagesAsRead,
])
const value = {
status: state.status,
messages: state.messages,
initialMessagesLoaded: state.initialMessagesLoaded,
atEnd: state.atEnd,
unreadMessageCount: state.unreadMessageCount,
loadInitialMessages,
loadMoreMessages,
reset,
sendMessage,
markMessagesAsRead,
error: state.error,
}
const value = useMemo(
() => ({
status: state.status,
messages: state.messages,
initialMessagesLoaded: state.initialMessagesLoaded,
atEnd: state.atEnd,
unreadMessageCount: state.unreadMessageCount,
loadInitialMessages,
loadMoreMessages,
reset,
sendMessage,
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>
}

View file

@ -35,99 +35,104 @@ const chatContextPropTypes = {
unreadMessageCount: PropTypes.number.isRequired,
}
function EditorNavigationToolbarRoot({
onlineUsersArray,
openDoc,
openShareProjectModal,
}) {
const { user } = useApplicationContext(applicationContextPropTypes)
const EditorNavigationToolbarRoot = React.memo(
function EditorNavigationToolbarRoot({
onlineUsersArray,
openDoc,
openShareProjectModal,
}) {
const { user } = useApplicationContext(applicationContextPropTypes)
const {
cobranding,
loading,
isRestrictedTokenMember,
projectName,
renameProject,
isProjectOwner,
} = useEditorContext(editorContextPropTypes)
const {
cobranding,
loading,
isRestrictedTokenMember,
projectName,
renameProject,
isProjectOwner,
} = useEditorContext(editorContextPropTypes)
const {
chatIsOpen,
setChatIsOpen,
reviewPanelOpen,
setReviewPanelOpen,
view,
setView,
setLeftMenuShown,
pdfLayout,
} = useLayoutContext(layoutContextPropTypes)
const {
chatIsOpen,
setChatIsOpen,
reviewPanelOpen,
setReviewPanelOpen,
view,
setView,
setLeftMenuShown,
pdfLayout,
} = useLayoutContext(layoutContextPropTypes)
const { markMessagesAsRead, unreadMessageCount } = useChatContext(
chatContextPropTypes
)
const { markMessagesAsRead, unreadMessageCount } = useChatContext(
chatContextPropTypes
)
const toggleChatOpen = useCallback(() => {
if (!chatIsOpen) {
markMessagesAsRead()
}
setChatIsOpen(value => !value)
}, [chatIsOpen, setChatIsOpen, markMessagesAsRead])
const toggleChatOpen = useCallback(() => {
if (!chatIsOpen) {
markMessagesAsRead()
}
setChatIsOpen(value => !value)
}, [chatIsOpen, setChatIsOpen, markMessagesAsRead])
const toggleReviewPanelOpen = useCallback(
() => setReviewPanelOpen(value => !value),
[setReviewPanelOpen]
)
const toggleReviewPanelOpen = useCallback(
() => setReviewPanelOpen(value => !value),
[setReviewPanelOpen]
)
const toggleHistoryOpen = useCallback(() => {
setView(view === 'history' ? 'editor' : 'history')
}, [view, setView])
const toggleHistoryOpen = useCallback(() => {
setView(view === 'history' ? 'editor' : 'history')
}, [view, setView])
const togglePdfView = useCallback(() => {
setView(view === 'pdf' ? 'editor' : 'pdf')
}, [view, setView])
const togglePdfView = useCallback(() => {
setView(view === 'pdf' ? 'editor' : 'pdf')
}, [view, setView])
const openShareModal = useCallback(() => {
openShareProjectModal(isProjectOwner)
}, [openShareProjectModal, isProjectOwner])
const openShareModal = useCallback(() => {
openShareProjectModal(isProjectOwner)
}, [openShareProjectModal, isProjectOwner])
const onShowLeftMenuClick = useCallback(
() => setLeftMenuShown(value => !value),
[setLeftMenuShown]
)
const onShowLeftMenuClick = useCallback(
() => setLeftMenuShown(value => !value),
[setLeftMenuShown]
)
function goToUser(user) {
if (user.doc && typeof user.row === 'number') {
openDoc(user.doc, { gotoLine: user.row + 1 })
}
const goToUser = useCallback(
user => {
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 = {
onlineUsersArray: PropTypes.array.isRequired,

View file

@ -15,7 +15,7 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.mac
const [publishModalModules] = importOverleafModules('publishModal')
const PublishButton = publishModalModules?.import.default
function ToolbarHeader({
const ToolbarHeader = React.memo(function ToolbarHeader({
cobranding,
onShowLeftMenuClick,
chatIsOpen,
@ -90,7 +90,7 @@ function ToolbarHeader({
</div>
</header>
)
}
})
ToolbarHeader.propTypes = {
onShowLeftMenuClick: PropTypes.func.isRequired,

View file

@ -18,7 +18,7 @@ import { useDroppable } from '../contexts/file-tree-draggable'
import { useFileTreeSocketListener } from '../hooks/file-tree-socket-listener'
import FileTreeModalCreateFile from './modals/file-tree-modal-create-file'
function FileTreeRoot({
const FileTreeRoot = React.memo(function FileTreeRoot({
projectId,
rootFolder,
rootDocId,
@ -64,7 +64,7 @@ function FileTreeRoot({
<FileTreeModalError />
</FileTreeContext>
)
}
})
function FileTreeRootFolder() {
useFileTreeSocketListener()

View file

@ -9,7 +9,7 @@ import localStorage from '../../../infrastructure/local-storage'
import withErrorBoundary from '../../../infrastructure/error-boundary'
import { useEditorContext } from '../../../shared/context/editor-context'
function OutlinePane({
const OutlinePane = React.memo(function OutlinePane({
isTexFile,
outline,
jumpToLine,
@ -73,7 +73,7 @@ function OutlinePane({
) : null}
</div>
)
}
})
OutlinePane.propTypes = {
isTexFile: PropTypes.bool.isRequired,

View file

@ -5,7 +5,7 @@ import PreviewLogsPane from './preview-logs-pane'
import PreviewFirstErrorPopUp from './preview-first-error-pop-up'
import { useTranslation } from 'react-i18next'
function PreviewPane({
const PreviewPane = React.memo(function PreviewPane({
compilerState,
onClearCache,
onRecompile,
@ -139,7 +139,7 @@ function PreviewPane({
) : null}
</>
)
}
})
PreviewPane.propTypes = {
compilerState: PropTypes.shape({

View file

@ -79,7 +79,7 @@ export function useProjectContext() {
return context
}
export default function ShareProjectModal({
const ShareProjectModal = React.memo(function ShareProjectModal({
handleHide,
show,
animation = true,
@ -161,10 +161,12 @@ export default function ShareProjectModal({
</ProjectContext.Provider>
</ShareProjectContext.Provider>
)
}
})
ShareProjectModal.propTypes = {
animation: PropTypes.bool,
handleHide: PropTypes.func.isRequired,
isAdmin: PropTypes.bool.isRequired,
show: PropTypes.bool.isRequired,
}
export default ShareProjectModal

View file

@ -1,4 +1,4 @@
import React, { createContext, useContext } from 'react'
import React, { createContext, useContext, useMemo } from 'react'
import PropTypes from 'prop-types'
export const ApplicationContext = createContext()
@ -15,16 +15,20 @@ ApplicationContext.Provider.propTypes = {
}
export function ApplicationProvider({ children }) {
const applicationContextValue = {
gitBridgePublicBaseUrl: window.gitBridgePublicBaseUrl,
}
const value = useMemo(() => {
const value = {
gitBridgePublicBaseUrl: window.gitBridgePublicBaseUrl,
}
if (window.user.id) {
applicationContextValue.user = window.user
}
if (window.user.id) {
value.user = window.user
}
return value
}, [])
return (
<ApplicationContext.Provider value={applicationContextValue}>
<ApplicationContext.Provider value={value}>
{children}
</ApplicationContext.Provider>
)

View file

@ -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 useScopeValue from './util/scope-value-hook'
import useBrowserWindow from '../hooks/use-browser-window'
@ -34,19 +40,23 @@ EditorContext.Provider.propTypes = {
export function EditorProvider({ children, settings }) {
const ide = useIdeContext()
const cobranding = window.brandVariation
? {
logoImgUrl: window.brandVariation.logo_url,
brandVariationName: window.brandVariation.name,
brandVariationId: window.brandVariation.id,
brandId: window.brandVariation.brand_id,
brandVariationHomeUrl: window.brandVariation.home_url,
publishGuideHtml: window.brandVariation.publish_guide_html,
partner: window.brandVariation.partner,
brandedMenu: window.brandVariation.branded_menu,
submitBtnHtml: window.brandVariation.submit_button_html,
}
: undefined
const cobranding = useMemo(
() =>
window.brandVariation
? {
logoImgUrl: window.brandVariation.logo_url,
brandVariationName: window.brandVariation.name,
brandVariationId: window.brandVariation.id,
brandId: window.brandVariation.brand_id,
brandVariationHomeUrl: window.brandVariation.home_url,
publishGuideHtml: window.brandVariation.publish_guide_html,
partner: window.brandVariation.partner,
brandedMenu: window.brandVariation.branded_menu,
submitBtnHtml: window.brandVariation.submit_button_html,
}
: undefined,
[]
)
const [loading] = useScopeValue('state.loading')
const [projectRootDocId] = useScopeValue('project.rootDoc_id')
@ -87,23 +97,33 @@ export function EditorProvider({ children, settings }) {
)
}, [projectName, setTitle])
const editorContextValue = {
cobranding,
hasPremiumCompile: compileGroup === 'priority',
loading,
projectId: window.project_id,
projectRootDocId,
projectName: projectName || '', // initially might be empty in Angular
renameProject,
isProjectOwner: ownerId === window.user.id,
isRestrictedTokenMember: window.isRestrictedTokenMember,
rootFolder,
}
const value = useMemo(
() => ({
cobranding,
hasPremiumCompile: compileGroup === 'priority',
loading,
projectId: window.project_id,
projectRootDocId,
projectName: projectName || '', // initially might be empty in Angular
renameProject,
isProjectOwner: ownerId === window.user.id,
isRestrictedTokenMember: window.isRestrictedTokenMember,
rootFolder,
}),
[
cobranding,
compileGroup,
loading,
ownerId,
projectName,
projectRootDocId,
renameProject,
rootFolder,
]
)
return (
<EditorContext.Provider value={editorContextValue}>
{children}
</EditorContext.Provider>
<EditorContext.Provider value={value}>{children}</EditorContext.Provider>
)
}

View file

@ -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 useScopeValue from './util/scope-value-hook'
import { useIdeContext } from './ide-context'
@ -43,19 +43,32 @@ export function LayoutProvider({ children }) {
'ui.reviewPanelOpen'
)
const [leftMenuShown, setLeftMenuShown] = useScopeValue('ui.leftMenuShown')
const [pdfLayout] = useScopeValue('ui.pdfLayout')
const [pdfLayout] = useScopeValue('ui.pdfLayout', $scope)
const value = {
view,
setView,
chatIsOpen,
setChatIsOpen,
reviewPanelOpen,
setReviewPanelOpen,
leftMenuShown,
setLeftMenuShown,
pdfLayout,
}
const value = useMemo(
() => ({
view,
setView,
chatIsOpen,
setChatIsOpen,
reviewPanelOpen,
setReviewPanelOpen,
leftMenuShown,
setLeftMenuShown,
pdfLayout,
}),
[
chatIsOpen,
leftMenuShown,
pdfLayout,
reviewPanelOpen,
setChatIsOpen,
setLeftMenuShown,
setReviewPanelOpen,
setView,
view,
]
)
return (
<LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>