jotaijs / jotai-valtio

Jotai integration library for Valtio
MIT License
6 stars 1 forks source link

[idea] Valtio like updates with Jotai? #4

Closed dmaskasky closed 11 months ago

dmaskasky commented 11 months ago

I'm not sure if this is the right place, but from the repo name only I wanted to do something like this.

const proxyAtom = withProxyEffect(atom(0), { sync: true, proxy });
  ...
const $state = useAtomValue(proxyAtom);
  ...
onClick={() => $state.value++}
const proxyAtom = atomWithProxyEffect(0, null, { sync: true, proxy });
...
const $state = useAtomValue(proxyAtom);
...
onClick={() => $state.value++}

Was this API considered, or is this repo specifically intended for syncing a jotai atom with a valtio proxy?

Example: https://codesandbox.io/s/jotai-demo-forked-skvmhs

What do you think? Would something like this fit in jotai-valtio?

dai-shi commented 11 months ago

I haven't considered such an API. Maybe because I'd simply do proxy.value++.

is this repo specifically intended for syncing a jotai atom with a valtio proxy?

That was my motivation, but it's not limited if someone else has an idea and can take the lead.

On the other hand, if we were able to create jotai-effect, this wouldn't be very useful because we can do it with jotai-effect, I guess.

dmaskasky commented 11 months ago

Welcome back Dai-Shi 😌.

Since state lives in the store in Jotai, this makes it unavailable to convert to a Valtio proxy.

The current api for jotai-valtio allows for Valtio -> Jotai but not Jotai -> Valtio. This is my motivation for introducing a new api. I think only withProxyEffect would be necessary to achieve Jotai -> Valtio.

dai-shi commented 11 months ago

Can't atomEffect https://github.com/pmndrs/jotai/discussions/2136 do Jotai -> Valtio?

dmaskasky commented 11 months ago

The main issue I've been having with using atomEffect this way is in supporting the initial read of the withProxyEffect atom. Because, atomEffects are designed to run async (post read). But I'll keep trying to get this to work.

Here's a simple prototype of withProxyEffect. The main drawback is that the initial read returns undefined. I have a more complex example that fixes this, but it is suffering from another strange bug.

const defaultOptions: Required<ValtioOptions<any>> = {
  sync: true,
  proxyFn: proxy,
};

type ProxyStore<Value> = {
  proxyState: ProxyState<Value>;
  unsubscribe: () => void;
};

export function withProxyEffect<Value>(
  atomToSync: WritableAtom<Value, [SetStateAction<Value>], any>,
  options: ValtioOptions<Value> = defaultOptions
) {
  const proxyAtom = makeProxyAtom(atomToSync, options);
  return makeWithProxyEffect(proxyAtom, atomToSync);
}

export function makeWithProxyEffect<Value>(
  proxyAtom: WritableAtom<ProxyStore<Value> | null, [boolean | ProxyStore<Value> | null], void>,
  atomToSync: WritableAtom<Value, [SetStateAction<Value>], any>
) {
  const derivedAtom = atom((get) => {
    const proxyStore = get(proxyAtom);
    const state = get(atomToSync);
    if (proxyStore === null) return;
    const { proxyState } = proxyStore;
    if (state !== snapshot(proxyState).value) {
      proxyState.value = state as Value;
    }
    return createProxyWrapper(proxyState);
  });
  return derivedAtom;
}

export function makeProxyAtom<Value>(
  atomToSync: WritableAtom<Value, [SetStateAction<Value>], any>,
  options: ValtioOptions<Value> = defaultOptions
) {
  const { sync, proxyFn } = { ...defaultOptions, ...options };

  const proxyAtom = atom<null | ProxyStore<Value>, [boolean | null | ProxyStore<Value>], void>(
    null,
    (get, set, isInit) => {
      if (isInit) {
        const value = get(atomToSync);
        const proxyState = proxyFn({ value }) as ProxyState<Value>;
        const unsubscribe = subscribe(
          proxyState,
          () => {
            const { value } = snapshot(proxyState);
            if (value !== get(atomToSync)) {
              set(atomToSync, value as Value);
            }
          },
          sync
        );
        set(proxyAtom, { proxyState, unsubscribe });
      } else {
        get(proxyAtom)?.unsubscribe?.();
      }
    }
  );
  proxyAtom.onMount = (init) => {
    init(true);
    return () => init(false);
  };
  return proxyAtom;
}

function createProxyWrapper<T>(state: ProxyState<T>): ProxyState<T> {
  return new Proxy(state, {
    get(target, property: string) {
      return target[property as keyof ProxyState<T>] as T;
    },
    set(target, property: string, value: any) {
      if (property === 'value') {
        target[property] = value;
        return true;
      }
      return false;
    },
  });
}
dai-shi commented 11 months ago

Hmm, I wonder if Jotai -> Valtio makes sense for multiple provider use case. ex: https://twitter.com/dai_shi/status/1447892237753278466

dmaskasky commented 11 months ago

Hmm, I wonder if Jotai -> Valtio makes sense for multiple provider use case. ex: https://twitter.com/dai_shi/status/1447892237753278466

The proxy is treated as Jotai state so it cannot be accessed outside of the react component tree. Does this feel natural? ex: https://codesandbox.io/s/pensive-wood-lspvpd

Perhaps its better to think of this utility as "updating Jotai atoms like how Valtio does it", rather than "converting Jotai into Valtio".

dai-shi commented 11 months ago

Perhaps its better to think of this utility as "updating Jotai atoms like how Valtio does it", rather than "converting Jotai into Valtio".

Yeah, that makes sense.

Would something like this fit in jotai-valtio?

It's different from the original motivation, but it's another style of integration, so it should fit.

I wouldn't mind adding it here if you keep maintaining it, but my suspicion is whether if people would use it and if I'd actually recommend it, because if you design Jotai atoms "atomic", deep proxy updating shouldn't be necessary.

dmaskasky commented 11 months ago

I've submitted PR https://github.com/jotaijs/jotai-valtio/pull/5. Happy to continue this discussion in the PR. Closing this ticket for now.