Omnistac / zedux

:zap: A Molecular State Engine for React
https://Omnistac.github.io/zedux/
MIT License
344 stars 7 forks source link

Is it possible to use Zedux with Electron and to share the store between renderer windows? #105

Open zdubin opened 1 month ago

zdubin commented 1 month ago

Sharing data between Electron windows?

Hello, In Electron the renderer windows can only communicate with the main window. I tried passing

const mainInternals = getInternals();

        window.electron.ipcRenderer.sendMessage('newwin', [
          'window',
          mainInternals,
        ]);

but mainInternals is JSON with a circular structure and I receive 'Error An object could not be cloned.'. I want to have a render window that open other render windows through sendMessage -> ipcMain -> new BrowserWindow, and these windows should share the same state. I'm also struggling to find the documentation for getInternals and setInternals on https://omnistac.github.io/zedux/ as the search never returns?

Could you guide me to the best place to get electron renderer windows using the same store?

bowheart commented 1 month ago

Hey there @zdubin. The kind of state reference sharing you want is only possible when opening child windows via window.open as described in the electron docs here. Then the windows can talk to each other via the object returned from window.open (in the parent window) and window.opener (in the child window). This is the use case that Zedux's getInternals and setInternals are designed for.

When creating multiple BrowserWindow objects from the main process, object reference sharing isn't possible. You have to synchronize state between two (or more) different instances of Zedux. The approach for this varies depending on how you're using Zedux and is unfortunately going to require some manual wiring.

If you're only using stores (created via createStore() similar to old-school Redux), this could be as simple as subscribing to a store in each window and sending the data to the other(s) via the main process. This assumes a basic project structure with little state and doesn't account for race conditions.

If you're using the atomic model, it's possible to write a ZeduxPlugin that listens to state changes in all (or a filtered list of) atoms and translates those state changes into hydrations in all other windows' ecosystems (again via the main process).

If full ecosystem sync is overkill, it's possible to make an injector that keeps individual atom instances in sync with a matching atom instance in all other windows e.g. injectWindowSync(localStore).

You can also try a "single source of truth" approach where the main process has its own ecosystem that holds all the real state and atoms in the child windows only read/write state from/to the main process. One way to accomplish this could be to create a few wrapper injectors around injectStore and injectEffect at least that check whether the current atom instance is running in the main process or the renderer process.

Really, if you want to keep state in sync across windows, I'd recommend using window.open() if possible. But I know that can cause headaches when e.g. saving/restoring the user's window layout. window.open() can also be a pain when using HMR to develop the app locally. This is a big reason why we use OpenFin to manage parent/child windows. I don't know of a lighter, less-intrusive tool that does this for electron.

If window.open() isn't suitable (understandable), then I recommend not trying to keep everything in sync. Keep any synchronization as granular as possible - only sharing state that needs to be shared. But whether that's practical depends on your app.

Some good news for this endeavor in the future: We are planning on resurrecting an old package called "Zedux Sync" (this old thing). We're only just starting to plan it out. I'll be sure to explore adding ipc syncing to its feature set.

The examples I gave are pretty vague. I didn't want to explore any too deeply since they're all fairly complex and I'm not sure which approach would be good for your app. Let me know if any of these approaches sound promising and I can give more specific code examples.

zdubin commented 1 month ago

Thank you for replying so quickly.

Can you give a small example of opening child windows via window.open? I'd like to use the parent/child setInternals/getInternals solution.

bowheart commented 1 month ago

Hey, sure, the gist of this approach is:

// parent window:
import { getInternals } from '@zedux/react'

const childWindow = window.open()
// ...set childWindow contents...
window.ZEDUX_INTERNALS = getInternals()

// child window:
import { setInternals } from '@zedux/react'

// before anything uses Zedux in the child window:
setInternals(window.opener.ZEDUX_INTERNALS)

It also works when opening the child window to a url at the same domain as the parent window.

We have a single helper that runs in all windows and detects which type (parent or child) of window it's running in. Basically this:

// setup-zedux.ts
import { getInternals, setInternals } from '@zedux/react'

export function setupZedux() {
  const isChild = !!window.opener

  if (isChild) {
    setInternals(window.opener.ZEDUX_INTERNALS)
  } else {
    window.ZEDUX_INTERNALS = getInternals()
  }
}
zdubin commented 1 month ago

Thank you so much, I just plunked your example in and it worked! I did need to make this change below as well from your previous conversation with Chris Boon.

import uuid from 'uuid4';

ReactDOM.createRoot(document.getElementById('root'), { identifierPrefix: uuid() }).render(

);