@zubridge/electron
Version:
A streamlined state management library for Electron applications using Zustand.
334 lines (248 loc) • 9.3 kB
Markdown
# Main Process
This document describes how to set up and use the `/electron` package in the main process of your Electron application.
## Bridge Setup Options
Zubridge provides multiple ways to connect your main process state to renderer processes. Choose the approach that best fits your state management solution.
### Option 1: Zustand Integration
For Zustand-based applications (recommended):
```ts
// `src/main/index.ts`
import { app, BrowserWindow } from 'electron';
import { createZustandBridge } from '@zubridge/electron/main';
import { store } from './store.js'; // Your Zustand store
// Create main window
const mainWindow = new BrowserWindow({
/* config */
});
// Option A: Instantiate bridge with initial window
const { unsubscribe, subscribe, dispatch, getSubscribedWindows } = createZustandBridge(store, [mainWindow]);
// Option B: Instantiate bridge without windows, subscribe later
const bridge = createZustandBridge(store);
const subscription = bridge.subscribe([mainWindow]);
// Use dispatch to send actions
dispatch('COUNTER:INCREMENT');
// Unsubscribe on quit
app.on('quit', unsubscribe);
```
### Option 2: Redux Integration
For Redux-based applications:
```ts
// `src/main/index.ts`
import { app, BrowserWindow } from 'electron';
import { createReduxBridge } from '@zubridge/electron/main';
import { store } from './redux-store.js'; // Your Redux store
// Create main window
const mainWindow = new BrowserWindow({
/* config */
});
// Option A: Instantiate bridge with Redux store and initial window
const { unsubscribe, subscribe, dispatch, getSubscribedWindows } = createReduxBridge(store, [mainWindow]);
// Option B: Instantiate bridge without windows, subscribe later
const bridge = createReduxBridge(store);
const subscription = bridge.subscribe([mainWindow]);
// Use dispatch to send actions
dispatch({ type: 'COUNTER:INCREMENT' });
// Unsubscribe on quit
app.on('quit', unsubscribe);
```
### Option 3: Custom State Managers
For other state management solutions, implement the `StateManager` interface:
```ts
// `src/main/state-adapter.ts`
import { createCoreBridge, createDispatch } from '@zubridge/electron/main';
import { StateManager, Action } from '@zubridge/types';
import { someThirdPartyStateManager } from 'third-party-library';
// Create an adapter for your state solution
function createStateAdapter(): StateManager<MyAppState> {
const existingStore = someThirdPartyStateManager({
initialState: { counter: 0 },
// other configuration...
});
return {
// Map to existing store's state retrieval method
getState: () => existingStore.getState(),
// Map to existing store's subscription method
subscribe: (listener) => {
return existingStore.subscribe((newState) => listener(newState));
},
// Map actions to existing store's dispatch method
processAction: (action) => {
existingStore.dispatch(action);
},
};
}
// `src/main/index.ts`
import { app, BrowserWindow } from 'electron';
import { createCoreBridge, createDispatch } from '@zubridge/electron/main';
import { createStateAdapter } from './state-adapter.js';
// Create main window
const mainWindow = new BrowserWindow({
/* config */
});
// Create adapter and connect to bridge
const stateManager = createStateAdapter();
// Option A: Connect bridge with initial window
const bridge = createCoreBridge(stateManager, [mainWindow]);
// Option B: Connect bridge without windows, subscribe later
const bridge = createCoreBridge(stateManager);
const subscription = bridge.subscribe([mainWindow]);
const dispatch = createDispatch(stateManager);
// Use dispatch to send actions
dispatch('COUNTER:INCREMENT');
// Unsubscribe on quit
app.on('quit', bridge.unsubscribe);
```
## Multi-Window Support
All bridge implementations support multiple windows and different types of Electron objects:
```ts
// Create windows
const mainWindow = new BrowserWindow({
/* config */
});
const secondaryWindow = new BrowserWindow({
/* config */
});
// Instantiate bridge with multiple windows
const { subscribe, unsubscribe, getSubscribedWindows } = createZustandBridge(store, [mainWindow, secondaryWindow]);
// Later, create a new window or view
const runtimeView = new WebContentsView({
/* config */
});
// You can use BrowserWindow, BrowserView, WebContentsView, or WebContents directly
const directWebContents = mainWindow.webContents; // Using WebContents directly
// Subscribe the new views to store updates
const subscription = subscribe([runtimeView, directWebContents]);
// When the view is closed, unsubscribe it
runtimeView.webContents.once('destroyed', () => {
subscription.unsubscribe();
});
// Get all currently subscribed window IDs
const subscribedWindowIds = getSubscribedWindows();
console.log('Currently subscribed windows:', subscribedWindowIds);
```
## Advanced Bridge Options
### Using Separate Handlers with Zustand
```ts
// `src/main/index.ts`
import { createZustandBridge } from '@zubridge/electron/main';
import { store } from './store.js';
import { actionHandlers } from '../features/index.js';
// Create handlers for store
const handlers = actionHandlers(store, initialState);
// Instantiate bridge with handlers
createZustandBridge(store, [mainWindow], { handlers });
```
### Using Redux-Style Reducers with Zustand
```ts
// `src/main/index.ts`
import { createZustandBridge } from '@zubridge/electron/main';
import { store } from './store.js';
import { rootReducer } from '../features/index.js';
// Instantiate bridge with reducer
createZustandBridge(store, [mainWindow], { reducer: rootReducer });
```
## Interacting with State
### Direct Store Access
In the main process, you can access the store directly:
```ts
// `src/main/counter/index.ts`
import { store } from '../store.js';
// Get current state
const { counter } = store.getState();
// Update state (Zustand example)
store.setState({ counter: counter + 1 });
```
### Using the Dispatch Helper
The dispatch helper provides a consistent way to trigger actions regardless of your state management solution:
```ts
// Using dispatch returned from the bridge
import { dispatch } from '../bridge.js';
// String action type
dispatch('COUNTER:INCREMENT');
// String action type with payload
dispatch('COUNTER:SET', 42);
// Action object
dispatch({ type: 'COUNTER:RESET', payload: 0 });
```
### Working with Thunks
Thunks provide a way to implement complex logic, especially for async operations:
```ts
// `src/main/counter/actions.ts`
import { dispatch } from '../bridge.js';
// Simple thunk with conditional logic
const incrementIfLessThan = (max) => (getState, dispatch) => {
const { counter } = getState();
if (counter < max) {
dispatch('COUNTER:INCREMENT');
return true;
}
return false;
};
// Usage
dispatch(incrementIfLessThan(10));
```
#### Async Thunk Example
```ts
// Complex thunk with async operations
export const fetchAndUpdateCounter = () => async (getState, dispatch) => {
try {
// Dispatch a loading action
dispatch('UI:SET_LOADING', true);
// Perform async operation
const response = await fetch('https://api.example.com/counter');
const data = await response.json();
// Dispatch results
dispatch('COUNTER:SET', data.value);
return data.value;
} catch (error) {
// Handle errors
dispatch('ERROR:SET', error.message);
return null;
} finally {
dispatch('UI:SET_LOADING', false);
}
};
// Usage
dispatch(fetchAndUpdateCounter());
```
### Using Nested Path Resolution for Handlers
Both Zustand and Redux bridges support nested path resolution for action handlers. This allows you to organize your handlers in a hierarchical structure:
```ts
// `src/main/index.ts`
import { createZustandBridge } from '@zubridge/electron/main';
import { store } from './store.js';
// Create hierarchical handlers
const handlers = {
counter: {
increment: (payload) => {
const step = payload?.step || 1;
store.setState((state) => ({ counter: state.counter + step }));
},
decrement: (payload) => {
const step = payload?.step || 1;
store.setState((state) => ({ counter: state.counter - step }));
},
reset: () => {
store.setState((state) => ({ counter: 0 }));
},
},
theme: {
toggle: () => {
store.setState((state) => ({ isDarkMode: !state.isDarkMode }));
},
set: (isDark) => {
store.setState((state) => ({ isDarkMode: isDark }));
},
},
};
// Instantiate bridge with nested handlers
const bridge = createZustandBridge(store, [mainWindow], { handlers });
// Dispatch using dot notation for paths
bridge.dispatch('counter.increment', { step: 5 }); // Calls handlers.counter.increment
bridge.dispatch('theme.toggle'); // Calls handlers.theme.toggle
// Handler lookup is case insensitive
bridge.dispatch('Counter.Increment'); // Still finds handlers.counter.increment
// Standard handler matching is case insensitive
bridge.dispatch('COUNTER:INCREMENT'); // Matches handlers['COUNTER:INCREMENT'] or handlers['counter:increment']
```
## Legacy API Note
Previous versions used `mainZustandBridge` instead of `createZustandBridge`. This function is still available for backward compatibility, but `createZustandBridge` is recommended for new projects.