-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(jotai): Added
createAtomStore
functionality
- Loading branch information
1 parent
b252414
commit 9031b61
Showing
14 changed files
with
588 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { atom } from "jotai"; | ||
|
||
import type { WritableAtom } from "jotai/vanilla"; | ||
|
||
type WrapFn<T> = T extends (...args: infer _A) => infer _R ? { __fn: T } : T; | ||
|
||
const wrapFn = <T>(fnOrValue: T): WrapFn<T> => | ||
(typeof fnOrValue === "function" ? { __fn: fnOrValue } : fnOrValue) as any; | ||
|
||
type UnwrapFn<T> = T extends { __fn: infer U } ? U : T; | ||
|
||
const unwrapFn = <T>(wrappedFnOrValue: T): UnwrapFn<T> => | ||
(wrappedFnOrValue && | ||
typeof wrappedFnOrValue === "object" && | ||
"__fn" in wrappedFnOrValue | ||
? wrappedFnOrValue.__fn | ||
: wrappedFnOrValue) as any; | ||
|
||
/** | ||
* Create an atom with a wrapper that allows functions as values. | ||
* | ||
* @remarks | ||
* Jotai atoms don't allow functions as values by default. This function is a | ||
* drop-in replacement for `atom` that wraps functions in an object while | ||
* leaving non-functions unchanged. The wrapper object should be completely | ||
* invisible to consumers of the atom. | ||
* | ||
* @param initialValue - The initial value of the atom | ||
* @returns An atom with a wrapper that allows functions as values. | ||
*/ | ||
export const atomWithWrapper = <TValue>( | ||
initialValue: TValue | ||
): WritableAtom<TValue, [TValue], void> => { | ||
const baseAtom = atom(wrapFn(initialValue)); | ||
|
||
return atom( | ||
get => unwrapFn(get(baseAtom)) as TValue, | ||
(_get, set, value) => set(baseAtom, wrapFn(value)) | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
export * from "./use-hydrate-store"; | ||
export * from "./use-prepare-atoms"; | ||
export * from "./use-sync-store"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { useHydrateAtoms } from "jotai/utils"; | ||
import { | ||
SimpleWritableAtomRecord, | ||
UseHydrateAtoms | ||
} from "../utilities/create-atom-store"; | ||
|
||
/** | ||
* Hydrate atoms with initial values for SSR. | ||
*/ | ||
export const useHydrateStore = ( | ||
atoms: SimpleWritableAtomRecord<any>, | ||
initialValues: Parameters<UseHydrateAtoms<any>>[0], | ||
options: Parameters<UseHydrateAtoms<any>>[1] = {} | ||
) => { | ||
const values: any[] = []; | ||
|
||
for (const key of Object.keys(atoms)) { | ||
const initialValue = initialValues[key]; | ||
|
||
if (initialValue !== undefined) { | ||
values.push([atoms[key], initialValue]); | ||
} | ||
} | ||
|
||
useHydrateAtoms(values, options); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { useSetAtom } from "jotai"; | ||
import { useEffect } from "react"; | ||
import { | ||
SimpleWritableAtomRecord, | ||
UseSyncAtoms | ||
} from "../utilities/create-atom-store"; | ||
|
||
/** | ||
* Update atoms with new values on changes. | ||
*/ | ||
export const useSyncStore = ( | ||
atoms: SimpleWritableAtomRecord<any>, | ||
values: any, | ||
{ store }: Parameters<UseSyncAtoms<any>>[1] = {} | ||
) => { | ||
for (const key of Object.keys(atoms)) { | ||
const value = values[key]; | ||
const set = useSetAtom(atoms[key]!, { store }); | ||
|
||
useEffect(() => { | ||
if (value !== undefined && value !== null) { | ||
set(value); | ||
} | ||
}, [set, value]); | ||
} | ||
}; |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { Provider as AtomProvider } from "jotai"; | ||
import { createStore } from "jotai/vanilla"; | ||
import { | ||
ComponentProps, | ||
createContext, | ||
FC, | ||
useContext, | ||
useEffect, | ||
useMemo, | ||
useState | ||
} from "react"; | ||
import { useHydrateStore, useSyncStore } from "../hooks"; | ||
import { JotaiStore, SimpleWritableAtomRecord } from "./create-atom-store"; | ||
|
||
type AtomProviderProps = ComponentProps<typeof AtomProvider>; | ||
|
||
const getFullyQualifiedScope = (storeName: string, scope: string) => { | ||
return `${storeName}:${scope}`; | ||
}; | ||
|
||
/** | ||
* Context mapping store name and scope to store. The 'provider' scope is used | ||
* to reference any provider belonging to the store, regardless of scope. | ||
*/ | ||
const PROVIDER_SCOPE = "provider"; | ||
const AtomStoreContext = createContext<Map<string, JotaiStore>>(new Map()); | ||
|
||
/** | ||
* Tries to find a store in each of the following places, in order: | ||
* 1. The store context, matching the store name and scope | ||
* 2. The store context, matching the store name and 'provider' scope | ||
* 3. Otherwise, return undefined | ||
*/ | ||
export const useAtomStore = ( | ||
storeName: string, | ||
scope: string = PROVIDER_SCOPE, | ||
warnIfUndefined: boolean = true | ||
): JotaiStore | undefined => { | ||
const storeContext = useContext(AtomStoreContext); | ||
const store = | ||
storeContext.get(getFullyQualifiedScope(storeName, scope)) ?? | ||
storeContext.get(getFullyQualifiedScope(storeName, PROVIDER_SCOPE)); | ||
|
||
if (!store && warnIfUndefined) { | ||
console.warn( | ||
`Tried to access jotai store '${storeName}' outside of a matching provider.` | ||
); | ||
} | ||
|
||
return store; | ||
}; | ||
|
||
export type ProviderProps<T extends object> = AtomProviderProps & | ||
Partial<T> & { | ||
scope?: string; | ||
initialValues?: Partial<T>; | ||
resetKey?: any; | ||
}; | ||
|
||
export const HydrateAtoms = <T extends object>({ | ||
initialValues, | ||
children, | ||
store, | ||
atoms, | ||
...props | ||
}: Omit<ProviderProps<T>, "scope"> & { | ||
atoms: SimpleWritableAtomRecord<T>; | ||
}) => { | ||
useHydrateStore(atoms, { ...initialValues, ...props } as any, { | ||
store | ||
}); | ||
useSyncStore(atoms, props as any, { | ||
store | ||
}); | ||
|
||
return <>{children}</>; | ||
}; | ||
|
||
/** | ||
* Creates a generic provider for a jotai store. | ||
* - `initialValues`: Initial values for the store. | ||
* - `props`: Dynamic values for the store. | ||
*/ | ||
export const createAtomProvider = <T extends object, N extends string = "">( | ||
storeScope: N, | ||
atoms: SimpleWritableAtomRecord<T>, | ||
options: { effect?: FC } = {} | ||
) => { | ||
const Effect = options.effect; | ||
|
||
// eslint-disable-next-line react/display-name | ||
return ({ store, scope, children, resetKey, ...props }: ProviderProps<T>) => { | ||
const [storeState, setStoreState] = useState<JotaiStore>(createStore()); | ||
|
||
useEffect(() => { | ||
if (resetKey) { | ||
setStoreState(createStore()); | ||
} | ||
}, [resetKey]); | ||
|
||
const previousStoreContext = useContext(AtomStoreContext); | ||
|
||
const storeContext = useMemo(() => { | ||
const newStoreContext = new Map(previousStoreContext); | ||
|
||
if (scope) { | ||
// Make the store findable by its fully qualified scope | ||
newStoreContext.set( | ||
getFullyQualifiedScope(storeScope, scope), | ||
storeState | ||
); | ||
} | ||
|
||
// Make the store findable by its store name alone | ||
newStoreContext.set( | ||
getFullyQualifiedScope(storeScope, PROVIDER_SCOPE), | ||
storeState | ||
); | ||
|
||
return newStoreContext; | ||
}, [previousStoreContext, scope, storeState]); | ||
|
||
return ( | ||
<AtomStoreContext.Provider value={storeContext}> | ||
<AtomProvider store={storeState}> | ||
<HydrateAtoms store={storeState} atoms={atoms} {...(props as any)}> | ||
{!!Effect && <Effect />} | ||
|
||
{children} | ||
</HydrateAtoms> | ||
</AtomProvider> | ||
</AtomStoreContext.Provider> | ||
); | ||
}; | ||
}; |
Oops, something went wrong.