From cc858d4f0f03df555846d0ef537e46e2454dbfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 5 Nov 2024 20:45:57 +0100 Subject: [PATCH] pl-fe: Replace immer with mutative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- packages/pl-fe/package.json | 5 +- packages/pl-fe/src/entity-store/reducer.ts | 26 +++--- .../pl-fe/src/features/admin/user-index.tsx | 4 +- packages/pl-fe/src/reducers/accounts-meta.ts | 9 +- .../pl-fe/src/reducers/admin-user-index.ts | 88 +++++++++++-------- packages/pl-fe/src/reducers/instance.ts | 47 +++++----- packages/pl-fe/src/store.ts | 6 +- packages/pl-fe/src/stores/modals.ts | 14 +-- packages/pl-fe/src/stores/settings.ts | 30 +++---- packages/pl-fe/yarn.lock | 12 ++- 10 files changed, 133 insertions(+), 108 deletions(-) diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index fe4898c8a..2fedc6821 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -87,7 +87,6 @@ "fuzzysort": "^3.0.2", "graphemesplit": "^2.4.4", "html-react-parser": "^5.1.16", - "immer": "^10.1.1", "immutable": "^4.3.7", "intersection-observer": "^0.12.2", "intl-messageformat": "^10.5.14", @@ -100,6 +99,7 @@ "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.9.1", "multiselect-react-dropdown": "^2.0.25", + "mutative": "^1.0.11", "path-browserify": "^1.0.1", "pl-api": "^0.1.9", "postcss": "^8.4.47", @@ -142,7 +142,8 @@ "vite-plugin-html": "^3.2.2", "vite-plugin-require": "^1.2.14", "vite-plugin-static-copy": "^1.0.6", - "zustand": "^5.0.0-rc.2" + "zustand": "^5.0.0-rc.2", + "zustand-mutative": "^1.0.5" }, "devDependencies": { "@formatjs/cli": "^6.2.12", diff --git a/packages/pl-fe/src/entity-store/reducer.ts b/packages/pl-fe/src/entity-store/reducer.ts index bc0c43397..a5a49a818 100644 --- a/packages/pl-fe/src/entity-store/reducer.ts +++ b/packages/pl-fe/src/entity-store/reducer.ts @@ -1,4 +1,4 @@ -import { produce, enableMapSet } from 'immer'; +import { create, type Immutable } from 'mutative'; import { ENTITIES_IMPORT, @@ -17,12 +17,10 @@ import { createCache, createList, updateStore, updateList } from './utils'; import type { DeleteEntitiesOpts } from './actions'; import type { EntitiesTransaction, Entity, EntityCache, EntityListState, ImportPosition } from './types'; -enableMapSet(); - /** Entity reducer state. */ -interface State { +type State = Immutable<{ [entityType: string]: EntityCache | undefined; -} +}>; /** Import entities into the cache. */ const importEntities = ( @@ -33,7 +31,7 @@ const importEntities = ( pos?: ImportPosition, newState?: EntityListState, overwrite = false, -): State => produce(state, draft => { +): State => create(state, draft => { const cache = draft[entityType] ?? createCache(); cache.store = updateStore(cache.store, entities); @@ -54,14 +52,16 @@ const importEntities = ( } draft[entityType] = cache; -}); + + return draft; +}, { enableAutoFreeze: true }); const deleteEntities = ( state: State, entityType: string, ids: Iterable, opts: DeleteEntitiesOpts, -) => produce(state, draft => { +) => create(state, draft => { const cache = draft[entityType] ?? createCache(); for (const id of ids) { @@ -88,7 +88,7 @@ const dismissEntities = ( entityType: string, ids: Iterable, listKey: string, -) => produce(state, draft => { +) => create(state, draft => { const cache = draft[entityType] ?? createCache(); const list = cache.lists[listKey]; @@ -110,7 +110,7 @@ const incrementEntities = ( entityType: string, listKey: string, diff: number, -) => produce(state, draft => { +) => create(state, draft => { const cache = draft[entityType] ?? createCache(); const list = cache.lists[listKey]; @@ -126,7 +126,7 @@ const setFetching = ( listKey: string | undefined, isFetching: boolean, error?: any, -) => produce(state, draft => { +) => create(state, draft => { const cache = draft[entityType] ?? createCache(); if (typeof listKey === 'string') { @@ -139,13 +139,13 @@ const setFetching = ( draft[entityType] = cache; }); -const invalidateEntityList = (state: State, entityType: string, listKey: string) => produce(state, draft => { +const invalidateEntityList = (state: State, entityType: string, listKey: string) => create(state, draft => { const cache = draft[entityType] ?? createCache(); const list = cache.lists[listKey] ?? createList(); list.state.invalid = true; }); -const doTransaction = (state: State, transaction: EntitiesTransaction) => produce(state, draft => { +const doTransaction = (state: State, transaction: EntitiesTransaction) => create(state, draft => { for (const [entityType, changes] of Object.entries(transaction)) { const cache = draft[entityType] ?? createCache(); for (const [id, change] of Object.entries(changes)) { diff --git a/packages/pl-fe/src/features/admin/user-index.tsx b/packages/pl-fe/src/features/admin/user-index.tsx index 93fe926e2..e39d151b0 100644 --- a/packages/pl-fe/src/features/admin/user-index.tsx +++ b/packages/pl-fe/src/features/admin/user-index.tsx @@ -39,9 +39,9 @@ const UserIndex: React.FC = () => { updateQuery(); }, []); - const hasMore = items.count() < total && !!next; + const hasMore = items.length < total && !!next; - const showLoading = isLoading && items.isEmpty(); + const showLoading = isLoading && !items.length; return ( diff --git a/packages/pl-fe/src/reducers/accounts-meta.ts b/packages/pl-fe/src/reducers/accounts-meta.ts index 28ee85b21..7c2fe80a9 100644 --- a/packages/pl-fe/src/reducers/accounts-meta.ts +++ b/packages/pl-fe/src/reducers/accounts-meta.ts @@ -2,8 +2,7 @@ * Accounts Meta: private user data only the owner should see. * @module pl-fe/reducers/accounts_meta */ - -import { produce } from 'immer'; +import { create, type Immutable } from 'mutative'; import { VERIFY_CREDENTIALS_SUCCESS, AUTH_ACCOUNT_REMEMBER_SUCCESS, type AuthAction } from 'pl-fe/actions/auth'; import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, type MeAction } from 'pl-fe/actions/me'; @@ -15,17 +14,17 @@ interface AccountMeta { source: Account['__meta']['source']; } -type State = Record; +type State = Immutable>; const importAccount = (state: State, account: CredentialAccount): State => - produce(state, draft => { + create(state, draft => { const existing = draft[account.id]; draft[account.id] = { pleroma: account.__meta.pleroma ?? existing?.pleroma, source: account.__meta.source ?? existing?.source, }; - }); + }, { enableAutoFreeze: true }); const accounts_meta = (state: Readonly = {}, action: AuthAction | MeAction): State => { switch (action.type) { diff --git a/packages/pl-fe/src/reducers/admin-user-index.ts b/packages/pl-fe/src/reducers/admin-user-index.ts index f44709842..0459cea4f 100644 --- a/packages/pl-fe/src/reducers/admin-user-index.ts +++ b/packages/pl-fe/src/reducers/admin-user-index.ts @@ -1,4 +1,4 @@ -import { OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable'; +import { create } from 'mutative'; import { ADMIN_USER_INDEX_EXPAND_FAIL, @@ -10,58 +10,74 @@ import { ADMIN_USER_INDEX_QUERY_SET, } from 'pl-fe/actions/admin'; +import type { AnyAction } from '@reduxjs/toolkit'; import type { AdminAccount, AdminGetAccountsParams, PaginatedResponse } from 'pl-api'; import type { APIEntity } from 'pl-fe/types/entities'; -import type { AnyAction } from 'redux'; -const ReducerRecord = ImmutableRecord({ +type State = { + isLoading: boolean; + loaded: boolean; + items: Array; + total: number; + page: number; + query: string; + next: (() => Promise>) | null; + params: AdminGetAccountsParams | null; +} + +const initialState: State = { isLoading: false, loaded: false, - items: ImmutableOrderedSet(), + items: [], total: Infinity, page: -1, query: '', - next: null as (() => Promise>) | null, - params: null as AdminGetAccountsParams | null, -}); + next: null, + params: null, +}; -type State = ReturnType; - -const admin_user_index = (state: State = ReducerRecord(), action: AnyAction): State => { +const admin_user_index = (state: State = initialState, action: AnyAction): State => { switch (action.type) { case ADMIN_USER_INDEX_QUERY_SET: - return state.set('query', action.query); + return create(state, draft => { + draft.query = action.query; + }); case ADMIN_USER_INDEX_FETCH_REQUEST: - return state - .set('isLoading', true) - .set('loaded', true) - .set('items', ImmutableOrderedSet()) - .set('total', action.total) - .set('page', 0) - .set('next', null); + return create(state, draft => { + draft.isLoading = true; + draft.loaded = true; + draft.items = []; + draft.total = action.total; + draft.page = 0; + draft.next = null; + }); case ADMIN_USER_INDEX_FETCH_SUCCESS: - return state - .set('isLoading', false) - .set('loaded', true) - .set('items', ImmutableOrderedSet(action.users.map((user: APIEntity) => user.id))) - .set('total', action.total) - .set('page', 1) - .set('next', action.next); + return create(state, draft => { + draft.isLoading = false; + draft.loaded = true; + draft.items = action.users.map((user: APIEntity) => user.id); + draft.total = action.total; + draft.page = 1; + draft.next = action.next; + }); case ADMIN_USER_INDEX_FETCH_FAIL: case ADMIN_USER_INDEX_EXPAND_FAIL: - return state - .set('isLoading', false); + return create(state, draft => { + draft.isLoading = false; + }); case ADMIN_USER_INDEX_EXPAND_REQUEST: - return state - .set('isLoading', true); + return create(state, draft => { + draft.isLoading = true; + }); case ADMIN_USER_INDEX_EXPAND_SUCCESS: - return state - .set('isLoading', false) - .set('loaded', true) - .set('items', state.items.union(action.users.map((user: APIEntity) => user.id))) - .set('total', action.total) - .set('page', 1) - .set('next', action.next); + return create(state, draft => { + draft.isLoading = false; + draft.loaded = true; + draft.items = [...new Set(draft.items.concat(action.users.map((user: APIEntity) => user.id)))]; + draft.total = action.total; + draft.page = 1; + draft.next = action.next; + }); default: return state; } diff --git a/packages/pl-fe/src/reducers/instance.ts b/packages/pl-fe/src/reducers/instance.ts index f5acfcf33..b7b4144e9 100644 --- a/packages/pl-fe/src/reducers/instance.ts +++ b/packages/pl-fe/src/reducers/instance.ts @@ -1,5 +1,5 @@ -import { produce } from 'immer'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { create } from 'mutative'; import { type Instance, instanceSchema } from 'pl-api'; import * as v from 'valibot'; @@ -11,9 +11,11 @@ import ConfigDB from 'pl-fe/utils/config-db'; import type { AnyAction } from 'redux'; -const initialState: Instance = v.parse(instanceSchema, {}); +const initialState: State = v.parse(instanceSchema, {}); -const preloadImport = (state: Instance, action: Record, path: string) => { +type State = Instance; + +const preloadImport = (state: State, action: Record, path: string) => { const instance = action.data[path]; return instance ? v.parse(instanceSchema, instance) : state; }; @@ -25,32 +27,30 @@ const getConfigValue = (instanceConfig: ImmutableMap, key: string) return v ? v.getIn(['tuple', 1]) : undefined; }; -const importConfigs = (state: Instance, configs: ImmutableList) => { +const importConfigs = (state: State, configs: ImmutableList) => { // FIXME: This is pretty hacked together. Need to make a cleaner map. const config = ConfigDB.find(configs, ':pleroma', ':instance'); const simplePolicy = ConfigDB.toSimplePolicy(configs); if (!config && !simplePolicy) return state; - return produce(state, (draft) => { - if (config) { - const value = config.get('value', ImmutableList()); - const registrationsOpen = getConfigValue(value, ':registrations_open') as boolean | undefined; - const approvalRequired = getConfigValue(value, ':account_approval_required') as boolean | undefined; + if (config) { + const value = config.get('value', ImmutableList()); + const registrationsOpen = getConfigValue(value, ':registrations_open') as boolean | undefined; + const approvalRequired = getConfigValue(value, ':account_approval_required') as boolean | undefined; - draft.registrations = { - enabled: registrationsOpen ?? draft.registrations.enabled, - approval_required: approvalRequired ?? draft.registrations.approval_required, - }; - } + state.registrations = { + enabled: registrationsOpen ?? state.registrations.enabled, + approval_required: approvalRequired ?? state.registrations.approval_required, + }; + } - if (simplePolicy) { - draft.pleroma.metadata.federation.mrf_simple = simplePolicy; - } - }); + if (simplePolicy) { + state.pleroma.metadata.federation.mrf_simple = simplePolicy; + } }; -const handleAuthFetch = (state: Instance) => { +const handleAuthFetch = (state: State) => { // Authenticated fetch is enabled, so make the instance appear censored return { ...state, @@ -78,7 +78,7 @@ const persistInstance = (instance: { domain: string }, host: string | null = get } }; -const handleInstanceFetchFail = (state: Instance, error: Record) => { +const handleInstanceFetchFail = (state: State, error: Record) => { if (error.response?.status === 401) { return handleAuthFetch(state); } else { @@ -86,10 +86,10 @@ const handleInstanceFetchFail = (state: Instance, error: Record) => } }; -const instance = (state = initialState, action: AnyAction | InstanceAction | PreloadAction): Instance => { +const instance = (state = initialState, action: AnyAction | InstanceAction | PreloadAction): State => { switch (action.type) { case PLEROMA_PRELOAD_IMPORT: - return preloadImport(state, action, '/api/v1/instance'); + return create(state, (draft) => preloadImport(draft, action, '/api/v1/instance')); case INSTANCE_FETCH_SUCCESS: persistInstance(action.instance); return action.instance; @@ -97,10 +97,9 @@ const instance = (state = initialState, action: AnyAction | InstanceAction | Pre return handleInstanceFetchFail(state, action.error); case ADMIN_CONFIG_UPDATE_REQUEST: case ADMIN_CONFIG_UPDATE_SUCCESS: - return importConfigs(state, ImmutableList(fromJS(action.configs))); + return create(state, (draft) => importConfigs(draft, ImmutableList(fromJS(action.configs)))); default: return state; } }; - export { instance as default }; diff --git a/packages/pl-fe/src/store.ts b/packages/pl-fe/src/store.ts index 275c8d002..c7cd834ff 100644 --- a/packages/pl-fe/src/store.ts +++ b/packages/pl-fe/src/store.ts @@ -1,12 +1,10 @@ -import { configureStore, Tuple } from '@reduxjs/toolkit'; +import { configureStore, Tuple, type AnyAction } from '@reduxjs/toolkit'; import { thunk, type ThunkDispatch } from 'redux-thunk'; import errorsMiddleware from './middleware/errors'; import soundsMiddleware from './middleware/sounds'; import appReducer from './reducers'; -import type { AnyAction } from 'redux'; - const store = configureStore({ reducer: appReducer, middleware: () => new Tuple( @@ -17,6 +15,8 @@ const store = configureStore({ devTools: true, }); +(window as any).store = store; + type Store = typeof store; // Infer the `RootState` and `AppDispatch` types from the store itself diff --git a/packages/pl-fe/src/stores/modals.ts b/packages/pl-fe/src/stores/modals.ts index 107dce67f..29ec4caf8 100644 --- a/packages/pl-fe/src/stores/modals.ts +++ b/packages/pl-fe/src/stores/modals.ts @@ -1,5 +1,5 @@ -import { produce } from 'immer'; import { create } from 'zustand'; +import { mutative } from 'zustand-mutative'; import type { ICryptoAddress } from 'pl-fe/features/crypto-donate/components/crypto-address'; import type { ModalType } from 'pl-fe/features/ui/components/modal-root'; @@ -86,12 +86,12 @@ type State = { closeModal: (modalType?: ModalType) => void; }; -const useModalsStore = create((set) => ({ +const useModalsStore = create()(mutative((set) => ({ modals: [], - openModal: (...[modalType, modalProps]) => set(produce((state: State) => { + openModal: (...[modalType, modalProps]) => set((state: State) => { state.modals.push({ modalType, modalProps }); - })), - closeModal: (modalType) => set(produce((state: State) => { + }), + closeModal: (modalType) => set((state: State) => { if (state.modals.length === 0) { return; } @@ -100,7 +100,7 @@ const useModalsStore = create((set) => ({ } else if (state.modals.some((modal) => modalType === modal.modalType)) { state.modals = state.modals.slice(0, state.modals.findLastIndex((modal) => modalType === modal.modalType)); } - })), -})); + }), +}), { enableAutoFreeze: true })); export { useModalsStore }; diff --git a/packages/pl-fe/src/stores/settings.ts b/packages/pl-fe/src/stores/settings.ts index f04b4ce4c..885769cee 100644 --- a/packages/pl-fe/src/stores/settings.ts +++ b/packages/pl-fe/src/stores/settings.ts @@ -1,6 +1,6 @@ -import { produce } from 'immer'; import * as v from 'valibot'; import { create } from 'zustand'; +import { mutative } from 'zustand-mutative'; import { settingsSchema, type Settings } from 'pl-fe/schemas/pl-fe/settings'; @@ -35,39 +35,39 @@ const changeSetting = (object: APIEntity, path: string[], value: any) => { const mergeSettings = (state: State) => state.settings = { ...state.defaultSettings, ...state.userSettings }; -const useSettingsStore = create((set) => ({ +const useSettingsStore = create()(mutative((set) => ({ defaultSettings: v.parse(settingsSchema, {}), userSettings: {}, settings: v.parse(settingsSchema, {}), - loadDefaultSettings: (settings: APIEntity) => set(produce((state: State) => { + loadDefaultSettings: (settings: APIEntity) => set((state: State) => { if (typeof settings !== 'object') return; state.defaultSettings = v.parse(settingsSchema, settings); mergeSettings(state); - })), + }), - loadUserSettings: (settings?: APIEntity) => set(produce((state: State) => { + loadUserSettings: (settings?: APIEntity) => set((state: State) => { if (typeof settings !== 'object') return; state.userSettings = v.parse(settingsSchemaPartial, settings); mergeSettings(state); - })), + }), - userSettingsSaving: () => set(produce((state: State) => { + userSettingsSaving: () => set((state: State) => { state.userSettings.saved = true; mergeSettings(state); - })), + }), - changeSetting: (path: string[], value: any) => set(produce((state: State) => { + changeSetting: (path: string[], value: any) => set((state: State) => { changeSetting(state.userSettings, path, value); mergeSettings(state); - })), + }), - rememberEmojiUse: (emoji: Emoji) => set(produce((state: State) => { + rememberEmojiUse: (emoji: Emoji) => set((state: State) => { const settings = state.userSettings; if (!settings.frequentlyUsedEmojis) settings.frequentlyUsedEmojis = {}; @@ -75,9 +75,9 @@ const useSettingsStore = create((set) => ({ settings.saved = false; mergeSettings(state); - })), + }), - rememberLanguageUse: (language: string) => set(produce((state: State) => { + rememberLanguageUse: (language: string) => set((state: State) => { const settings = state.userSettings; if (!settings.frequentlyUsedLanguages) settings.frequentlyUsedLanguages = {}; @@ -85,8 +85,8 @@ const useSettingsStore = create((set) => ({ settings.saved = false; mergeSettings(state); - })), -})); + }), +}), { enableAutoFreeze: true })); export { useSettingsStore }; diff --git a/packages/pl-fe/yarn.lock b/packages/pl-fe/yarn.lock index aed313f75..b48bbcade 100644 --- a/packages/pl-fe/yarn.lock +++ b/packages/pl-fe/yarn.lock @@ -5812,7 +5812,7 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== -immer@^10.0.3, immer@^10.1.1: +immer@^10.0.3: version "10.1.1" resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc" integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw== @@ -7146,6 +7146,11 @@ multiselect-react-dropdown@^2.0.25: resolved "https://registry.yarnpkg.com/multiselect-react-dropdown/-/multiselect-react-dropdown-2.0.25.tgz#0c8d16f20d78023d5be2f3af4f15a4a164b6b427" integrity sha512-z8kUSyBNOuV7vn4Dk35snzXWtIfTdSEEXhgDdLMvOmR+xJFx35vc1voUlSuOvrk3khb+hXC219Qs9szOvNm25Q== +mutative@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/mutative/-/mutative-1.0.11.tgz#a03b48151800129fbc464257eff9ffa869c648a8" + integrity sha512-DfxsNvHfJlxp5yul7jhcNSI0EEWlP1vatiOr6Q7cvr8RNFBbIU5nENilUULbNJiOtbXznOxgbxHf4cYbqPDPlg== + mz@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -10429,6 +10434,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zustand-mutative@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/zustand-mutative/-/zustand-mutative-1.0.5.tgz#00ce93900a25d812b48d3f0d360da8a3890d0dd9" + integrity sha512-/Rc2huxi2SxD21EcSrwW6moCRv+phFcrv3bMyrsQMDkI49LqLqMzhfNCZnOv0TmUctgG0u9xD6gHLUoYEwjkkQ== + zustand@^5.0.0-rc.2: version "5.0.0-rc.2" resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.0-rc.2.tgz#d28d95ffb6f0b20cadbaea39210f18446a5bf989"