Skip to content

Commit 57e8345

Browse files
feat(useSetting); sync values with LS and store in api
1 parent 5eeadb3 commit 57e8345

File tree

5 files changed

+134
-102
lines changed

5 files changed

+134
-102
lines changed

src/services/api/metaSettings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class MetaSettingsAPI extends BaseMetaAPI {
2525
preventBatching,
2626
}: GetSingleSettingParams & {preventBatching?: boolean}) {
2727
if (preventBatching) {
28-
return this.get<Setting>(this.getPath('/meta/user_settings'), {name, user});
28+
return this.get<Setting | undefined>(this.getPath('/meta/user_settings'), {name, user});
2929
}
3030

3131
return new Promise<Setting>((resolve, reject) => {

src/store/reducers/settings/api.ts

Lines changed: 119 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,105 @@ import type {
66
SetSingleSettingParams,
77
Setting,
88
} from '../../../types/api/settings';
9-
import type {AppDispatch} from '../../defaultStore';
9+
import type {AppDispatch, RootState} from '../../defaultStore';
1010
import {api} from '../api';
1111

1212
import {SETTINGS_OPTIONS} from './constants';
13+
import {getSettingValue, setSettingValue} from './settings';
14+
import {
15+
getSettingDefault,
16+
parseSettingValue,
17+
readSettingValueFromLS,
18+
setSettingValueToLS,
19+
shouldSyncSettingToLS,
20+
stringifySettingValue,
21+
} from './utils';
22+
23+
const invalidParamsError =
24+
'Missing required parameters (name, user) or MetaSettings API is not available';
1325

1426
export const settingsApi = api.injectEndpoints({
1527
endpoints: (builder) => ({
16-
getSingleSetting: builder.query({
17-
queryFn: async ({name, user}: GetSingleSettingParams, baseApi) => {
28+
getSingleSetting: builder.query<Setting | undefined, Partial<GetSingleSettingParams>>({
29+
queryFn: async ({name, user}) => {
1830
try {
19-
if (!window.api.metaSettings) {
20-
throw new Error('MetaSettings API is not available');
31+
// In this case localStorage should be used for settings
32+
// Actual value will be loaded to store in onQueryStarted
33+
if (!name || !user || !window.api.metaSettings) {
34+
throw new Error(invalidParamsError);
2135
}
36+
2237
const data = await window.api.metaSettings.getSingleSetting({
2338
name,
2439
user,
2540
// Directly access options here to avoid them in cache key
2641
preventBatching: SETTINGS_OPTIONS[name]?.preventBatching,
2742
});
2843

29-
const dispatch = baseApi.dispatch as AppDispatch;
30-
31-
// Try to sync local value if there is no backend value
32-
syncLocalValueToMetaIfNoData(data, dispatch);
33-
3444
return {data};
3545
} catch (error) {
3646
return {error};
3747
}
3848
},
49+
async onQueryStarted(args, {dispatch, queryFulfilled}) {
50+
const {name, user} = args;
51+
52+
if (!name) {
53+
return;
54+
}
55+
56+
const shouldUseLocalSettings =
57+
!user || !window.api.metaSettings || shouldSyncSettingToLS(name);
58+
59+
const defaultValue = getSettingDefault(name);
60+
61+
// Preload value from LS or default to store
62+
if (shouldUseLocalSettings) {
63+
const savedValue = readSettingValueFromLS(name);
64+
const value = savedValue ?? defaultValue;
65+
66+
dispatch(setSettingValue(name, value));
67+
} else {
68+
dispatch(setSettingValue(name, defaultValue));
69+
}
70+
71+
try {
72+
const {data} = await queryFulfilled;
73+
74+
// Load api value to store if present
75+
// In case local storage should be used
76+
// query will finish with an error and this code will not run
77+
const parsedValue = parseSettingValue(data?.value);
78+
79+
if (isNil(data?.value)) {
80+
// Try to sync local value if there is no backend value
81+
syncLocalValueToMetaIfNoData({...data}, dispatch);
82+
} else {
83+
dispatch(setSettingValue(name, parsedValue));
84+
85+
if (shouldSyncSettingToLS(name)) {
86+
setSettingValueToLS(name, data.value);
87+
}
88+
}
89+
} catch {}
90+
},
3991
}),
4092
setSingleSetting: builder.mutation({
41-
queryFn: async (params: SetSingleSettingParams) => {
93+
queryFn: async ({
94+
name,
95+
user,
96+
value,
97+
}: Partial<Omit<SetSingleSettingParams, 'value'>> & {value: unknown}) => {
4298
try {
43-
if (!window.api.metaSettings) {
44-
throw new Error('MetaSettings API is not available');
99+
if (!name || !user || !window.api.metaSettings) {
100+
throw new Error(invalidParamsError);
45101
}
46102

47-
const data = await window.api.metaSettings.setSingleSetting(params);
103+
const data = await window.api.metaSettings.setSingleSetting({
104+
name,
105+
user,
106+
value: stringifySettingValue(value),
107+
});
48108

49109
if (data.status !== 'SUCCESS') {
50110
throw new Error('Setting status is not SUCCESS');
@@ -55,34 +115,51 @@ export const settingsApi = api.injectEndpoints({
55115
return {error};
56116
}
57117
},
58-
async onQueryStarted(args, {dispatch, queryFulfilled}) {
118+
async onQueryStarted(args, {dispatch, queryFulfilled, getState}) {
59119
const {name, user, value} = args;
60120

61-
// Optimistically update existing cache entry
62-
const patchResult = dispatch(
63-
settingsApi.util.updateQueryData('getSingleSetting', {name, user}, (draft) => {
64-
return {...draft, name, user, value};
65-
}),
66-
);
121+
if (!name) {
122+
return;
123+
}
124+
125+
// Extract previous value to revert to it if set is not succesfull
126+
const previousSettingValue = getSettingValue(getState() as RootState, name);
127+
128+
// Optimistically update store
129+
dispatch(setSettingValue(name, value));
130+
131+
// If local storage settings should be used
132+
// Update LS and do not do any further code
133+
if (!user || !window.api.metaSettings) {
134+
setSettingValueToLS(name, value);
135+
return;
136+
}
137+
67138
try {
68139
await queryFulfilled;
140+
141+
// If mutation is successful, we can store new value in LS
142+
if (shouldSyncSettingToLS(name)) {
143+
setSettingValueToLS(name, value);
144+
}
69145
} catch {
70-
patchResult.undo();
146+
// Set previous value to store in case of error
147+
dispatch(setSettingValue(name, previousSettingValue));
71148
}
72149
},
73150
}),
74151
getSettings: builder.query({
75-
queryFn: async ({name, user}: GetSettingsParams, baseApi) => {
152+
queryFn: async ({name, user}: Partial<GetSettingsParams>, baseApi) => {
76153
try {
77-
if (!window.api.metaSettings) {
78-
throw new Error('MetaSettings API is not available');
154+
if (!window.api.metaSettings || !name || !user) {
155+
throw new Error(invalidParamsError);
79156
}
80157
const data = await window.api.metaSettings.getSettings({name, user});
81158

82-
const patches: Promise<void>[] = [];
159+
const patches: Promise<unknown>[] = [];
83160
const dispatch = baseApi.dispatch as AppDispatch;
84161

85-
// Upsert received data in getSingleSetting cache
162+
// Upsert received data in getSingleSetting cache to prevent further redundant requests
86163
name.forEach((settingName) => {
87164
const settingData = data[settingName] ?? {};
88165

@@ -98,15 +175,18 @@ export const settingsApi = api.injectEndpoints({
98175
cacheEntryParams,
99176
newValue,
100177
),
101-
).then(() => {
178+
);
179+
if (isNil(settingData.value)) {
102180
// Try to sync local value if there is no backend value
103-
// Do it after upsert if finished to ensure proper values update order
104-
// 1. New entry added to cache with nil value
105-
// 2. Positive entry update - local storage value replace nil in cache
106-
// 3.1. Set is successful, local value in cache
107-
// 3.2. Set is not successful, cache value reverted to previous nil
108181
syncLocalValueToMetaIfNoData(settingData, dispatch);
109-
});
182+
} else {
183+
const parsedValue = parseSettingValue(settingData.value);
184+
dispatch(setSettingValue(settingName, parsedValue));
185+
186+
if (shouldSyncSettingToLS(settingName)) {
187+
setSettingValueToLS(settingName, settingData.value);
188+
}
189+
}
110190

111191
patches.push(patch);
112192
});
@@ -124,7 +204,11 @@ export const settingsApi = api.injectEndpoints({
124204
overrideExisting: 'throw',
125205
});
126206

127-
function syncLocalValueToMetaIfNoData(params: Setting, dispatch: AppDispatch) {
207+
function syncLocalValueToMetaIfNoData(params: Partial<Setting>, dispatch: AppDispatch) {
208+
if (!params.name) {
209+
return;
210+
}
211+
128212
const localValue = localStorage.getItem(params.name);
129213

130214
if (isNil(params.value) && !isNil(localValue)) {

src/store/reducers/settings/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export const DEFAULT_USER_SETTINGS = {
7373
[SETTING_KEYS.ACL_SYNTAX]: AclSyntax.YdbShort,
7474
} as const satisfies Record<SettingKey, unknown>;
7575

76-
export const SETTINGS_OPTIONS: Record<string, SettingOptions> = {
76+
export const SETTINGS_OPTIONS: Record<string, SettingOptions | undefined> = {
7777
[SETTING_KEYS.THEME]: {
7878
preventBatching: true,
7979
},

src/store/reducers/settings/useSetting.ts

Lines changed: 4 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
11
import React from 'react';
22

3-
import {skipToken} from '@reduxjs/toolkit/query';
4-
5-
import {uiFactory} from '../../../uiFactory/uiFactory';
6-
import {useTypedDispatch} from '../../../utils/hooks/useTypedDispatch';
73
import {useTypedSelector} from '../../../utils/hooks/useTypedSelector';
84
import {selectID, selectUser} from '../authentication/authentication';
95

106
import {settingsApi} from './api';
11-
import type {SettingKey} from './constants';
12-
import {DEFAULT_USER_SETTINGS, SETTINGS_OPTIONS} from './constants';
13-
import {getSettingValue, setSettingValue} from './settings';
14-
import {
15-
parseSettingValue,
16-
readSettingValueFromLS,
17-
setSettingValueToLS,
18-
stringifySettingValue,
19-
} from './utils';
7+
import {getSettingValue} from './settings';
208

219
type SaveSettingValue<T> = (value: T | undefined) => void;
2210

@@ -25,70 +13,21 @@ export function useSetting<T>(name?: string): {
2513
saveValue: SaveSettingValue<T>;
2614
isLoading: boolean;
2715
} {
28-
const dispatch = useTypedDispatch();
29-
30-
const preventSyncWithLS = Boolean(name && SETTINGS_OPTIONS[name]?.preventSyncWithLS);
31-
3216
const settingValue = useTypedSelector((state) => getSettingValue(state, name)) as T | undefined;
3317

3418
const authUserSID = useTypedSelector(selectUser);
3519
const anonymousUserId = useTypedSelector(selectID);
36-
3720
const user = authUserSID || anonymousUserId;
38-
const shouldUseMetaSettings = uiFactory.useMetaSettings && user && name;
39-
40-
const shouldUseOnlyExternalSettings = shouldUseMetaSettings && preventSyncWithLS;
41-
42-
const params = React.useMemo(() => {
43-
return shouldUseMetaSettings ? {user, name} : skipToken;
44-
}, [shouldUseMetaSettings, user, name]);
4521

46-
const {currentData: metaSetting, isLoading: isSettingLoading} =
47-
settingsApi.useGetSingleSettingQuery(params);
22+
const {isLoading} = settingsApi.useGetSingleSettingQuery({user, name});
4823

4924
const [setMetaSetting] = settingsApi.useSetSingleSettingMutation();
5025

51-
// Add loading state to settings that are stored externally
52-
const isLoading = shouldUseMetaSettings ? isSettingLoading : false;
53-
54-
// Load initial value
55-
React.useEffect(() => {
56-
let value = name ? (DEFAULT_USER_SETTINGS[name as SettingKey] as T | undefined) : undefined;
57-
58-
if (!shouldUseOnlyExternalSettings) {
59-
const savedValue = readSettingValueFromLS<T>(name);
60-
value = savedValue ?? value;
61-
}
62-
63-
dispatch(setSettingValue(name, value));
64-
}, [name, shouldUseOnlyExternalSettings, dispatch]);
65-
66-
// Sync value from backend with LS and store
67-
React.useEffect(() => {
68-
if (shouldUseMetaSettings && metaSetting?.value) {
69-
if (!shouldUseOnlyExternalSettings) {
70-
setSettingValueToLS(name, metaSetting.value);
71-
}
72-
const parsedValue = parseSettingValue<T>(metaSetting.value);
73-
dispatch(setSettingValue(name, parsedValue));
74-
}
75-
}, [shouldUseMetaSettings, shouldUseOnlyExternalSettings, metaSetting, name, dispatch]);
76-
7726
const saveValue = React.useCallback<SaveSettingValue<T>>(
7827
(value) => {
79-
if (shouldUseMetaSettings) {
80-
setMetaSetting({
81-
user,
82-
name: name,
83-
value: stringifySettingValue(value),
84-
});
85-
}
86-
87-
if (!shouldUseOnlyExternalSettings) {
88-
setSettingValueToLS(name, value);
89-
}
28+
setMetaSetting({user, name: name, value: value});
9029
},
91-
[shouldUseMetaSettings, shouldUseOnlyExternalSettings, user, name, setMetaSetting],
30+
[user, name, setMetaSetting],
9231
);
9332

9433
return {value: settingValue, saveValue, isLoading} as const;

src/store/reducers/settings/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type {SettingValue} from '../../../types/api/settings';
22
import {parseJson} from '../../../utils/utils';
33

4+
import type {SettingKey} from './constants';
5+
import {DEFAULT_USER_SETTINGS, SETTINGS_OPTIONS} from './constants';
6+
47
export function stringifySettingValue<T>(value?: T): string {
58
return typeof value === 'string' ? value : JSON.stringify(value);
69
}
@@ -34,3 +37,9 @@ export function setSettingValueToLS(name: string | undefined, value: unknown): v
3437
localStorage.setItem(name, preparedValue);
3538
} catch {}
3639
}
40+
export function getSettingDefault(name: string) {
41+
return DEFAULT_USER_SETTINGS[name as SettingKey];
42+
}
43+
export function shouldSyncSettingToLS(name: string) {
44+
return !SETTINGS_OPTIONS[name]?.preventSyncWithLS;
45+
}

0 commit comments

Comments
 (0)