Skip to content

Commit

Permalink
Merge branch 'main' into shallow
Browse files Browse the repository at this point in the history
  • Loading branch information
luisherranz committed Jan 15, 2024
2 parents de9584c + 5cf8d8f commit 94bb52e
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 48 deletions.
120 changes: 107 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Use [Preact signals](https://github.com/preactjs/signals) with the interface of
- [Preact & TypeScript](https://stackblitz.com/edit/vitejs-vite-hktyyf?file=src%2Fmain.tsx)
- [React](https://stackblitz.com/edit/vitejs-vite-zoh464?file=src%2Fmain.jsx)
- [React & TypeScript](https://stackblitz.com/edit/vitejs-vite-r2stgq?file=src%2Fmain.tsx)
- [Lit](https://stackblitz.com/edit/lit-and-deepsignal?file=src%2Fmy-element.js)
- Or on Codesandbox
- [Preact](https://codesandbox.io/s/deepsignal-demo-hv1i1p)
- [Preact & TypeScript](https://codesandbox.io/s/deepsignal-demo-typescript-os7ox0?file=/src/index.tsx)
Expand All @@ -38,15 +39,19 @@ Use [Preact signals](https://github.com/preactjs/signals) with the interface of
- [`shallow(obj)`](#shallow)
- [`state.$prop = signal(value)`](#stateprop--signalvalue)
- [`useDeepSignal`](#usedeepsignal)
- [Common Patterns](#common-patterns)
- [Resetting the store](#resetting-the-store)
- [When do you need access to signals?](#when-do-you-need-access-to-signals)
- [Passing the value of a signal directly to JSX](#passing-the-value-of-a-signal-directly-to-jsx)
- [Passing a signal to a child component](#passing-a-signal-to-a-child-component)
- [TypeScript](#typescript)
- [`DeepSignal`](#deepsignal-1)
- [`RevertDeepSignal`](#revertdeepsignal)
- [License](#license)

## Features

- **Transparent**: `deepsignal` wraps the objects with proxies that intercept all property accesses, but does not modify the object. This means that you can still use the object as you normally would, and it will behave exactly as expected. Mutating the object updates the value of the underlying signals.
- **Transparent**: `deepsignal` wraps the object with a proxy that intercepts all property accesses, but does not modify how you interact with the object. This means that you can still use the object as you normally would, and it will behave exactly as you would expect, except that mutating the object also updates the value of the underlying signals.
- **Tiny (less than 1kB)**: `deepsignal` is designed to be lightweight and has a minimal footprint, making it easy to include in your projects. It's just a small wrapper around `@preact/signals-core`.
- **Full array support**: `deepsignal` fully supports arrays, including nested arrays.
- **Deep**: `deepsignal` converts nested objects and arrays to deep signal objects/arrays, allowing you to create fully reactive data structures.
Expand All @@ -60,36 +65,89 @@ The most important feature is that **it just works**. You don't need to do anyth

## Installation

### With Preact

```sh
npm install deepsignal
npm install deepsignal @preact/signals
```

If you are using `deepsignal` with Preact (`@preact/signals`), you should use the `deepsignal` import. You don't need to install or import `@preact/signals` anywhere in your code if you don't need it.
If you are using `deepsignal` with Preact (`@preact/signals`), you should use the `deepsignal` import. You also need to install `@preact/signals`.

```js
import { deepSignal } from "deepsignal";

const state = deepSignal({});
const state = deepSignal({
count: 0,
});

const Count = () => <div>{state.$count}</div>;
```

### With React

If you are using the library with React, you should use the `deepsignal/react` import. You don't need to install or import `@preact/signals-react` anywhere in your code if you don't need it.
```sh
npm install deepsignal @preact/signals-react
```

If you are using the library with React (`@preact/signals-react`), you should use the `deepsignal/react` import. You also need to install `@preact/signals-react`.

```js
import { deepSignal } from "deepsignal/react";

const state = deepSignal({});
const state = deepSignal({
count: 0,
});

const Count = () => <div>{state.$count}</div>;
```

- If you want to use `deepSignal` outside of the components, please follow the [React integration guide of `@preact/signals-react`](https://github.com/preactjs/signals/blob/main/packages/react/README.md#react-integration) to choose one of the integration methods.
- For `useDeepSignal`, no integration is required.

### With Lit

Lit now [supports Preact Signals](https://lit.dev/blog/2023-10-10-lit-3.0/#preact-signals-integration), so you can also use `deepsignal` in Lit.

```sh
npm install deepsignal @lit-labs/preact-signals
```

If you are using the library just with Lit, you should use the `deepsignal/core` import. You also need to install `@lit-labs/preact-signals` and use its `SignalWatcher` function.

```js
import { SignalWatcher } from "@lit-labs/preact-signals";
import { deepSignal } from "deepsignal/core";

const state = deepSignal({
count: 0,
});

class Count extends SignalWatcher(LitElement) {
render() {
return html`<div>${state.$count}</div>`;
}
}
```

### Without Preact/React
### Without Preact/React/Lit

If you are using the library just with `@preact/signals-core`, you should use the `deepsignal/core` import.
```sh
npm install deepsignal @preact/signals-core
```

If you are using the library just with `@preact/signals-core`, you should use the `deepsignal/core` import. You also need to install `@preact/signals-core`.

```js
import { effect } from "@preact/signals-core";
import { deepSignal } from "deepsignal/core";

const state = deepSignal({});
const state = deepSignal({
count: 0,
});

effect(() => {
console.log(`Count: ${state.count}`);
});
```

This is because the `deepsignal` import includes a dependency on `@preact/signals`, while the `deepsignal/core` import does not. This allows you to use deep signals with either `@preact/signals` or `@preact/signals-core`, depending on your needs. **Do not use both.**
Expand Down Expand Up @@ -345,7 +403,7 @@ With `shallow`, you have control over the granularity of reactivity in your stor

### `state.$prop = signal(value)`

You can modify the underlying signal of an object's property doing an assignment to the `$`-prefixed name.
You can modify the underlying signal of an object's property by doing an assignment to the `$`-prefixed name.

```js
const state = deepSignal({ counter: 0 });
Expand Down Expand Up @@ -387,6 +445,28 @@ function Counter() {
}
```

## Common Patterns

### Resetting the store

If you need to reset your store to some initial values, don't overwrite the reference. Instead, replace each value using something like `Object.assign`.

```js
const initialState = { counter: 0 };

const store = deepSignal({
...initialState,
inc: () => {
store.counter++;
},
reset: () => {
Object.assign(store, initialState);
},
});
```

Take into account that the object that you pass to `deepSignal` during the creation is also mutated when you mutate the deep signal. Therefore, if you need to keep a set of initial values, you need to store them in a different object or clone it before assigning it to the deepsignal.

## When do you need access to signals?

You will only need access to the underlying signals for performance optimizations.
Expand Down Expand Up @@ -475,11 +555,25 @@ console.log(array.$![0].value); // 1

Note that here the position of the non-null assertion operator changes because `array.$` is an object in itself.

### DeepSignal and RevertDeepSignal types
DeepSignal exports two types, one to convert from a plain object/array to a `deepSignal` instance, and other to revert from a `deepSignal` instance back to the plain object/array.

### DeepSignal

You can use the `DeepSignal` type if you want to declare your type instead of inferring it.

```ts
import type { DeepSignal } from "deepsignal";

type Store = DeepSignal<{
counter: boolean;
}>;

const store = deepSignal<Store>({ counter: 0 });
```

DeepSignal exports two types, one to convert from a raw state/store to a `deepSignal` instance, and other to revert from a `deepSignal` instance back to the raw store.
### RevertDeepSignal

These types are handy when manual casting is needed, like when you try to use `Object.values()`:
You can use the `RevertDeepSignal` type if you want to recover the type of the plain object/array using the type of the `deepSignal` instance. For example, when you need to use `Object.values()`.

```ts
import type { RevertDeepSignal } from "deepsignal";
Expand Down
18 changes: 16 additions & 2 deletions packages/deepsignal/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
# deepsignal

## 1.4.0-shallow.0
## 1.4.0

### Minor Changes

- [`18a098e`](https://github.com/luisherranz/deepsignal/commit/18a098e6671061ef5830fc89f6dee364f414573d) Thanks [@luisherranz](https://github.com/luisherranz)! - Support storing shallow objects as part of the deepsignal with `shallow`.
- [#62](https://github.com/luisherranz/deepsignal/pull/62) [`beee51e`](https://github.com/luisherranz/deepsignal/commit/beee51e38c56ff94ccb6b3b14583a34f629a006a) Thanks [@luisherranz](https://github.com/luisherranz)! - Add support for setters.

* [#59](https://github.com/luisherranz/deepsignal/pull/59) [`9b0ebbb`](https://github.com/luisherranz/deepsignal/commit/9b0ebbba3707f4170596671e97975c15e1b7650c) Thanks [@luisherranz](https://github.com/luisherranz)! - Add support for @preact/signals-react 2.0.0

## 1.3.6

### Patch Changes

- [#42](https://github.com/luisherranz/deepsignal/pull/42) [`79db35b`](https://github.com/luisherranz/deepsignal/commit/79db35bebe4002c5d4e4ad77156b9ba609e14633) Thanks [@luisherranz](https://github.com/luisherranz)! - Add `preact` as peer dependency back and mark them as optional.

## 1.3.5

### Patch Changes

- [#40](https://github.com/luisherranz/deepsignal/pull/40) [`6284cd6`](https://github.com/luisherranz/deepsignal/commit/6284cd6db785a4ec48a6e2987fd6ea745cc36bdd) Thanks [@luisherranz](https://github.com/luisherranz)! - Use `@preact/signals` dependencies as peer dependencies.

## 1.3.4

Expand Down
5 changes: 4 additions & 1 deletion packages/deepsignal/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const arrayToArrayOfSignals = new WeakMap();
const ignore = new WeakSet();
const objToIterable = new WeakMap();
const rg = /^\$/;
const descriptor = Object.getOwnPropertyDescriptor;
let peeking = false;

export const deepSignal = <T extends object>(obj: T): DeepSignal<T> => {
Expand Down Expand Up @@ -64,7 +65,7 @@ const get =
const key = returnSignal ? fullKey.replace(rg, "") : fullKey;
if (
!signals.has(key) &&
typeof Object.getOwnPropertyDescriptor(target, key)?.get === "function"
typeof descriptor(target, key)?.get === "function"
) {
signals.set(
key,
Expand All @@ -89,6 +90,8 @@ const get =
const objectHandlers = {
get: get(false),
set(target: object, fullKey: string, val: any, receiver: object): boolean {
if (typeof descriptor(target, fullKey)?.set === "function")
return Reflect.set(target, fullKey, val, receiver);
if (!proxyToSignals.has(receiver)) proxyToSignals.set(receiver, new Map());
const signals = proxyToSignals.get(receiver);
if (fullKey[0] === "$") {
Expand Down
74 changes: 74 additions & 0 deletions packages/deepsignal/core/test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,21 @@ describe("deepsignal/core", () => {
expect(store.nested.b).to.equal(3);
});

it("should support setting values with setters", () => {
const store = deepSignal({
counter: 1,
get double() {
return store.counter * 2;
},
set double(val) {
store.counter = val / 2;
},
});
expect(store.counter).to.equal(1);
store.double = 4;
expect(store.counter).to.equal(2);
});

it("should update array length", () => {
expect(store.array.length).to.equal(2);
store.array.push(4);
Expand Down Expand Up @@ -296,6 +311,16 @@ describe("deepsignal/core", () => {
expect(store.a.nested.id).to.equal(4);
expect(store.b.nested.id).to.equal(4);
});

it("should be able to reset values with Object.assign", () => {
const initialNested = { ...nested };
const initialState = { ...state, nested: initialNested };
store.a = 2;
store.nested.b = 3;
Object.assign(store, initialState);
expect(store.a).to.equal(1);
expect(store.nested.b).to.equal(2);
});
});

describe("delete", () => {
Expand Down Expand Up @@ -384,6 +409,31 @@ describe("deepsignal/core", () => {
});

describe("computations", () => {
it("should subscribe to values mutated with setters", () => {
const store = deepSignal({
counter: 1,
get double() {
return store.counter * 2;
},
set double(val) {
store.counter = val / 2;
},
});
let counter = 0;
let double = 0;

effect(() => {
counter = store.counter;
double = store.double;
});

expect(counter).to.equal(1);
expect(double).to.equal(2);
store.double = 4;
expect(counter).to.equal(2);
expect(double).to.equal(4);
});

it("should subscribe to changes when an item is removed from the array", () => {
const store = deepSignal([0, 0, 0]);
let sum = 0;
Expand Down Expand Up @@ -754,6 +804,30 @@ describe("deepsignal/core", () => {
expect(spy1).callCount(4);
expect(spy2).callCount(4);
});

it("should be able to reset values with Object.assign and still react to changes", () => {
const initialNested = { ...nested };
const initialState = { ...state, nested: initialNested };
let a, b;

effect(() => {
a = store.a;
});
effect(() => {
b = store.nested.b;
});

store.a = 2;
store.nested.b = 3;

expect(a).to.equal(2);
expect(b).to.equal(3);

Object.assign(store, initialState);

expect(a).to.equal(1);
expect(b).to.equal(2);
});
});

describe("peek", () => {
Expand Down
27 changes: 21 additions & 6 deletions packages/deepsignal/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "deepsignal",
"version": "1.4.0-shallow.0",
"version": "1.4.0",
"license": "MIT",
"description": "",
"keywords": [],
Expand Down Expand Up @@ -44,17 +44,32 @@
"scripts": {
"prepublishOnly": "cp ../../README.md . && cd ../.. && pnpm build"
},
"dependencies": {
"@preact/signals-core": "^1.3.1",
"peerDependencies": {
"@preact/signals-core": "^1.5.1",
"@preact/signals": "^1.1.4",
"@preact/signals-react": "^1.3.3"
"@preact/signals-react": "^1.3.8 || ^2.0.0",
"preact": "^10.16.0"
},
"peerDependencies": {
"preact": "10.x"
"peerDependenciesMeta": {
"@preact/signals-core": {
"optional": true
},
"@preact/signals": {
"optional": true
},
"@preact/signals-react": {
"optional": true
},
"preact": {
"optional": true
}
},
"devDependencies": {
"preact": "10.9.0",
"preact-render-to-string": "^5.2.4",
"@preact/signals-core": "^1.5.1",
"@preact/signals": "^1.1.4",
"@preact/signals-react": "^2.0.0",
"@types/react": "^18.0.18",
"@types/react-dom": "^18.0.6",
"react": "^18.2.0",
Expand Down
Loading

0 comments on commit 94bb52e

Please sign in to comment.