Skip to content

Commit

Permalink
docs: rework getting started
Browse files Browse the repository at this point in the history
  • Loading branch information
goosewobbler committed Dec 8, 2024
1 parent 355984a commit b5df9c1
Showing 1 changed file with 104 additions and 76 deletions.
180 changes: 104 additions & 76 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,134 +8,162 @@ npm i zutron zustand

Or use your dependency manager of choice, e.g. `pnpm`, `yarn`.

The following instructions assume you are using TypeScript.
The code fragments in this documentation are based on the minimal working (TypeScript) examples found in the [apps directory](../apps).

#### Create Store

First, create your Zustand store using `zustand/vanilla` in the main process:
First, create the Zustand store for your application using `zustand/vanilla` in the main process. If you are using TS, provide your application state type:

```ts
```ts annotate
// `src/main/store.ts`
import { createStore } from 'zustand/vanilla';
import type { AppState } from '../features/index.js';

const initialState: AppState = {
counter: 0,
ui: { ... }
};

store = createStore<AppState>()(() => initialState);
// create app store
export const store = createStore<AppState>()(() => initialState);
```

#### Initialize Bridge in Main process
#### Instantiate Bridge in Main process

In the main process, the bridge needs your store and an array of window or view objects for your app. `BrowserWindow`, `BrowserView` and `WebContentsView` objects are supported.
In the main process, the bridge needs to be instantiated with your store and an array of window or view objects for your app. `BrowserWindow`, `BrowserView` and `WebContentsView` objects are supported.

The bridge returns an `unsubscribe` function which can be used to deactivate the bridge, and a `subscribe` function which can be used to enable any additional windows or views created after the bridge was initialised.

So, for a single window application:

```ts
```ts annotate
// `src/main/index.ts`
import { app, BrowserWindow } from 'electron';
import { mainZustandBridge } from 'zutron/main';

// create mainWindow
const mainWindow = new BrowserWindow(windowConfig);
// create main window
const mainWindow = new BrowserWindow({ ... });

// instantiate bridge
const { unsubscribe } = mainZustandBridge(store, [mainWindow]);

// unsubscribe on quit
app.on('quit', unsubscribe);
```

#### Initialize Bridge in Preload

Next, initialise the bridge in your preload script. Here the bridge needs the type of your app state. The bridge initialiser will return a set of handlers which should be exposed to the renderer process via the `contextBridge` module.
For a multi-window application:

```ts
import { contextBridge } from 'electron';
import { preloadZustandBridge } from 'zutron/preload';

import type { AppState } from '../features/index.js';

export const { handlers } = preloadZustandBridge<AppState>();
```ts annotate
// `src/main/index.ts`
import { app, BrowserWindow, WebContentsView } from 'electron';
import { mainZustandBridge } from 'zutron/main';

contextBridge.exposeInMainWorld('zutron', handlers);
```
// create main window
const mainWindow = new BrowserWindow({ ... });

#### Create hook in Renderer process
// create secondary window
const secondaryWindow = new BrowserWindow({ ... });

Finally, in the renderer process you will need to create the `useStore` hook:
// instantiate bridge
const { unsubscribe, subscribe } = mainZustandBridge(store, [mainWindow, secondaryWindow]);

`/renderer/hooks/useStore.ts`
// unsubscribe on quit
app.on('quit', unsubscribe);

```ts
import { createUseStore } from 'zutron';
import { AppState } from '../../features/index.js';
// create a view some time after the bridge has been instantiated
const runtimeView = new WebContentsView({ ... });

export const useStore = createUseStore<AppState>(window.zutron);
// subscribe the view to store updates
subscribe([runtimeView]);
```

#### Accessing the Store in the Renderer Process
By default the main process bridge assumes your store handler functions are located on the store object.

In the renderer process you should now be able to access the store via the `useStore` hook:
If you keep your store handler functions separate from the store then you will need to pass them in as an option:

```ts
const counter = useStore((x) => x.counter);
```
```ts annotate
// `src/main/index.ts`
import { mainZustandBridge } from 'zutron/main';
import { actionHandlers } from '../features/index.js';

You can use the `useDispatch` hook to dispatch actions and thunks to the store:
// create handlers for store
const handlers = actionHandlers(store, initialState);

```ts
const dispatch = useDispatch(window.zutron);
const onIncrement = () => dispatch('COUNTER:INCREMENT');
// instantiate bridge
const { unsubscribe } = mainZustandBridge(store, [mainWindow], { handlers });
```

If you are using a thunk, the dispatch function and the store are passed in:
Alternatively, if you are using Redux-style reducers, you should pass in the root reducer:

```ts
const onIncrementThunk = (getState, dispatch) => {
// do something based on the store
dispatch('COUNTER:INCREMENT');
```ts annotate
// `src/features/index.ts`
import type { Reducer } from 'zutron';
import { counterReducer } from '../features/counter/index.js';
import { uiReducer } from '../features/ui/index.js';

export type AppState = {
counter: number
ui: { ... }
};
const dispatch = useDispatch(window.zutron);
const onIncrement = () => dispatch(onIncrementThunk);

// create root reducer
export const rootReducer: Reducer<AppState> = (state, action) => ({
counter: counterReducer(state.counter, action),
ui: uiReducer(state.ui, action)
});
```

#### Accessing the Store in the Main Process
```ts annotate
// `src/main/index.ts`
import { app, BrowserWindow } from 'electron';
import { mainZustandBridge } from 'zutron/main';
import { rootReducer } from '../features/index.js'

In the main process you can access the store object directly, any updates will be propagated to the renderer process.
// create main window
const mainWindow = new BrowserWindow({ ... });

The main process dispatch helper can be used to dispatch actions and thunks, in a similar way to the `useDispatch` hook in the renderer process:
// instantiate bridge
const { unsubscribe } = mainZustandBridge(store, [mainWindow], { reducer: rootReducer });

```ts
import { createDispatch } from 'zutron/main';
// unsubscribe on quit
app.on('quit', unsubscribe);
```

dispatch = createDispatch(store);
#### Instantiate Bridge in Preload

dispatch('COUNTER:INCREMENT');
```
Next, instantiate the bridge in your preload script. If you are using TS, the bridge needs the type of your app state.

By default the main process dispatch helper assumes your store handler functions are located on the store object.
The preload bridge function will return a set of handlers which you need to expose to the renderer process via the Electron `contextBridge` module.

If you keep your store handler functions separate from the store then you will need to pass them in as an option:
```ts annotate
// `src/preload/index.ts`
import { contextBridge } from 'electron';
import { preloadZustandBridge } from 'zutron/preload';
import type { Handlers } from 'zutron';
import type { AppState } from '../features/index.js';

```ts
import { handlers as counterHandlers } from '../../features/counter/index.js';
import { handlers as uiHandlers } from '../../features/ui/index.js';
// instantiate bridge
export const { handlers } = preloadZustandBridge<AppState>();

const actionHandlers = (store: AppStore, initialState: AppState) => ({
...counterHandlers(store),
...uiHandlers(store),
'STORE:RESET': () => store.setState(initialState, true),
});
// expose handlers to renderer process
contextBridge.exposeInMainWorld('zutron', handlers);

dispatch = createDispatch(store, { handlers: actionHandlers(store, initialState) });
// declare handlers type
declare global {
interface Window {
zutron: Handlers<AppState>;
}
}
```

Alternatively if you are using Redux-style reducers you will need to pass the root reducer in as an option:
#### Create hook in Renderer process

```ts
import { reducer as counterReducer } from '../../features/counter/index.js';
import { reducer as uiReducer } from '../../features/ui/index.js';
Finally, in the renderer process you will need to create the `useStore` hook. This requires the `window.zutron` object exposed by the preload bridge:

const rootReducer = (state, action) => {
switch (action.type) {
case types.counter:
return counterReducer(state.counter, action);
case types.ui:
return uiReducer(state.ui, action);
}
};
```ts annotate
// `src/renderer/hooks/useStore.ts`
import { createUseStore } from 'zutron';
import type { AppState } from '../../features/index.js';

dispatch = createDispatch(store, { reducer: rootReducer });
export const useStore = createUseStore<AppState>(window.zutron);
```

0 comments on commit b5df9c1

Please sign in to comment.