Skip to content

refactor: Replace StateContextProvider with Zustand for state management (resolves #168, resolves #211). #224

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

Merged
merged 58 commits into from
May 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
47ffe61
introduce Zustand, code fails
Henry8192 Mar 31, 2025
9623f99
refactor: Rename exportStore to useLogExportStore, fix not working
Henry8192 Apr 1, 2025
dec8596
fix: Correct export progress check to prevent button disablement duri…
Henry8192 Apr 1, 2025
1dff9fe
refactor log file variables to logFileStore
Henry8192 Apr 1, 2025
e865326
add mainWorkerStore
Henry8192 Apr 7, 2025
1bcb881
add queryStore
Henry8192 Apr 9, 2025
0e51fb5
add query parameter setters, update QueryInputBox
Henry8192 Apr 10, 2025
40ce59b
clean up code, refactor loadPageByAction, add uiStore
Henry8192 Apr 13, 2025
35b341f
fix query result not reactive
Henry8192 Apr 13, 2025
0a98905
revert deletion of useRef
Henry8192 Apr 14, 2025
69b0892
cleanup StateContext usage
Henry8192 Apr 14, 2025
2b06efd
fix not clearing previous query results
Henry8192 Apr 14, 2025
30487c6
fix zustand internal store usage syntax, refactor activeTabName to lo…
Henry8192 Apr 14, 2025
c7cd652
need to implement format popup and notification worker response case
Henry8192 Apr 14, 2025
7f4343d
refactor SidebarTabs to use logFileStore and clean up imports
Henry8192 Apr 14, 2025
87b2da3
implement postPopUp, notification; clean up context usage
Henry8192 Apr 14, 2025
f1d67f7
create setters for log file state and clean up unused code in state c…
Henry8192 Apr 15, 2025
e7b7481
refactor logFileStore
Henry8192 Apr 17, 2025
b7b4fef
enhance error messages in MainWorker; trigger startQuery after filter…
Henry8192 Apr 17, 2025
9f6c256
update condition in StateContextProvider to handle loadPage before lo…
Henry8192 Apr 17, 2025
5874893
Merge branch 'main' into zustand
Henry8192 Apr 21, 2025
ebb664c
complete merging prettify button. however ui now have scroll bars
Henry8192 Apr 21, 2025
af78846
fix lint; change `clearQueryResults()` to `clearQuery()` because we a…
Henry8192 Apr 22, 2025
d99deb6
change export format for logFileStores
Henry8192 Apr 22, 2025
b7d6bdf
edit `clearQuery()` definition and add back `clearQueryResults()`; sy…
Henry8192 Apr 24, 2025
f61cc09
exportLogs error logs fix
Henry8192 Apr 27, 2025
fd77cff
migrate activeTabName state management to useUiStore, resolve other s…
Henry8192 Apr 28, 2025
0d5e948
refactor logFileStore
Henry8192 Apr 29, 2025
75d203a
rename pageStore to viewStore
Henry8192 Apr 30, 2025
572b319
refactor `logEventNum` & `postPopUp()` from logFileStore to contextSt…
Henry8192 Apr 30, 2025
d59dd4a
refactor: Replace service worker communication with RPC. (#3)
hoophalab May 1, 2025
50d3f27
slice queryStore
Henry8192 May 4, 2025
d3b52dc
Update src/contexts/states/LogFileManagerStore.ts
hoophalab May 5, 2025
589d246
refactor: Rename `WrappedLogFileManager` as `LogFileManagerProxy`.
hoophalab May 5, 2025
351757d
update query slice types to use specific value interfaces
Henry8192 May 5, 2025
03552b6
Merge branch 'y-scope:main' into zustand
Henry8192 May 5, 2025
c085441
fix: Address coderabbit's comments.
hoophalab May 5, 2025
81e7d2e
slice zustand stores by values & actions
Henry8192 May 7, 2025
1443bb8
revert UrlContextProvider.tsx
Henry8192 May 7, 2025
8d816d0
fix: Post a popup when opening structured logs without setting a form…
hoophalab May 8, 2025
cec35ea
fix coderabbit comments.
hoophalab May 8, 2025
ff16262
address suggestions from code review
Henry8192 May 8, 2025
68f9325
extract `loadFile()` helper functions to the outside; reorder variabl…
Henry8192 May 8, 2025
4b8c532
resolve suggestions
Henry8192 May 9, 2025
3f065a5
add a new line
Henry8192 May 9, 2025
b0135b3
create const pop-up format message.
Henry8192 May 9, 2025
301ce91
address review suggestions
Henry8192 May 9, 2025
84be9b7
fix comments
hoophalab May 10, 2025
8b2c8f6
update zustand to version 5.0.4 and move comlink from devDependencies…
Henry8192 May 12, 2025
2cc29f6
update viewStore.ts
Henry8192 May 12, 2025
0bd4385
rename StateContextProvider to AppController; rename zustand states f…
Henry8192 May 12, 2025
02f929d
fix comment
Henry8192 May 12, 2025
4389632
address comments
Henry8192 May 12, 2025
46471ed
Update worker location
Henry8192 May 12, 2025
6695360
Apply suggestions from code review
Henry8192 May 12, 2025
d5e5b55
delete trailing space
Henry8192 May 12, 2025
72650b9
Apply suggestions from code review
Henry8192 May 12, 2025
1b49af7
fix lint
Henry8192 May 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@
"@mui/joy": "^5.0.0-beta.51",
"axios": "^1.8.2",
"clp-ffi-js": "^0.5.0",
"comlink": "^4.4.2",
"dayjs": "^1.11.13",
"js-beautify": "^1.15.4",
"monaco-editor": "0.50.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"zustand": "^5.0.4"
},
"devDependencies": {
"@babel/core": "^7.26.0",
Expand Down
6 changes: 3 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Layout from "./components/Layout";
import AppController from "./contexts/AppController";
import NotificationContextProvider from "./contexts/NotificationContextProvider";
import StateContextProvider from "./contexts/StateContextProvider";
import UrlContextProvider from "./contexts/UrlContextProvider";


Expand All @@ -13,9 +13,9 @@ const App = () => {
return (
<NotificationContextProvider>
<UrlContextProvider>
<StateContextProvider>
<AppController>
<Layout/>
</StateContextProvider>
</AppController>
</UrlContextProvider>
</NotificationContextProvider>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
useContext,
useMemo,
} from "react";
import {useMemo} from "react";

import {
Divider,
Expand All @@ -11,7 +8,7 @@ import {
import AbcIcon from "@mui/icons-material/Abc";
import StorageIcon from "@mui/icons-material/Storage";

import {StateContext} from "../../../../contexts/StateContextProvider";
import useLogFileStore from "../../../../stores/logFileStore";
import {
TAB_DISPLAY_NAMES,
TAB_NAME,
Expand All @@ -27,7 +24,8 @@ import CustomTabPanel from "./CustomTabPanel";
* @return
*/
const FileInfoTabPanel = () => {
const {fileName, onDiskFileSizeInBytes} = useContext(StateContext);
const fileName = useLogFileStore((state) => state.fileName);
const onDiskFileSizeInBytes = useLogFileStore((state) => state.onDiskFileSizeInBytes);

const isFileUnloaded = 0 === fileName.length;
const formattedOnDiskSize = useMemo(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import React, {
useContext,
useState,
} from "react";
import React from "react";

import {
LinearProgress,
Stack,
Textarea,
} from "@mui/joy";

import {StateContext} from "../../../../../contexts/StateContextProvider";
import {
QUERY_PROGRESS_VALUE_MAX,
QueryArgs,
} from "../../../../../typings/query";
import useQueryStore from "../../../../../stores/queryStore";
import useUiStore from "../../../../../stores/uiStore";
import {QUERY_PROGRESS_VALUE_MAX} from "../../../../../typings/query";
import {UI_ELEMENT} from "../../../../../typings/states";
import {isDisabled} from "../../../../../utils/states";
import ToggleIconButton from "./ToggleIconButton";
Expand All @@ -27,34 +22,29 @@ import "./QueryInputBox.css";
* @return
*/
const QueryInputBox = () => {
const {queryProgress, startQuery, uiState} = useContext(StateContext);

const [queryString, setQueryString] = useState<string>("");
const [isCaseSensitive, setIsCaseSensitive] = useState<boolean>(false);
const [isRegex, setIsRegex] = useState<boolean>(false);

const handleQuerySubmit = (newArgs: Partial<QueryArgs>) => {
startQuery({
isCaseSensitive: isCaseSensitive,
isRegex: isRegex,
queryString: queryString,
...newArgs,
});
};
const isCaseSensitive = useQueryStore((state) => state.queryIsCaseSensitive);
const isRegex = useQueryStore((state) => state.queryIsRegex);
const querystring = useQueryStore((state) => state.queryString);
const setQueryIsCaseSensitive = useQueryStore((state) => state.setQueryIsCaseSensitive);
const setQueryIsRegex = useQueryStore((state) => state.setQueryIsRegex);
const setQueryString = useQueryStore((state) => state.setQueryString);
const queryProgress = useQueryStore((state) => state.queryProgress);
const startQuery = useQueryStore((state) => state.startQuery);
const uiState = useUiStore((state) => state.uiState);
Comment on lines 24 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider selector batching to avoid unnecessary re-renders

Each useQueryStore / useUiStore call subscribes the component to a separate slice of state. Whenever any of those slices changes the component re-renders, which – with eight selectors – can become chatty.

Two small tweaks improve performance without losing readability:

-const isCaseSensitive   = useQueryStore((s) => s.queryIsCaseSensitive);
-const isRegex           = useQueryStore((s) => s.queryIsRegex);
-const querystring       = useQueryStore((s) => s.queryString);
-const setQueryIsCaseSensitive = useQueryStore((s) => s.setQueryIsCaseSensitive);
-const setQueryIsRegex   = useQueryStore((s) => s.setQueryIsRegex);
-const setQueryString    = useQueryStore((s) => s.setQueryString);
-const queryProgress     = useQueryStore((s) => s.queryProgress);
-const startQuery        = useQueryStore((s) => s.startQuery);
+const {
+  queryIsCaseSensitive : isCaseSensitive,
+  queryIsRegex         : isRegex,
+  queryString          : querystring,
+  setQueryIsCaseSensitive,
+  setQueryIsRegex,
+  setQueryString,
+  queryProgress,
+  startQuery,
+} = useQueryStore((s) => ({
+  queryIsCaseSensitive : s.queryIsCaseSensitive,
+  queryIsRegex         : s.queryIsRegex,
+  queryString          : s.queryString,
+  setQueryIsCaseSensitive : s.setQueryIsCaseSensitive,
+  setQueryIsRegex         : s.setQueryIsRegex,
+  setQueryString          : s.setQueryString,
+  queryProgress           : s.queryProgress,
+  startQuery              : s.startQuery,
+}), shallow);

Advantages
• One subscription instead of eight.
• With shallow equality the component only re-renders when one of the selected values actually changes.

You can pull shallow from "zustand/shallow".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const QueryInputBox = () => {
const {queryProgress, startQuery, uiState} = useContext(StateContext);
const [queryString, setQueryString] = useState<string>("");
const [isCaseSensitive, setIsCaseSensitive] = useState<boolean>(false);
const [isRegex, setIsRegex] = useState<boolean>(false);
const handleQuerySubmit = (newArgs: Partial<QueryArgs>) => {
startQuery({
isCaseSensitive: isCaseSensitive,
isRegex: isRegex,
queryString: queryString,
...newArgs,
});
};
const isCaseSensitive = useQueryStore((state) => state.queryIsCaseSensitive);
const isRegex = useQueryStore((state) => state.queryIsRegex);
const querystring = useQueryStore((state) => state.queryString);
const setQueryIsCaseSensitive = useQueryStore((state) => state.setQueryIsCaseSensitive);
const setQueryIsRegex = useQueryStore((state) => state.setQueryIsRegex);
const setQueryString = useQueryStore((state) => state.setQueryString);
const queryProgress = useQueryStore((state) => state.queryProgress);
const startQuery = useQueryStore((state) => state.startQuery);
const uiState = useUiStore((state) => state.uiState);
const QueryInputBox = () => {
const {
queryIsCaseSensitive: isCaseSensitive,
queryIsRegex: isRegex,
queryString: querystring,
setQueryIsCaseSensitive,
setQueryIsRegex,
setQueryString,
queryProgress,
startQuery,
} = useQueryStore(
(s) => ({
queryIsCaseSensitive: s.queryIsCaseSensitive,
queryIsRegex: s.queryIsRegex,
queryString: s.queryString,
setQueryIsCaseSensitive: s.setQueryIsCaseSensitive,
setQueryIsRegex: s.setQueryIsRegex,
setQueryString: s.setQueryString,
queryProgress: s.queryProgress,
startQuery: s.startQuery,
}),
shallow,
);
const uiState = useUiStore((state) => state.uiState);
// …rest of component
}


const handleQueryInputChange = (ev: React.ChangeEvent<HTMLTextAreaElement>) => {
setQueryString(ev.target.value);
handleQuerySubmit({queryString: ev.target.value});
startQuery();
};
Comment on lines 35 to 38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Running a query on every keystroke may exhaust the worker

handleQueryInputChange invokes startQuery() on every text change, which can fire dozens of expensive worker calls per second on fast typing. Debouncing (e.g. 300 ms) or requiring Enter would markedly reduce load and still feel responsive.

Would you like a sample debounce implementation?


const handleCaseSensitivityButtonClick = () => {
handleQuerySubmit({isCaseSensitive: !isCaseSensitive});
setIsCaseSensitive(!isCaseSensitive);
setQueryIsCaseSensitive(!isCaseSensitive);
startQuery();
};

const handleRegexButtonClick = () => {
handleQuerySubmit({isRegex: !isRegex});
setIsRegex(!isRegex);
setQueryIsRegex(!isRegex);
startQuery();
};
Comment on lines 40 to 48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Follow project guideline: avoid !boolean in favour of false == boolean

The coding-guidelines section for *.tsx asks for false == <expression> instead of logical negation. These two lines violate the rule:

setQueryIsCaseSensitive(!isCaseSensitive);
...
setQueryIsRegex(!isRegex);

Suggested fix:

-setQueryIsCaseSensitive(!isCaseSensitive);
+setQueryIsCaseSensitive(false == isCaseSensitive);

-setQueryIsRegex(!isRegex);
+setQueryIsRegex(false == isRegex);

This also keeps the style consistent across the codebase.


const isQueryInputBoxDisabled = isDisabled(uiState, UI_ELEMENT.QUERY_INPUT_BOX);
Expand All @@ -66,6 +56,7 @@ const QueryInputBox = () => {
maxRows={7}
placeholder={"Search"}
size={"sm"}
value={querystring}
endDecorator={
<Stack
direction={"row"}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
useContext,
useState,
} from "react";
import {useState} from "react";

import {
AccordionGroup,
Expand All @@ -11,7 +8,7 @@ import {
import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";

import {StateContext} from "../../../../../contexts/StateContextProvider";
import useQueryStore from "../../../../../stores/queryStore";
import {
TAB_DISPLAY_NAMES,
TAB_NAME,
Expand All @@ -30,7 +27,7 @@ import "./index.css";
* @return
*/
const SearchTabPanel = () => {
const {queryResults} = useContext(StateContext);
const queryResults = useQueryStore((state) => state.queryResults);

const [isAllExpanded, setIsAllExpanded] = useState<boolean>(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "@mui/joy";

import {NotificationContext} from "../../../../../contexts/NotificationContextProvider";
import {StateContext} from "../../../../../contexts/StateContextProvider";
import useViewStore from "../../../../../stores/viewStore";
import {Nullable} from "../../../../../typings/common";
import {
CONFIG_KEY,
Expand Down Expand Up @@ -108,7 +108,7 @@ const handleConfigFormReset = (ev: React.FormEvent) => {
*/
const SettingsTabPanel = () => {
const {postPopUp} = useContext(NotificationContext);
const {loadPageByAction} = useContext(StateContext);
const loadPageByAction = useViewStore((state) => state.loadPageByAction);

const handleConfigFormSubmit = useCallback((ev: React.FormEvent) => {
ev.preventDefault();
Expand Down
15 changes: 5 additions & 10 deletions src/components/CentralContainer/Sidebar/SidebarTabs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
Ref,
useContext,
} from "react";
import {Ref} from "react";

import {
TabList,
Expand All @@ -14,7 +11,7 @@ import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import SearchIcon from "@mui/icons-material/Search";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";

import {StateContext} from "../../../../contexts/StateContextProvider";
import useUiStore from "../../../../stores/uiStore";
import {TAB_NAME} from "../../../../typings/tab";
import {openInNewTab} from "../../../../utils/url";
import FileInfoTabPanel from "./FileInfoTabPanel";
Expand Down Expand Up @@ -49,11 +46,9 @@ interface SidebarTabsProps {
* @param props.ref Reference object used to access the TabList DOM element.
* @return
*/
const SidebarTabs = ({
ref,
}: SidebarTabsProps) => {
const {activeTabName, setActiveTabName} = useContext(StateContext);

const SidebarTabs = ({ref}: SidebarTabsProps) => {
const activeTabName = useUiStore((state) => state.activeTabName);
const setActiveTabName = useUiStore((state) => state.setActiveTabName);
const handleTabButtonClick = (tabName: TAB_NAME) => {
switch (tabName) {
case TAB_NAME.DOCUMENTATION:
Expand Down
6 changes: 3 additions & 3 deletions src/components/CentralContainer/Sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import {
useCallback,
useContext,
useEffect,
useRef,
} from "react";

import {StateContext} from "../../../contexts/StateContextProvider";
import useUiStore from "../../../stores/uiStore";
import {CONFIG_KEY} from "../../../typings/config";
import {TAB_NAME} from "../../../typings/tab";
import {setConfig} from "../../../utils/config";
Expand Down Expand Up @@ -46,7 +45,8 @@ const setPanelWidth = (newValue: number) => {
* @return
*/
const Sidebar = () => {
const {activeTabName, setActiveTabName} = useContext(StateContext);
const activeTabName = useUiStore((state) => state.activeTabName);
const setActiveTabName = useUiStore((state) => state.setActiveTabName);
const tabListRef = useRef<HTMLDivElement>(null);

const handleResizeHandleRelease = useCallback(() => {
Expand Down
11 changes: 5 additions & 6 deletions src/components/DropFileContainer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, {
useContext,
useState,
} from "react";
import React, {useState} from "react";

import {StateContext} from "../../contexts/StateContextProvider";
import useLogFileStore from "../../stores/logFileStore";
import useUiStore from "../../stores/uiStore";
import {UI_ELEMENT} from "../../typings/states";
import {CURSOR_CODE} from "../../typings/worker";
import {isDisabled} from "../../utils/states";
Expand All @@ -23,7 +21,8 @@ interface DropFileContextProviderProps {
* @return
*/
const DropFileContainer = ({children}: DropFileContextProviderProps) => {
const {loadFile, uiState} = useContext(StateContext);
const loadFile = useLogFileStore((state) => state.loadFile);
const uiState = useUiStore((state) => state.uiState);
const [isFileHovering, setIsFileHovering] = useState(false);
const disabled = isDisabled(uiState, UI_ELEMENT.DRAG_AND_DROP);

Expand Down
6 changes: 4 additions & 2 deletions src/components/Editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import {
import {useColorScheme} from "@mui/joy";
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";

import {StateContext} from "../../contexts/StateContextProvider";
import {
updateWindowUrlHashParams,
UrlContext,
} from "../../contexts/UrlContextProvider";
import useViewStore from "../../stores/viewStore";
import {Nullable} from "../../typings/common";
import {
CONFIG_KEY,
Expand Down Expand Up @@ -137,7 +137,9 @@ const handleWordWrapAction = (editor: monaco.editor.IStandaloneCodeEditor) => {
const Editor = () => {
const {mode, systemMode} = useColorScheme();

const {beginLineNumToLogEventNum, logData, loadPageByAction} = useContext(StateContext);
const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum);
const logData = useViewStore((state) => state.logData);
const loadPageByAction = useViewStore((state) => state.loadPageByAction);
const {isPrettified, logEventNum} = useContext(UrlContext);

const [lineNum, setLineNum] = useState<number>(1);
Expand Down
Loading