Skip to content

Commit

Permalink
Add initial support for xstate/store/solid (#5056)
Browse files Browse the repository at this point in the history
* Add initial SolidJS support with `useSelector` function

* Include changeset for xstate/store/solid

* Remove test which doesn't test `useSelector`

* Remove unnecessary types

* Improve typing and remove unused import

* Clean imports
  • Loading branch information
steveadams authored Aug 28, 2024
1 parent 853f6da commit 8c35da9
Show file tree
Hide file tree
Showing 7 changed files with 409 additions and 5 deletions.
24 changes: 24 additions & 0 deletions .changeset/mean-peaches-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@xstate/store': minor
---

You can now use the xstate/store package with SolidJS.

Import `useSelector` from `@xstate/store/solid`. Select the data you want via `useSelector(…)` and send events using `store.send(eventObject)`:

```tsx
import { donutStore } from './donutStore.ts';
import { useSelector } from '@xstate/store/solid';

function DonutCounter() {
const donutCount = useSelector(donutStore, (state) => state.context.donuts);

return (
<div>
<button onClick={() => donutStore.send({ type: 'addDonut' })}>
Add donut ({donutCount()})
</button>
</div>
);
}
```
2 changes: 1 addition & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ module.exports = {
]
},
{
test: /\/xstate-solid\//,
test: /\/xstate-solid\/|solid\.test\.tsx$/,
presets: ['babel-preset-solid']
}
],
Expand Down
21 changes: 21 additions & 0 deletions packages/xstate-store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ function DonutCounter() {
}
```

## Usage with SolidJS

Import `useSelector` from `@xstate/store/solid`. Select the data you want via `useSelector(…)` and send events using `store.send(eventObject)`:

```tsx
import { donutStore } from './donutStore.ts';
import { useSelector } from '@xstate/store/solid';

function DonutCounter() {
const donutCount = useSelector(donutStore, (state) => state.context.donuts);

return (
<div>
<button onClick={() => donutStore.send({ type: 'addDonut' })}>
Add donut ({donutCount()})
</button>
</div>
);
}
```

## Usage with Immer

XState Store makes it really easy to integrate with immutable update libraries like [Immer](https://github.com/immerjs/immer) or [Mutative](https://github.com/unadlib/mutative). Pass the `produce` function into `createStoreWithProducer(producer, …)`, and update `context` in transition functions using the convenient pseudo-mutative API:
Expand Down
24 changes: 20 additions & 4 deletions packages/xstate-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,27 @@
"import": "./react/dist/xstate-store-react.cjs.mjs",
"default": "./react/dist/xstate-store-react.cjs.js"
},
"./solid": {
"types": {
"import": "./solid/dist/xstate-store-solid.cjs.mjs",
"default": "./solid/dist/xstate-store-solid.cjs.js"
},
"module": "./solid/dist/xstate-store-solid.esm.js",
"import": "./solid/dist/xstate-store-solid.cjs.mjs",
"default": "./solid/dist/xstate-store-solid.cjs.js"
},
"./package.json": "./package.json"
},
"sideEffects": false,
"files": [
"dist",
"react"
"react",
"solid"
],
"repository": {
"type": "git",
"url": "git+ssh://[email protected]/statelyai/xstate.git"
},
"scripts": {},
"bugs": {
"url": "https://github.com/statelyai/xstate/issues"
},
Expand All @@ -53,21 +62,28 @@
"immer": "^10.0.2",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"solid-js": "^1.7.6",
"solid-testing-library": "^0.3.0",
"xstate": "^5.17.4"
},
"peerDependencies": {
"react": "^18.2.0"
"react": "^18.2.0",
"solid-js": "^1.7.6"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"solid-js": {
"optional": true
}
},
"preconstruct": {
"umdName": "XStateStore",
"entrypoints": [
"./index.ts",
"./react.ts"
"./react.ts",
"./solid.ts"
]
}
}
4 changes: 4 additions & 0 deletions packages/xstate-store/solid/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "dist/xstate-store-solid.cjs.js",
"module": "dist/xstate-store-solid.esm.js"
}
80 changes: 80 additions & 0 deletions packages/xstate-store/src/solid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* @jsxImportSource solid-js */
import { createEffect, createSignal, onCleanup } from 'solid-js';
import type { Store, SnapshotFromStore } from './types';

function defaultCompare<T>(a: T | undefined, b: T) {
return a === b;
}

function useSelectorWithCompare<TStore extends Store<any, any>, T>(
selector: (snapshot: SnapshotFromStore<TStore>) => T,
compare: (a: T | undefined, b: T) => boolean
): (snapshot: SnapshotFromStore<TStore>) => T {
let previous: T | undefined;

return (state): T => {
const next = selector(state);

if (previous === undefined || !compare(previous, next)) {
previous = next;
}

return previous;
};
}

/**
* Creates a selector which subscribes to the store and selects a value from the
* store's snapshot, using an optional comparison function.
*
* @example
*
* ```tsx
* import { donutStore } from './donutStore.ts';
* import { useSelector } from '@xstate/store/solid';
*
* function DonutCounter() {
* const donutCount = useSelector(donutStore, (state) => state.context.donuts);
*
* return (
* <div>
* <button onClick={() => donutStore.send({ type: 'addDonut' })}>
* Add donut ({donutCount()})
* </button>
* </div>
* );
* }
* ```
*
* @param store The store, created from `createStore(…)`
* @param selector A function which takes in the snapshot and returns a selected
* value from it
* @param compare An optional function which compares the selected value to the
* previously selected value
* @returns A read-only signal of the selected value
*/
export function useSelector<TStore extends Store<any, any>, T>(
store: TStore,
selector: (snapshot: SnapshotFromStore<TStore>) => T,
compare: (a: T | undefined, b: T) => boolean = defaultCompare
): () => T {
const selectorWithCompare = useSelectorWithCompare(selector, compare);
const [selectedValue, setSelectedValue] = createSignal(
selectorWithCompare(store.getSnapshot() as SnapshotFromStore<TStore>)
);

createEffect(() => {
const subscription = store.subscribe(() => {
const newValue = selectorWithCompare(
store.getSnapshot() as SnapshotFromStore<TStore>
);
setSelectedValue(() => newValue);
});

onCleanup(() => {
subscription.unsubscribe();
});
});

return selectedValue;
}
Loading

0 comments on commit 8c35da9

Please sign in to comment.