-
Notifications
You must be signed in to change notification settings - Fork 18
feat(settings): simplify hook, split meta and LS cases #3111
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
Conversation
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.
Pull Request Overview
This PR refactors the settings synchronization architecture by moving the sync logic from useEffect hooks in useSetting.ts to RTK Query's onQueryStarted lifecycle hooks in the API layer. The main goal is to centralize settings synchronization between localStorage, Redux store, and the meta settings API.
Key Changes:
- Consolidates all settings sync logic into the
settingsApiendpoints'onQueryStartedhooks - Simplifies
useSettinghook by removing multipleuseEffectdependencies - Handles localStorage and API settings synchronization at the API layer instead of component layer
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/store/reducers/settings/utils.ts |
Adds helper functions getSettingDefault and shouldSyncSettingToLS to support centralized settings sync logic |
src/store/reducers/settings/useSetting.ts |
Removes all useEffect-based sync logic and simplifies hook to only trigger API queries and mutations |
src/store/reducers/settings/constants.ts |
Updates SETTINGS_OPTIONS type to explicitly allow undefined values for better type safety |
src/store/reducers/settings/api.ts |
Implements comprehensive sync logic in onQueryStarted hooks for both query and mutation endpoints, handling localStorage and store updates |
src/services/api/metaSettings.ts |
Updates return type to allow undefined for settings that don't exist in the backend |
|
@greptile-review |
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.
8 files reviewed, 2 comments
src/store/reducers/settings/api.ts
Outdated
|
|
||
| return {data}; | ||
| } catch (error) { | ||
| console.error('Cannot get settings:', error); |
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.
Can it be that error is "Cannot get setting" - but the real situation is that one of patches failed ?
|
Now as I see we have three sources of truth:
And we struggle to keep all of them in sync |
Yes, everything is to keep them in sync |
Is it a goal or temporary state? |
It's a goal |
src/store/reducers/settings/api.ts
Outdated
| const newValue = data ?? {name, user}; | ||
|
|
||
| // Try to sync local value if there is no backend value | ||
| syncLocalValueToMetaIfNoData(newValue, dispatch); |
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.
is it possible that null/undefined on backend is intentional?
i mean we removed some setting for example
but LS has some old value (in another browser for example) and pushes this setting to backend?
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.
It's a current limitation, yes. We cannot have null or undefined as backend value, there should be at least something, otherwise defined localStorage value will be loaded to backend. Without this it's unclear, when we should sync local value to backend
src/store/reducers/settings/api.ts
Outdated
| return; | ||
| } | ||
|
|
||
| const localValue = localStorage.getItem(params.name); |
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.
we use localStorage directly here but there are already functions for this like
readSettingValueFromLS / shouldSyncSettingToLS
these functions as I may suppose were created to make work with LS somehow guarded
I thought this over again and came to a conclusion that actually there is one source of truth (backend) while LS and rtq storage are used as caches for interface rensponsiveness so actually its not "three sources of truth" but "one with two caches" Does it make sense? |
In general, you are right. But it depends on app installation. There are 3 cases.
So, preload data from LS to store, load actual value from meta when request is finished.
In all cases redux is used as sync cache for data. |
|
So there are two main scenarios with mainly opposite mechanics
It appears we try to handle both at once with one handler and therefore it looks a bit complex I'm trying to imagine if we could explicitly split these scenarios for clarity and maintainability its possible to postpone this and separate to some tech issue but would be great if we could handle it at once |
29a9e3d to
759ba53
Compare
| isFetching={isFetching} | ||
| isLoading={isLoading} |
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.
Table loading changes are needed to synchronize settings and data loading states, without it tests somehow fail to find table container
| proxyMeta: undefined | boolean; | ||
| // this setting allows to use schema object path relative to database in api requests | ||
| useRelativePath: undefined | boolean; | ||
| useMetaSettings: undefined | boolean; |
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.
Moved useMetaSettings from uiFactory to api setup. The reason is that uiFactory property requires api and uiFactory to be inited in the specific order: first we need to configureUIFactory and only after setup api. Otherwise we need two settings - for uiFactory and to api.
So this help to reduce it to only one setting without any additional requirements
| isAuthenticated: true, | ||
| user: undefined, | ||
| id: undefined, | ||
| metaUser: undefined, |
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.
I did not figured out how to properly receive metaUser value yet, so I decided to test everything with some hardcoded user and to do all metaUser code separately - there are additional user points in issue checklist.
What are the problems with metaUser:
- The logic may be different in different installations
- It should not break current authentication cases
- We need somehow to tackle the case, when user do not have access to meta cluster
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.
17 files reviewed, 2 comments
2f7f3b5 to
d7dd85e
Compare
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.
17 files reviewed, 3 comments
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.
17 files reviewed, 1 comment
| import {useTypedSelector} from './useTypedSelector'; | ||
|
|
||
| export const useSetting = <T>(key: string, defaultValue?: T): [T, (value: T) => void] => { | ||
| export const useSetting = <T>(key?: string, defaultValue?: T): [T, (value: T) => void] => { |
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.
@greptile why key is optional here?
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.
@greptile why key is optional here?
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.
Looking at this useSetting hook, the key parameter is optional (marked with ?) which seems unusual and potentially problematic.
If you look at how the hook is used internally:
- Line 13:
getSettingValue(state, key)- passing potentiallyundefinedkey - Line 18:
dispatch(setSettingValue(key, value))- dispatching with potentiallyundefinedkey
This means the hook could be called without a key like useSetting(), which would result in trying to get/set a setting with an undefined key. This doesn't make much practical sense since you need a specific key to identify which setting you're working with.
The optional key seems like it could lead to bugs where:
- Someone calls
useSetting()without a key by mistake - The hook "works" but doesn't actually store/retrieve any meaningful setting
- The setting operations silently fail or behave unexpectedly
This should probably be required: key: string instead of key?: string. The only scenario where optional might make sense is if there's some default key behavior in the getSettingValue/setSettingValue functions, but that would be unusual and error-prone.
Is there a specific use case where you need to call useSetting without a key, or should this be made required?
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.
Key is optional in useSetting for one specific case - table columns width. We use useSetting hook in ResizeableDataTable via useTableResize, if there is no key provided, only value from useState will be used, if there is a key - it syncs with settings
|
|
||
| if (!shouldUseOnlyExternalSettings) { | ||
| setSettingValueToLS(name, value); | ||
| if (isNil(window.api?.metaSettings)) { |
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.
upper in code we have straighforward condition but here is reverted logics - maybe needs unification
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.
Unlike for settingValue, here we also check if user is present, so condition is different. Altered it a little, now it's not reversed
| this.meta = new MetaAPI(axiosParams, baseApiParams); | ||
| } | ||
| if (uiFactory.useMetaSettings) { | ||
| if (useMetaSettings) { |
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.
previosly we had this in uiFactory - so this value could be overriden
Is it an intentional removal of this possibility? (probably a breaking change - major version)
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.
Now it can be configured in configureStore.
In internal version we configure two entities - store and uiFactory. With setting from uiFactory there was an issue when uiFactory was configured after store
Code with bug:
const {store, history} = configureStore({
singleClusterMode: SINGLE_CLUSTER_MODE,
api: new YdbInternalAPI({...params}),
});
configureUIFactory<IssueType, MetricsIds>({
...params
});
Correct code:
configureUIFactory<IssueType, MetricsIds>({
...params
});
const {store, history} = configureStore({
singleClusterMode: SINGLE_CLUSTER_MODE,
api: new YdbInternalAPI({...params}),
});
It's not obvious at all and should not be that way. So I moved this setting to api.
I don't consider this major, because this feature is not finished yet and does not work anywhere
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.
17 files reviewed, 1 comment
|
|
||
| user: string | undefined; | ||
| id: string | undefined; | ||
|
|
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.
logic: metaUser field is never set anywhere in the codebase - no reducer action populates this field. Since selectMetaUser returns metaUser ?? id, this will always fall back to id.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/authentication/types.ts
Line: 8:8
Comment:
**logic:** `metaUser` field is never set anywhere in the codebase - no reducer action populates this field. Since `selectMetaUser` returns `metaUser ?? id`, this will always fall back to `id`.
How can I resolve this? If you propose a fix, please make it concise.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.
It's intentional, user will be added with another PR
| if (isNil(window.api?.metaSettings)) { | ||
| saveSettingToLS(value); | ||
| } else if (user) { | ||
| setMetaSetting({user, name, value}); | ||
| } |
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.
logic: when window.api?.metaSettings exists but user is undefined, settings changes are silently dropped. Since metaUser field is never populated (no reducer sets it), and selectMetaUser returns metaUser ?? id, this means settings will fail to save if id is also undefined during early app initialization
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/settings/useSetting.ts
Line: 56:60
Comment:
**logic:** when `window.api?.metaSettings` exists but `user` is `undefined`, settings changes are silently dropped. Since `metaUser` field is never populated (no reducer sets it), and `selectMetaUser` returns `metaUser ?? id`, this means settings will fail to save if `id` is also undefined during early app initialization
How can I resolve this? If you propose a fix, please make it concise.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.
That's right, if there is no id provided, it will not work as expected. However, there should be always id in whoami as fallback
| dispatch(settingsSlice.actions.setSettingValue({name, value})); | ||
| setSettingValueToLS(name, value); |
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.
logic: every call to setSettingValue now writes to localStorage, even when meta settings API is available. In environments using meta settings, this duplicates storage and may cause sync issues
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/settings/settings.ts
Line: 60:61
Comment:
**logic:** every call to `setSettingValue` now writes to localStorage, even when meta settings API is available. In environments using meta settings, this duplicates storage and may cause sync issues
How can I resolve this? If you propose a fix, please make it concise.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.
It's intentional, setSettingValue is called outside new useSetting hook only for settings that are not migrated
| if (window.api?.metaSettings) { | ||
| value = settingFromMeta; | ||
| } else { | ||
| value = settingFromLS; | ||
| } |
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.
logic: when window.api?.metaSettings exists but query hasn't completed (params are skipToken or loading), settingFromMeta is undefined and meta path is chosen, causing fallback to default instead of using LS value. This breaks the intended graceful degradation during loading states
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/settings/useSetting.ts
Line: 43:47
Comment:
**logic:** when `window.api?.metaSettings` exists but query hasn't completed (params are skipToken or loading), `settingFromMeta` is `undefined` and meta path is chosen, causing fallback to default instead of using LS value. This breaks the intended graceful degradation during loading states
How can I resolve this? If you propose a fix, please make it concise.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.
It's intentional, when there is meta, LS should not be used
src/store/utils.ts
Outdated
| * | ||
| * It prevents redux console error on string error in code - `throw new Error("description")` | ||
| */ | ||
| export function serializeError(error: unknown) { |
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.
nitpick: maybe we could enhance prepareErrorMessage in shared utils
or move this function to shared utils (with renaming to serializeReduxError)
since this may be needed elsewhere
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.
prepareErrorMessage always return string error, while serializeReduxError could return an object
Moved function to utils
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.
17 files reviewed, 2 comments
|
|
||
| if (!shouldUseOnlyExternalSettings) { | ||
| setSettingValueToLS(name, value); | ||
| if (window.api.metaSettings && user) { |
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.
logic: missing optional chaining - will throw if window.api is undefined
| if (window.api.metaSettings && user) { | |
| if (window.api?.metaSettings && user) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/settings/useSetting.ts
Line: 56:56
Comment:
**logic:** missing optional chaining - will throw if `window.api` is undefined
```suggestion
if (window.api?.metaSettings && user) {
```
How can I resolve this? If you propose a fix, please make it concise.| if (window.api?.metaSettings) { | ||
| value = settingFromMeta; | ||
| } else { | ||
| value = settingFromLS; | ||
| } |
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.
logic: the check on line 43 evaluates to true even when query hasn't started (params are skipToken) or during loading state. This causes settingFromMeta to be undefined and the hook returns default value instead of using the localStorage fallback. During initial load before meta query completes, users will see default values instead of their saved LS values.
Check the query status to ensure meta is actually available:
| if (window.api?.metaSettings) { | |
| value = settingFromMeta; | |
| } else { | |
| value = settingFromLS; | |
| } | |
| if (window.api?.metaSettings && settingFromMeta !== undefined) { | |
| value = settingFromMeta; | |
| } else { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/store/reducers/settings/useSetting.ts
Line: 43:47
Comment:
**logic:** the check on line 43 evaluates to true even when query hasn't started (params are skipToken) or during loading state. This causes `settingFromMeta` to be `undefined` and the hook returns default value instead of using the localStorage fallback. During initial load before meta query completes, users will see default values instead of their saved LS values.
Check the query status to ensure meta is actually available:
```suggestion
if (window.api?.metaSettings && settingFromMeta !== undefined) {
value = settingFromMeta;
} else {
```
How can I resolve this? If you propose a fix, please make it concise.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.
It should never use local setting if there is meta
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.
17 files reviewed, no comments
|
@artemmufazalov I had a couple of questions in comments |
Part of #2892
Currently it can be checked with table columns width.
Stand (without meta): https://nda.ya.ru/t/BEIjuuLI7N8Qd7
Sync all values within api
onQueryStartedinstead of useEffects.This approach proved itself good with and without meta api. Without meta api there is only 1 failed test (against 100-200 before): #3054
greptile-review
CI Results
Test Status:⚠️ FLAKY
📊 Full Report
Test Changes Summary ⏭️2
⏭️ Skipped Tests (2)
Bundle Size: ✅
Current: 62.48 MB | Main: 62.49 MB
Diff: 1.98 KB (-0.00%)
✅ Bundle size unchanged.
ℹ️ CI Information