fix: wrong component wrapping cause excessive app rerender

This commit is contained in:
Louis 2024-11-27 17:28:59 +07:00
parent 697fdc235d
commit eab19b30cf
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
19 changed files with 127 additions and 122 deletions

View File

@ -52,9 +52,8 @@ export default function RootLayout() {
<body className="font-sans antialiased">
<JotaiWrapper>
<ThemeWrapper>
<ClipboardListener>
<Search />
</ClipboardListener>
<ClipboardListener />
<Search />
</ThemeWrapper>
</JotaiWrapper>
</body>

View File

@ -1,12 +1,8 @@
import { ReactNode, useCallback, useEffect, useRef } from 'react'
import { PropsWithChildren, useCallback, useEffect, useRef } from 'react'
import { ScrollArea } from '@janhq/joi'
type Props = {
children: ReactNode
}
const ListContainer = ({ children }: Props) => {
const ListContainer = ({ children }: PropsWithChildren) => {
const listRef = useRef<HTMLDivElement>(null)
const prevScrollTop = useRef(0)
const isUserManuallyScrollingUp = useRef(false)

View File

@ -2,7 +2,6 @@ import '@testing-library/jest-dom'
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import ModelConfigInput from './index'
import { Tooltip } from '@janhq/joi'
// Mocking the Tooltip component to simplify testing
jest.mock('@janhq/joi', () => ({

View File

@ -1,4 +1,4 @@
import { Fragment, PropsWithChildren, useEffect } from 'react'
import { Fragment, useEffect } from 'react'
import { AppUpdateInfo } from '@janhq/core'
import { useSetAtom } from 'jotai'
@ -8,7 +8,7 @@ import {
updateVersionErrorAtom,
} from '@/helpers/atoms/App.atom'
const AppUpdateListener = ({ children }: PropsWithChildren) => {
const AppUpdateListener = () => {
const setProgress = useSetAtom(appDownloadProgressAtom)
const setUpdateVersionError = useSetAtom(updateVersionErrorAtom)
@ -39,7 +39,7 @@ const AppUpdateListener = ({ children }: PropsWithChildren) => {
}
}, [setProgress, setUpdateVersionError])
return <Fragment>{children}</Fragment>
return <Fragment></Fragment>
}
export default AppUpdateListener

View File

@ -1,10 +1,10 @@
import { Fragment, PropsWithChildren } from 'react'
import { Fragment } from 'react'
import { useSetAtom } from 'jotai'
import { selectedTextAtom } from './Jotai'
const ClipboardListener = ({ children }: PropsWithChildren) => {
const ClipboardListener = () => {
const setSelectedText = useSetAtom(selectedTextAtom)
if (typeof window !== 'undefined') {
@ -13,7 +13,7 @@ const ClipboardListener = ({ children }: PropsWithChildren) => {
})
}
return <Fragment>{children}</Fragment>
return <Fragment></Fragment>
}
export default ClipboardListener

View File

@ -1,13 +1,12 @@
'use client'
import { Fragment, ReactNode, useEffect } from 'react'
import { Fragment, useEffect } from 'react'
import { AppConfiguration, getUserHomePath } from '@janhq/core'
import { useSetAtom } from 'jotai'
import useAssistants from '@/hooks/useAssistants'
import useGetSystemResources from '@/hooks/useGetSystemResources'
import { useLoadTheme } from '@/hooks/useLoadTheme'
import useModels from '@/hooks/useModels'
import useThreads from '@/hooks/useThreads'
@ -20,27 +19,20 @@ import {
} from '@/helpers/atoms/AppConfig.atom'
import { janSettingScreenAtom } from '@/helpers/atoms/Setting.atom'
type Props = {
children: ReactNode
}
const DataLoader: React.FC<Props> = ({ children }) => {
const DataLoader: React.FC = () => {
const setJanDataFolderPath = useSetAtom(janDataFolderPathAtom)
const setQuickAskEnabled = useSetAtom(quickAskEnabledAtom)
const setJanDefaultDataFolder = useSetAtom(defaultJanDataFolderAtom)
const setJanSettingScreen = useSetAtom(janSettingScreenAtom)
const { loadDataModel, configurePullOptions } = useModels()
const { getData: loadModels } = useModels()
useThreads()
useAssistants()
useGetSystemResources()
useLoadTheme()
useEffect(() => {
// Load data once
loadDataModel()
// Configure pull options once
configurePullOptions()
loadModels()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -71,7 +63,7 @@ const DataLoader: React.FC<Props> = ({ children }) => {
console.debug('Load Data...')
return <Fragment>{children}</Fragment>
return <Fragment></Fragment>
}
export default DataLoader

View File

@ -1,4 +1,4 @@
import { Fragment, ReactNode } from 'react'
import { Fragment } from 'react'
import { useSetAtom } from 'jotai'
@ -13,11 +13,8 @@ import {
importHuggingFaceModelStageAtom,
importingHuggingFaceRepoDataAtom,
} from '@/helpers/atoms/HuggingFace.atom'
type Props = {
children: ReactNode
}
const DeepLinkListener: React.FC<Props> = ({ children }) => {
const DeepLinkListener: React.FC = () => {
const { getHfRepoData } = useGetHFRepoData()
const setLoadingInfo = useSetAtom(loadingModalInfoAtom)
const setImportingHuggingFaceRepoData = useSetAtom(
@ -69,7 +66,7 @@ const DeepLinkListener: React.FC<Props> = ({ children }) => {
handleDeepLinkAction(action)
})
return <Fragment>{children}</Fragment>
return <Fragment></Fragment>
}
type DeepLinkAction = {

View File

@ -1,4 +1,4 @@
import { PropsWithChildren, useCallback, useEffect } from 'react'
import { useCallback, useEffect } from 'react'
import React from 'react'
@ -23,7 +23,7 @@ import { toaster } from '../Toast'
import AppUpdateListener from './AppUpdateListener'
import ClipboardListener from './ClipboardListener'
import EventHandler from './EventHandler'
import ModelHandler from './ModelHandler'
import ModelImportListener from './ModelImportListener'
import QuickAskListener from './QuickAskListener'
@ -39,7 +39,7 @@ import {
removeDownloadingModelAtom,
} from '@/helpers/atoms/Model.atom'
const EventListenerWrapper = ({ children }: PropsWithChildren) => {
const EventListener = () => {
const setDownloadState = useSetAtom(setDownloadStateAtom)
const setInstallingExtension = useSetAtom(setInstallingExtensionAtom)
const removeInstallingExtension = useSetAtom(removeInstallingExtensionAtom)
@ -156,16 +156,14 @@ const EventListenerWrapper = ({ children }: PropsWithChildren) => {
])
return (
<AppUpdateListener>
<ClipboardListener>
<ModelImportListener>
<QuickAskListener>
<EventHandler>{children}</EventHandler>
</QuickAskListener>
</ModelImportListener>
</ClipboardListener>
</AppUpdateListener>
<>
<AppUpdateListener />
<ClipboardListener />
<ModelImportListener />
<QuickAskListener />
<ModelHandler />
</>
)
}
export default EventListenerWrapper
export default EventListener

View File

@ -1,13 +1,9 @@
'use client'
import { ReactNode } from 'react'
import { PropsWithChildren } from 'react'
import { Provider, atom } from 'jotai'
type Props = {
children: ReactNode
}
export const editPromptAtom = atom<string>('')
export const currentPromptAtom = atom<string>('')
export const fileUploadAtom = atom<FileInfo[]>([])
@ -16,7 +12,7 @@ export const searchAtom = atom<string>('')
export const selectedTextAtom = atom('')
export default function JotaiWrapper({ children }: Props) {
export default function JotaiWrapper({ children }: PropsWithChildren) {
return <Provider>{children}</Provider>
}

View File

@ -1,6 +1,6 @@
'use client'
import { Fragment, ReactNode, useEffect } from 'react'
import { Fragment, useEffect } from 'react'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
@ -22,11 +22,7 @@ import {
ThreadModalAction,
} from '@/helpers/atoms/Thread.atom'
type Props = {
children: ReactNode
}
export default function KeyListener({ children }: Props) {
export default function KeyListener() {
const setShowLeftPanel = useSetAtom(showLeftPanelAtom)
const setShowRightPanel = useSetAtom(showRightPanelAtom)
const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
@ -94,5 +90,5 @@ export default function KeyListener({ children }: Props) {
setShowRightPanel,
])
return <Fragment>{children}</Fragment>
return <Fragment></Fragment>
}

View File

@ -1,4 +1,4 @@
import { Fragment, ReactNode, useCallback, useEffect, useRef } from 'react'
import { Fragment, useCallback, useEffect, useRef } from 'react'
import {
ChatCompletionMessage,
@ -43,7 +43,7 @@ import {
const maxWordForThreadTitle = 10
const defaultThreadTitle = 'New Thread'
export default function EventHandler({ children }: { children: ReactNode }) {
export default function ModelHandler() {
const messages = useAtomValue(getCurrentChatMessagesAtom)
const addNewMessage = useSetAtom(addNewMessageAtom)
const updateMessage = useSetAtom(updateMessageAtom)
@ -333,5 +333,5 @@ export default function EventHandler({ children }: { children: ReactNode }) {
}
}, [onNewMessageResponse, onMessageResponseUpdate, onModelStopped])
return <Fragment>{children}</Fragment>
return <Fragment></Fragment>
}

View File

@ -1,4 +1,4 @@
import { Fragment, PropsWithChildren, useCallback, useEffect } from 'react'
import { Fragment, useCallback, useEffect } from 'react'
import {
ImportingModel,
@ -17,7 +17,7 @@ import {
updateImportingModelProgressAtom,
} from '@/helpers/atoms/Model.atom'
const ModelImportListener = ({ children }: PropsWithChildren) => {
const ModelImportListener = () => {
const updateImportingModelProgress = useSetAtom(
updateImportingModelProgressAtom
)
@ -103,7 +103,7 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
onImportModelFailed,
])
return <Fragment>{children}</Fragment>
return <Fragment></Fragment>
}
export default ModelImportListener

View File

@ -1,4 +1,4 @@
import { Fragment, ReactNode } from 'react'
import { Fragment } from 'react'
import { useSetAtom } from 'jotai'
@ -10,11 +10,7 @@ import useSendChatMessage from '@/hooks/useSendChatMessage'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
type Props = {
children: ReactNode
}
const QuickAskListener: React.FC<Props> = ({ children }) => {
const QuickAskListener: React.FC = () => {
const { sendChatMessage } = useSendChatMessage()
const setMainState = useSetAtom(mainViewStateAtom)
@ -27,7 +23,7 @@ const QuickAskListener: React.FC<Props> = ({ children }) => {
debounced(input)
})
return <Fragment>{children}</Fragment>
return <Fragment></Fragment>
}
export default QuickAskListener

View File

@ -1,11 +1,11 @@
import { Fragment, PropsWithChildren, useEffect, useRef } from 'react'
import { Fragment, useEffect, useRef } from 'react'
import { useMediaQuery } from '@janhq/joi'
import { useAtom } from 'jotai'
import { showLeftPanelAtom, showRightPanelAtom } from '@/helpers/atoms/App.atom'
const Responsive = ({ children }: PropsWithChildren) => {
const Responsive = () => {
const matches = useMediaQuery('(max-width: 880px)')
const [showLeftPanel, setShowLeftPanel] = useAtom(showLeftPanelAtom)
const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom)
@ -30,7 +30,7 @@ const Responsive = ({ children }: PropsWithChildren) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [matches, setShowLeftPanel, setShowRightPanel])
return <Fragment>{children}</Fragment>
return <Fragment></Fragment>
}
export default Responsive

View File

@ -0,0 +1,20 @@
'use client'
import { useEffect } from 'react'
import { useConfigurations } from '@/hooks/useConfigurations'
import { useLoadTheme } from '@/hooks/useLoadTheme'
const SettingsHandler: React.FC = () => {
useLoadTheme()
const { configurePullOptions } = useConfigurations()
useEffect(() => {
configurePullOptions()
}, [configurePullOptions])
return <></>
}
export default SettingsHandler

View File

@ -5,7 +5,7 @@ import { PropsWithChildren, useCallback, useEffect, useState } from 'react'
import { Toaster } from 'react-hot-toast'
import Loader from '@/containers/Loader'
import EventListenerWrapper from '@/containers/Providers/EventListener'
import EventListener from '@/containers/Providers/EventListener'
import JotaiWrapper from '@/containers/Providers/Jotai'
import ThemeWrapper from '@/containers/Providers/Theme'
@ -24,6 +24,8 @@ import DeepLinkListener from './DeepLinkListener'
import KeyListener from './KeyListener'
import Responsive from './Responsive'
import SettingsHandler from './SettingsHandler'
import { extensionManager } from '@/extension'
const Providers = ({ children }: PropsWithChildren) => {
@ -76,16 +78,14 @@ const Providers = ({ children }: PropsWithChildren) => {
{settingUp && <Loader description="Preparing Update..." />}
{setupCore && activated && (
<>
<Responsive>
<KeyListener>
<EventListenerWrapper>
<DataLoader>
<DeepLinkListener>{children}</DeepLinkListener>
</DataLoader>
</EventListenerWrapper>
<Toaster />
</KeyListener>
</Responsive>
<Responsive />
<KeyListener />
<EventListener />
<DataLoader />
<SettingsHandler />
<DeepLinkListener />
<Toaster />
{children}
</>
)}
</JotaiWrapper>

View File

@ -0,0 +1,41 @@
import { useCallback, useEffect } from 'react'
import { ExtensionTypeEnum, ModelExtension } from '@janhq/core'
import { useAtomValue } from 'jotai'
import { extensionManager } from '@/extension'
import {
ignoreSslAtom,
proxyAtom,
proxyEnabledAtom,
} from '@/helpers/atoms/AppConfig.atom'
export const useConfigurations = () => {
const proxyEnabled = useAtomValue(proxyEnabledAtom)
const proxyUrl = useAtomValue(proxyAtom)
const proxyIgnoreSSL = useAtomValue(ignoreSslAtom)
const configurePullOptions = useCallback(() => {
extensionManager
.get<ModelExtension>(ExtensionTypeEnum.Model)
?.configurePullOptions(
proxyEnabled
? {
proxy_url: proxyUrl,
verify_peer_ssl: !proxyIgnoreSSL,
}
: {
proxy_url: '',
verify_peer_ssl: false,
}
)
}, [proxyEnabled, proxyUrl, proxyIgnoreSSL])
useEffect(() => {
configurePullOptions()
}, [])
return {
configurePullOptions,
}
}

View File

@ -9,18 +9,14 @@ import {
ModelManager,
} from '@janhq/core'
import { useSetAtom, useAtom, useAtomValue } from 'jotai'
import { useSetAtom } from 'jotai'
import { useDebouncedCallback } from 'use-debounce'
import { isLocalEngine } from '@/utils/modelEngine'
import { extensionManager } from '@/extension'
import {
ignoreSslAtom,
proxyAtom,
proxyEnabledAtom,
} from '@/helpers/atoms/AppConfig.atom'
import {
configuredModelsAtom,
downloadedModelsAtom,
@ -32,11 +28,8 @@ import {
* and updates the atoms accordingly.
*/
const useModels = () => {
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
const setDownloadedModels = useSetAtom(downloadedModelsAtom)
const setExtensionModels = useSetAtom(configuredModelsAtom)
const proxyEnabled = useAtomValue(proxyEnabledAtom)
const proxyUrl = useAtomValue(proxyAtom)
const proxyIgnoreSSL = useAtomValue(ignoreSslAtom)
const getData = useCallback(() => {
const getDownloadedModels = async () => {
@ -87,18 +80,17 @@ const useModels = () => {
const updateStates = useCallback(() => {
const cachedModels = ModelManager.instance().models.values().toArray()
const toUpdate = [
setDownloadedModels((downloadedModels) => [
...downloadedModels,
...cachedModels.filter(
(e) =>
!isLocalEngine(e.engine) &&
!downloadedModels.some((g: Model) => g.id === e.id)
),
]
])
setDownloadedModels(toUpdate)
setExtensionModels(cachedModels)
}, [downloadedModels, setDownloadedModels, setExtensionModels])
}, [setDownloadedModels, setExtensionModels])
const getModels = async (): Promise<Model[]> =>
extensionManager
@ -118,25 +110,8 @@ const useModels = () => {
}
}, [reloadData, updateStates])
const configurePullOptions = useCallback(() => {
extensionManager
.get<ModelExtension>(ExtensionTypeEnum.Model)
?.configurePullOptions(
proxyEnabled
? {
proxy_url: proxyUrl,
verify_peer_ssl: !proxyIgnoreSSL,
}
: {
proxy_url: '',
verify_peer_ssl: false,
}
)
}, [proxyEnabled, proxyUrl, proxyIgnoreSSL])
return {
loadDataModel: getData,
configurePullOptions,
getData,
}
}

View File

@ -25,7 +25,7 @@ import { useDebouncedCallback } from 'use-debounce'
import { snackbar, toaster } from '@/containers/Toast'
import { useActiveModel } from '@/hooks/useActiveModel'
import useModels from '@/hooks/useModels'
import { useConfigurations } from '@/hooks/useConfigurations'
import { useSettings } from '@/hooks/useSettings'
import DataFolder from './DataFolder'
@ -68,7 +68,7 @@ const Advanced = () => {
const [dropdownOptions, setDropdownOptions] = useState<HTMLDivElement | null>(
null
)
const { configurePullOptions } = useModels()
const { configurePullOptions } = useConfigurations()
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)