Start Jotai Migration (#17893)
## Recoil → Jotai progressive migration: infrastructure + ChipFieldDisplay ### Benchmark In the beginning, there was no hope: <img width="1180" height="948" alt="image" src="https://github.com/user-attachments/assets/f8635991-52e6-4958-8240-6ba7214132b2" /> Then the hope was reborn <img width="2070" height="948" alt="image" src="https://github.com/user-attachments/assets/be1182b9-1c8d-4fdc-ab4c-1484ad74449d" /> ### Approach We introduce a **V2 state management layer** backed by Jotai that mirrors the existing Recoil API, enabling component-by-component migration without a big-bang rewrite. #### V2 API (Jotai-backed, Recoil-ergonomic) - `createStateV2` / `createFamilyStateV2` — drop-in replacements for `createState` / `createFamilyState`, returning wrapper types over Jotai atoms - `useRecoilValueV2`, `useRecoilStateV2`, `useFamilyRecoilValueV2`, etc. — thin wrappers around Jotai's `useAtomValue` / `useAtom` / `useSetAtom` - A shared `jotaiStore` (via `createStore()`) passed to a `<JotaiProvider>` wrapping `<RecoilRoot>`, also accessible imperatively for dual-writes #### Dual-write bridge for progressive migration For state shared between migrated and non-migrated components, we use **dual-write**: writers update both the Recoil atom and the Jotai V2 atom (via `jotaiStore.set()`). This avoids sync components or extra subscriptions. Write sites updated: `useUpsertRecordsInStore`, `useSetRecordTableData`, `ListenRecordUpdatesEffect`, `RecordShowEffect`, `useLoadRecordIndexStates`, `useUpdateObjectViewOptions`. #### First migration: ChipFieldDisplay render path - `useChipFieldDisplay` → reads `recordStoreFamilyStateV2` via `useFamilyRecoilValueV2` (was `useRecoilValue(recordStoreFamilyState)`) - `RecordChip` → reads `recordIndexOpenRecordInStateV2` via `useRecoilValueV2` (was `useRecoilValue(recordIndexOpenRecordInState)`) - `Avatar` (twenty-ui) and event handlers (`useOpenRecordInCommandMenu`) left on Recoil — not on the render path / in a different package #### Pattern for migrating additional state 1. Create V2 atom: `createStateV2` or `createFamilyStateV2` 2. Add `jotaiStore.set(v2Atom, value)` at each write site 3. Switch readers to `useRecoilValueV2(v2Atom)` 4. Once all readers are migrated, remove the Recoil atom and dual-writes #### Why not jotai-recoil-adapter? Evaluated [jotai-recoil-adapter](https://github.com/clockelliptic/jotai-recoil-adapter) — not production-ready (21 open issues, no React 19, forces providerless mode, missing types). We built a purpose-built thin layer instead.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { type FieldDateMetadataSettings } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { dateLocaleStateV2 } from '~/localization/states/dateLocaleStateV2';
|
||||
import { formatDateString } from '~/utils/string/formatDateString';
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
@@ -12,7 +12,7 @@ type DateDisplayProps = {
|
||||
};
|
||||
export const DateDisplay = ({ value, dateFieldSettings }: DateDisplayProps) => {
|
||||
const { dateFormat } = useContext(UserContext);
|
||||
const dateLocale = useRecoilValue(dateLocaleState);
|
||||
const dateLocale = useRecoilValueV2(dateLocaleStateV2);
|
||||
|
||||
const formattedDate = formatDateString({
|
||||
value,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { type FieldDateMetadataSettings } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { TimeZoneAbbreviation } from '@/ui/input/components/internal/date/components/TimeZoneAbbreviation';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Temporal } from 'temporal-polyfill';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { dateLocaleStateV2 } from '~/localization/states/dateLocaleStateV2';
|
||||
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
@@ -24,7 +24,7 @@ export const DateTimeDisplay = ({
|
||||
dateFieldSettings,
|
||||
}: DateTimeDisplayProps) => {
|
||||
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
||||
const dateLocale = useRecoilValue(dateLocaleState);
|
||||
const dateLocale = useRecoilValueV2(dateLocaleStateV2);
|
||||
|
||||
const formattedDate = formatDateTimeString({
|
||||
value,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { downloadFile } from '@/activities/files/utils/downloadFile';
|
||||
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
|
||||
import { isAttachmentPreviewEnabledStateV2 } from '@/client-config/states/isAttachmentPreviewEnabledStateV2';
|
||||
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { FileChip } from '@/ui/field/display/components/FileChip';
|
||||
import { UploadFileChip } from '@/ui/field/display/components/UploadFileChip';
|
||||
import { filePreviewState } from '@/ui/field/display/states/filePreviewState';
|
||||
import { filePreviewStateV2 } from '@/ui/field/display/states/filePreviewStateV2';
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type FilesDisplayProps = {
|
||||
@@ -21,9 +22,9 @@ export const FilesDisplay = ({
|
||||
isUploadWindowOpen = false,
|
||||
isFileUploading = false,
|
||||
}: FilesDisplayProps) => {
|
||||
const setFilePreview = useSetRecoilState(filePreviewState);
|
||||
const isAttachmentPreviewEnabled = useRecoilValue(
|
||||
isAttachmentPreviewEnabledState,
|
||||
const setFilePreview = useSetRecoilStateV2(filePreviewStateV2);
|
||||
const isAttachmentPreviewEnabled = useRecoilValueV2(
|
||||
isAttachmentPreviewEnabledStateV2,
|
||||
);
|
||||
|
||||
const handlePreview = (file: FieldFilesValue) => {
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
import { downloadFile } from '@/activities/files/utils/downloadFile';
|
||||
import { filePreviewState } from '@/ui/field/display/states/filePreviewState';
|
||||
import { filePreviewStateV2 } from '@/ui/field/display/states/filePreviewStateV2';
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { lazy, Suspense, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconDownload, IconX } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
@@ -72,7 +72,7 @@ const StyledLoadingText = styled.div`
|
||||
|
||||
export const GlobalFilePreviewModal = (): JSX.Element | null => {
|
||||
const { t } = useLingui();
|
||||
const [filePreview, setFilePreview] = useRecoilState(filePreviewState);
|
||||
const [filePreview, setFilePreview] = useRecoilStateV2(filePreviewStateV2);
|
||||
const { openModal, closeModal } = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
|
||||
|
||||
export const filePreviewStateV2 = createStateV2<FieldFilesValue | null>({
|
||||
key: 'filePreviewStateV2',
|
||||
defaultValue: null,
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
|
||||
|
||||
export const useFamilyRecoilValueV2 = <ValueType, FamilyKey>(
|
||||
familyState: FamilyStateV2<ValueType, FamilyKey>,
|
||||
familyKey: FamilyKey,
|
||||
): ValueType => {
|
||||
return useAtomValue(familyState.atomFamily(familyKey));
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { type WritableFamilySelectorV2 } from '@/ui/utilities/state/jotai/types/WritableFamilySelectorV2';
|
||||
|
||||
export const useFamilySelectorStateV2 = <ValueType, FamilyKey>(
|
||||
familySelector: WritableFamilySelectorV2<ValueType, FamilyKey>,
|
||||
familyKey: FamilyKey,
|
||||
): [
|
||||
ValueType,
|
||||
(value: ValueType | ((prev: ValueType) => ValueType)) => void,
|
||||
] => {
|
||||
return useAtom(familySelector.selectorFamily(familyKey));
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { type FamilySelectorV2 } from '@/ui/utilities/state/jotai/types/FamilySelectorV2';
|
||||
import { type WritableFamilySelectorV2 } from '@/ui/utilities/state/jotai/types/WritableFamilySelectorV2';
|
||||
|
||||
export const useFamilySelectorValueV2 = <ValueType, FamilyKey>(
|
||||
familySelector:
|
||||
| FamilySelectorV2<ValueType, FamilyKey>
|
||||
| WritableFamilySelectorV2<ValueType, FamilyKey>,
|
||||
familyKey: FamilyKey,
|
||||
): ValueType => {
|
||||
return useAtomValue(familySelector.selectorFamily(familyKey));
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
|
||||
import { type WritableSelectorV2 } from '@/ui/utilities/state/jotai/types/WritableSelectorV2';
|
||||
|
||||
export const useRecoilStateV2 = <ValueType>(
|
||||
state: StateV2<ValueType> | WritableSelectorV2<ValueType>,
|
||||
): [
|
||||
ValueType,
|
||||
(value: ValueType | ((prev: ValueType) => ValueType)) => void,
|
||||
] => {
|
||||
return useAtom(state.atom);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { type SelectorV2 } from '@/ui/utilities/state/jotai/types/SelectorV2';
|
||||
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
|
||||
import { type WritableSelectorV2 } from '@/ui/utilities/state/jotai/types/WritableSelectorV2';
|
||||
|
||||
export const useRecoilValueV2 = <ValueType>(
|
||||
state:
|
||||
| StateV2<ValueType>
|
||||
| SelectorV2<ValueType>
|
||||
| WritableSelectorV2<ValueType>,
|
||||
): ValueType => {
|
||||
return useAtomValue(state.atom);
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { useSetAtom } from 'jotai';
|
||||
|
||||
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
|
||||
|
||||
export const useSetFamilyRecoilStateV2 = <ValueType, FamilyKey>(
|
||||
familyState: FamilyStateV2<ValueType, FamilyKey>,
|
||||
familyKey: FamilyKey,
|
||||
): ((value: ValueType | ((prev: ValueType) => ValueType)) => void) => {
|
||||
return useSetAtom(familyState.atomFamily(familyKey));
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { useSetAtom } from 'jotai';
|
||||
|
||||
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
|
||||
|
||||
export const useSetRecoilStateV2 = <ValueType>(
|
||||
state: StateV2<ValueType>,
|
||||
): ((value: ValueType | ((prev: ValueType) => ValueType)) => void) => {
|
||||
return useSetAtom(state.atom);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createStore } from 'jotai';
|
||||
|
||||
export const jotaiStore = createStore();
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type Atom } from 'jotai';
|
||||
|
||||
export type FamilySelectorV2<ValueType, FamilyKey> = {
|
||||
type: 'FamilySelectorV2';
|
||||
key: string;
|
||||
selectorFamily: (key: FamilyKey) => Atom<ValueType>;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { type WritableAtom } from 'jotai';
|
||||
|
||||
type JotaiWritableAtom<ValueType> = WritableAtom<
|
||||
ValueType,
|
||||
[ValueType | ((prev: ValueType) => ValueType)],
|
||||
void
|
||||
>;
|
||||
|
||||
export type FamilyStateV2<ValueType, FamilyKey> = {
|
||||
type: 'FamilyStateV2';
|
||||
key: string;
|
||||
atomFamily: (key: FamilyKey) => JotaiWritableAtom<ValueType>;
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
|
||||
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
|
||||
|
||||
export type SelectorGetterV2 = {
|
||||
get: {
|
||||
<ValueType>(state: StateV2<ValueType>): ValueType;
|
||||
<ValueType, FamilyKey>(
|
||||
familyState: FamilyStateV2<ValueType, FamilyKey>,
|
||||
familyKey: FamilyKey,
|
||||
): ValueType;
|
||||
};
|
||||
};
|
||||
|
||||
export type SelectorSetterV2 = SelectorGetterV2 & {
|
||||
set: {
|
||||
<ValueType>(
|
||||
state: StateV2<ValueType>,
|
||||
value: ValueType | ((prev: ValueType) => ValueType),
|
||||
): void;
|
||||
<ValueType, FamilyKey>(
|
||||
familyState: FamilyStateV2<ValueType, FamilyKey>,
|
||||
familyKey: FamilyKey,
|
||||
value: ValueType | ((prev: ValueType) => ValueType),
|
||||
): void;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type Atom } from 'jotai';
|
||||
|
||||
export type SelectorV2<ValueType> = {
|
||||
type: 'SelectorV2';
|
||||
key: string;
|
||||
atom: Atom<ValueType>;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { type WritableAtom } from 'jotai';
|
||||
|
||||
export type StateV2<ValueType> = {
|
||||
type: 'StateV2';
|
||||
key: string;
|
||||
atom: WritableAtom<
|
||||
ValueType,
|
||||
[ValueType | ((prev: ValueType) => ValueType)],
|
||||
void
|
||||
>;
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type WritableAtom } from 'jotai';
|
||||
|
||||
export type WritableFamilySelectorV2<ValueType, FamilyKey> = {
|
||||
type: 'WritableFamilySelectorV2';
|
||||
key: string;
|
||||
selectorFamily: (
|
||||
key: FamilyKey,
|
||||
) => WritableAtom<
|
||||
ValueType,
|
||||
[ValueType | ((prev: ValueType) => ValueType)],
|
||||
void
|
||||
>;
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { type WritableAtom } from 'jotai';
|
||||
|
||||
export type WritableSelectorV2<ValueType> = {
|
||||
type: 'WritableSelectorV2';
|
||||
key: string;
|
||||
atom: WritableAtom<
|
||||
ValueType,
|
||||
[ValueType | ((prev: ValueType) => ValueType)],
|
||||
void
|
||||
>;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { type Getter } from 'jotai';
|
||||
|
||||
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
|
||||
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
|
||||
|
||||
export const buildGetHelper =
|
||||
(jotaiGet: Getter) =>
|
||||
<ValueType, FamilyKey = never>(
|
||||
stateOrFamily: StateV2<ValueType> | FamilyStateV2<ValueType, FamilyKey>,
|
||||
familyKey?: FamilyKey,
|
||||
): ValueType => {
|
||||
if (stateOrFamily.type === 'FamilyStateV2') {
|
||||
return jotaiGet(
|
||||
(stateOrFamily as FamilyStateV2<ValueType, FamilyKey>).atomFamily(
|
||||
familyKey as FamilyKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return jotaiGet((stateOrFamily as StateV2<ValueType>).atom);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { type Setter } from 'jotai';
|
||||
|
||||
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
|
||||
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
|
||||
|
||||
export const buildSetHelper =
|
||||
(jotaiSet: Setter) =>
|
||||
<ValueType, FamilyKey = never>(
|
||||
stateOrFamily: StateV2<ValueType> | FamilyStateV2<ValueType, FamilyKey>,
|
||||
valueOrFamilyKey: ValueType | ((prev: ValueType) => ValueType) | FamilyKey,
|
||||
familyValue?: ValueType | ((prev: ValueType) => ValueType),
|
||||
): void => {
|
||||
if (stateOrFamily.type === 'FamilyStateV2') {
|
||||
jotaiSet(
|
||||
(stateOrFamily as FamilyStateV2<ValueType, FamilyKey>).atomFamily(
|
||||
valueOrFamilyKey as FamilyKey,
|
||||
),
|
||||
familyValue as ValueType | ((prev: ValueType) => ValueType),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
jotaiSet(
|
||||
(stateOrFamily as StateV2<ValueType>).atom,
|
||||
valueOrFamilyKey as ValueType | ((prev: ValueType) => ValueType),
|
||||
);
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { atom, type Atom } from 'jotai';
|
||||
|
||||
import { type FamilySelectorV2 } from '@/ui/utilities/state/jotai/types/FamilySelectorV2';
|
||||
import { type SelectorGetterV2 } from '@/ui/utilities/state/jotai/types/SelectorCallbacksV2';
|
||||
import { buildGetHelper } from '@/ui/utilities/state/jotai/utils/buildGetHelper';
|
||||
|
||||
export const createFamilySelectorV2 = <ValueType, FamilyKey>({
|
||||
key,
|
||||
get,
|
||||
}: {
|
||||
key: string;
|
||||
get: (familyKey: FamilyKey) => (callbacks: SelectorGetterV2) => ValueType;
|
||||
}): FamilySelectorV2<ValueType, FamilyKey> => {
|
||||
const atomCache = new Map<string, Atom<ValueType>>();
|
||||
|
||||
const selectorFamily = (familyKey: FamilyKey): Atom<ValueType> => {
|
||||
const cacheKey =
|
||||
typeof familyKey === 'string' ? familyKey : JSON.stringify(familyKey);
|
||||
|
||||
const existing = atomCache.get(cacheKey);
|
||||
|
||||
if (existing !== undefined) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const getForKey = get(familyKey);
|
||||
|
||||
const derivedAtom = atom((jotaiGet) => {
|
||||
const getHelper = buildGetHelper(jotaiGet);
|
||||
|
||||
return getForKey({ get: getHelper });
|
||||
});
|
||||
|
||||
derivedAtom.debugLabel = `${key}__${cacheKey}`;
|
||||
atomCache.set(cacheKey, derivedAtom);
|
||||
|
||||
return derivedAtom;
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'FamilySelectorV2',
|
||||
key,
|
||||
selectorFamily,
|
||||
};
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import { type FamilyStateV2 } from '@/ui/utilities/state/jotai/types/FamilyStateV2';
|
||||
|
||||
export const createFamilyStateV2 = <ValueType, FamilyKey>({
|
||||
key,
|
||||
defaultValue,
|
||||
}: {
|
||||
key: string;
|
||||
defaultValue: ValueType;
|
||||
}): FamilyStateV2<ValueType, FamilyKey> => {
|
||||
const atomCache = new Map<
|
||||
string,
|
||||
ReturnType<FamilyStateV2<ValueType, FamilyKey>['atomFamily']>
|
||||
>();
|
||||
|
||||
const familyFunction = (
|
||||
familyKey: FamilyKey,
|
||||
): ReturnType<FamilyStateV2<ValueType, FamilyKey>['atomFamily']> => {
|
||||
const cacheKey =
|
||||
typeof familyKey === 'string' ? familyKey : JSON.stringify(familyKey);
|
||||
|
||||
const existing = atomCache.get(cacheKey);
|
||||
|
||||
if (existing !== undefined) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const baseAtom = atom(defaultValue);
|
||||
baseAtom.debugLabel = `${key}__${cacheKey}`;
|
||||
atomCache.set(cacheKey, baseAtom);
|
||||
|
||||
return baseAtom;
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'FamilyStateV2',
|
||||
key,
|
||||
atomFamily: familyFunction,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import { type SelectorGetterV2 } from '@/ui/utilities/state/jotai/types/SelectorCallbacksV2';
|
||||
import { type SelectorV2 } from '@/ui/utilities/state/jotai/types/SelectorV2';
|
||||
import { buildGetHelper } from '@/ui/utilities/state/jotai/utils/buildGetHelper';
|
||||
|
||||
export const createSelectorV2 = <ValueType>({
|
||||
key,
|
||||
get,
|
||||
}: {
|
||||
key: string;
|
||||
get: (callbacks: SelectorGetterV2) => ValueType;
|
||||
}): SelectorV2<ValueType> => {
|
||||
const derivedAtom = atom((jotaiGet) => {
|
||||
const getHelper = buildGetHelper(jotaiGet);
|
||||
|
||||
return get({ get: getHelper });
|
||||
});
|
||||
|
||||
derivedAtom.debugLabel = key;
|
||||
|
||||
return {
|
||||
type: 'SelectorV2',
|
||||
key,
|
||||
atom: derivedAtom,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import { type StateV2 } from '@/ui/utilities/state/jotai/types/StateV2';
|
||||
|
||||
export const createStateV2 = <ValueType>({
|
||||
key,
|
||||
defaultValue,
|
||||
}: {
|
||||
key: string;
|
||||
defaultValue: ValueType;
|
||||
}): StateV2<ValueType> => {
|
||||
const baseAtom = atom(defaultValue);
|
||||
baseAtom.debugLabel = key;
|
||||
|
||||
return {
|
||||
type: 'StateV2',
|
||||
key,
|
||||
atom: baseAtom,
|
||||
};
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { atom, type WritableAtom } from 'jotai';
|
||||
|
||||
import {
|
||||
type SelectorGetterV2,
|
||||
type SelectorSetterV2,
|
||||
} from '@/ui/utilities/state/jotai/types/SelectorCallbacksV2';
|
||||
import { type WritableFamilySelectorV2 } from '@/ui/utilities/state/jotai/types/WritableFamilySelectorV2';
|
||||
import { buildGetHelper } from '@/ui/utilities/state/jotai/utils/buildGetHelper';
|
||||
import { buildSetHelper } from '@/ui/utilities/state/jotai/utils/buildSetHelper';
|
||||
|
||||
export const createWritableFamilySelectorV2 = <ValueType, FamilyKey>({
|
||||
key,
|
||||
get,
|
||||
set,
|
||||
}: {
|
||||
key: string;
|
||||
get: (familyKey: FamilyKey) => (callbacks: SelectorGetterV2) => ValueType;
|
||||
set: (
|
||||
familyKey: FamilyKey,
|
||||
) => (callbacks: SelectorSetterV2, newValue: ValueType) => void;
|
||||
}): WritableFamilySelectorV2<ValueType, FamilyKey> => {
|
||||
const atomCache = new Map<
|
||||
string,
|
||||
WritableAtom<
|
||||
ValueType,
|
||||
[ValueType | ((prev: ValueType) => ValueType)],
|
||||
void
|
||||
>
|
||||
>();
|
||||
|
||||
const selectorFamily = (
|
||||
familyKey: FamilyKey,
|
||||
): WritableAtom<
|
||||
ValueType,
|
||||
[ValueType | ((prev: ValueType) => ValueType)],
|
||||
void
|
||||
> => {
|
||||
const cacheKey =
|
||||
typeof familyKey === 'string' ? familyKey : JSON.stringify(familyKey);
|
||||
|
||||
const existing = atomCache.get(cacheKey);
|
||||
|
||||
if (existing !== undefined) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const getForKey = get(familyKey);
|
||||
const setForKey = set(familyKey);
|
||||
|
||||
const derivedAtom = atom(
|
||||
(jotaiGet) => {
|
||||
const getHelper = buildGetHelper(jotaiGet);
|
||||
|
||||
return getForKey({ get: getHelper });
|
||||
},
|
||||
(
|
||||
jotaiGet,
|
||||
jotaiSet,
|
||||
valueOrUpdater: ValueType | ((prev: ValueType) => ValueType),
|
||||
) => {
|
||||
const getHelper = buildGetHelper(jotaiGet);
|
||||
const setHelper = buildSetHelper(jotaiSet);
|
||||
|
||||
const resolvedValue =
|
||||
typeof valueOrUpdater === 'function'
|
||||
? (valueOrUpdater as (prev: ValueType) => ValueType)(
|
||||
getForKey({ get: getHelper }),
|
||||
)
|
||||
: valueOrUpdater;
|
||||
|
||||
setForKey({ get: getHelper, set: setHelper }, resolvedValue);
|
||||
},
|
||||
);
|
||||
|
||||
derivedAtom.debugLabel = `${key}__${cacheKey}`;
|
||||
atomCache.set(cacheKey, derivedAtom);
|
||||
|
||||
return derivedAtom;
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'WritableFamilySelectorV2',
|
||||
key,
|
||||
selectorFamily,
|
||||
};
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import {
|
||||
type SelectorGetterV2,
|
||||
type SelectorSetterV2,
|
||||
} from '@/ui/utilities/state/jotai/types/SelectorCallbacksV2';
|
||||
import { type WritableSelectorV2 } from '@/ui/utilities/state/jotai/types/WritableSelectorV2';
|
||||
import { buildGetHelper } from '@/ui/utilities/state/jotai/utils/buildGetHelper';
|
||||
import { buildSetHelper } from '@/ui/utilities/state/jotai/utils/buildSetHelper';
|
||||
|
||||
export const createWritableSelectorV2 = <ValueType>({
|
||||
key,
|
||||
get,
|
||||
set,
|
||||
}: {
|
||||
key: string;
|
||||
get: (callbacks: SelectorGetterV2) => ValueType;
|
||||
set: (callbacks: SelectorSetterV2, newValue: ValueType) => void;
|
||||
}): WritableSelectorV2<ValueType> => {
|
||||
const derivedAtom = atom(
|
||||
(jotaiGet) => {
|
||||
const getHelper = buildGetHelper(jotaiGet);
|
||||
|
||||
return get({ get: getHelper });
|
||||
},
|
||||
(
|
||||
jotaiGet,
|
||||
jotaiSet,
|
||||
valueOrUpdater: ValueType | ((prev: ValueType) => ValueType),
|
||||
) => {
|
||||
const getHelper = buildGetHelper(jotaiGet);
|
||||
const setHelper = buildSetHelper(jotaiSet);
|
||||
|
||||
const resolvedValue =
|
||||
typeof valueOrUpdater === 'function'
|
||||
? (valueOrUpdater as (prev: ValueType) => ValueType)(
|
||||
get({ get: getHelper }),
|
||||
)
|
||||
: valueOrUpdater;
|
||||
|
||||
set({ get: getHelper, set: setHelper }, resolvedValue);
|
||||
},
|
||||
);
|
||||
|
||||
derivedAtom.debugLabel = key;
|
||||
|
||||
return {
|
||||
type: 'WritableSelectorV2',
|
||||
key,
|
||||
atom: derivedAtom,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user