jotai-valtio
Version:
123 lines (105 loc) • 3.13 kB
text/typescript
import { atom } from 'jotai/vanilla';
import { proxy, snapshot, subscribe } from 'valtio/vanilla';
import { deepClone } from 'valtio/vanilla/utils';
type Wrapped<T> = { value: T };
type ProxyFn<T> = (obj: Wrapped<T>) => Wrapped<T>;
type Options<T> = {
proxyFn?: ProxyFn<T>;
};
export function mutableAtom<Value>(
initialValue: Value,
options: Options<Value> = defaultOptions,
) {
const valueAtom = atom({ value: initialValue });
if (process.env.NODE_ENV !== 'production') {
valueAtom.debugPrivate = true;
}
const { proxyFn } = { ...defaultOptions, ...options };
const storeAtom = atom<
Store<Value>,
[ActionWithPayload<'setValue', Value> | Action<'getValue'>],
void | Value
>(
(_get, { setSelf }) => {
const store: Store<Value> = {
proxyState: createProxyState(() => store),
getValue: () => setSelf({ type: 'getValue' }) as Value,
setValue: (value: Value) =>
setSelf({ type: 'setValue', payload: value }) as void,
};
return store;
},
(get, set, action) => {
if (action.type === 'setValue') {
set(valueAtom, { value: action.payload });
} else if (action.type === 'getValue') {
return get(valueAtom).value;
}
},
);
if (process.env.NODE_ENV !== 'production') {
storeAtom.debugPrivate = true;
}
/**
* sync the proxy state with the atom
*/
function onChange(getStore: () => Store<Value>) {
return () => {
const { proxyState, getValue, setValue } = getStore();
const { value } = snapshot(proxyState);
if (!Object.is(value, getValue())) {
setValue(value as Awaited<Value>);
}
};
}
/**
* create the proxy state and subscribe to it
*/
function createProxyState(getStore: () => Store<Value>) {
const proxyState = proxyFn({ value: deepClone(initialValue) });
// We never unsubscribe, but it's garbage collectable.
subscribe(proxyState, onChange(getStore), true);
return proxyState;
}
/**
* wrap the proxy state in a proxy to ensure rerender on value change
*/
function wrapProxyState(proxyState: ProxyState<Value>) {
return new Proxy(proxyState, {
get(target, property) {
return target[property as keyof ProxyState<Value>];
},
set(target, property, value) {
if (property === 'value') {
target[property] = value;
return true;
}
return false;
},
});
}
/**
* create an atom that returns the proxy state
*/
const proxyEffectAtom = atom<ProxyState<Value>>((get) => {
get(valueAtom); // subscribe to value updates
const store = get(storeAtom);
return wrapProxyState(store.proxyState);
});
return proxyEffectAtom;
}
const defaultOptions = {
proxyFn: proxy,
};
type Store<Value> = {
proxyState: ProxyState<Value>;
getValue: () => Value;
setValue: (value: Value) => void;
};
export type ProxyState<Value> = { value: Value };
type Action<Type extends string> = {
type: Type;
};
type ActionWithPayload<Type extends string, Payload> = Action<Type> & {
payload: Payload;
};