Installations
npm install @dpsignal/react
Developer Guide
Typescript
Yes
Module System
ESM
Node Version
20.11.1
NPM Version
10.2.4
Score
60.2
Supply Chain
93.5
Quality
77.6
Maintenance
100
Vulnerability
100
License
Releases
Unable to fetch releases
Contributors
Unable to fetch Contributors
Languages
TypeScript (100%)
Developer
divyam234
Download Statistics
Total Downloads
218
Last Day
3
Last Week
6
Last Month
17
Last Year
218
GitHub Statistics
38 Commits
1 Branches
1 Contributors
Bundle Size
2.10 kB
Minified
886.00 B
Minified + Gzipped
Package Meta Information
Latest Version
5.0.1
Package Id
@dpsignal/react@5.0.1
Unpacked Size
12.90 kB
Size
4.70 kB
File Count
7
NPM Version
10.2.4
Node Version
20.11.1
Publised On
09 Oct 2024
Total Downloads
Cumulative downloads
Total Downloads
218
Last day
0%
3
Compared to previous day
Last week
50%
6
Compared to previous week
Last month
0%
17
Compared to previous month
Last year
0%
218
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
Dependencies
1
Peer Dependencies
1
📶 @dpsignal/react
This library is meant to expand on Preact's new Signal
primitive to make it a viable state management solution at the scale of a full
state management system by wrapping the built in primitive with a new DeepSignal
model. This package is intended for use with React
applications only.
Installation
1# npm 2npm i @dpsignal/react @preact/signals-react 3# yarn 4yarn add @dpsignal/react @preact/signals-react 5# pnpm 6pnpm add @dpsignal/react @preact/signals-react
This package also requires @preact/signals-react
as peer dependencies, as well as react
itself (although the commands above assume you have react
already installed).
Why use @dpsignal/react
?
In a current React application you would be easily tempted to use a state management solution from the React ecosystem which relies on the VDOM. If you use a library like this then then your application wide state updates could be causing full VDOM rerenders. Signals provide us an escape from that on the scale of micro-state, but this library aims to scale that solution for larger applications.
How it works
Very simply, the library just takes an arbitrary object of any scale or shape, and recursively turns all properties into signals. The objects
themselves each turn into a DeepSignal
which contains a value
getter & setter as well as a peek
method just like regular Signal
instances. However, if you assign a new value to a DeepSignal
, the setter method will recursively find every true Signal
inside the object
and assign them to a new value. So if you subscribe to a Signal
in a component, you can guarantee that it will be updated no matter what level
of the store gets reassigned.
So a simple example like this
1import { deepSignal } from "@dpsignal/react"; 2 3const userStore = deepSignal({ 4 name: { 5 first: "Thor", 6 last: "Odinson" 7 }, 8 email: "thor@avengers.org" 9});
...is equivalent to this code...
1import { signal, batch } from "@preact/signals-react"; 2 3const userStore = { 4 name: { 5 first: signal("Thor"), 6 last: signal("Odinson"), 7 get value(): { first: string, last: string } { 8 return { 9 first: this.first.value, 10 last: this.last.value 11 } 12 }, 13 set value(payload: { first: string, last: string }) { 14 batch(() => { 15 this.first.value = payload.first; 16 this.last.value = payload.last; 17 }); 18 }, 19 peek(): { first: string, last: string } { 20 return { 21 first: this.first.peek(), 22 last: this.last.peek() 23 } 24 }, 25 }, 26 email: signal("thor@avengers.org"), 27 get value(): { name: { first: string, last: string }, email: string } { 28 return { 29 name: { 30 first: this.name.first.value, 31 last: this.name.last.value 32 }, 33 email: this.email.value 34 } 35 }, 36 set value(payload: { name: { first: string, last: string }, email: string }) { 37 batch(() => { 38 this.name.first.value = payload.name.first; 39 this.name.last.value = payload.name.last; 40 this.email.value = payload.email; 41 }); 42 }, 43 peek(): { name: { first: string, last: string }, email: string } { 44 return { 45 name: { 46 first: this.name.first.peek(), 47 last: this.name.last.peek() 48 }, 49 email: this.email.peek() 50 } 51 }, 52};
Side note for "How it works"
This example is now somewhat antiquated. Starting with version 4.0.0, part of the DeepSignal model tracks it's own structural shape. This way you can store modifiable records of nested DeepSignals & Signals. The example above is still the most simple representation of the most common use case for DeepSignals. However, it is not a perfect representation of all of it's capabilities.
Using DeepSignal
in a local context
By utilizing useDeepSignal
you can get a local state DX that's very similar to class components while continuing to have the performance
advantages of signals.
1import { useDeepSignal } from "@dpsignal/react"; 2 3const UserRegistrationForm = () => { 4 const user = useDeepSignal(() => ({ 5 name: { 6 first: "", 7 last: "" 8 }, 9 email: "" 10 })); 11 12 const submitRegistration = (event) => { 13 event.preventDefault(); 14 fetch( 15 "/register", 16 { method: "POST", body: JSON.stringify(user.peek()) } 17 ); 18 } 19 20 return ( 21 <form onSubmit={submitRegistration}> 22 <label> 23 First name 24 <input value={user.name.first} 25 onInput={e => user.name.first.value = e.currentTarget.value} /> 26 </label> 27 <label> 28 Last name 29 <input value={user.name.last} 30 onInput={e => user.name.last.value = e.currentTarget.value} /> 31 </label> 32 <label> 33 Email 34 <input value={user.email} 35 onInput={e => user.email.value = e.currentTarget.value} /> 36 </label> 37 <button>Submit</button> 38 </form> 39 ); 40}
TypeScript support
The API for deepStore
and useDeepStore
will handle dynamic typing for arbitrary input! It will also help you avoid a case like this
1import { deepSignal } from "@dpsignal/react"; 2 3const userStore = deepSignal({ 4 name: { 5 first: "Thor", 6 last: "Odinson" 7 }, 8 email: "thor@avengers.org" 9}); 10 11// TS error: Cannot assign to 'email' because it is a read-only property. 12userStore.value.email = "another@email.com"
Recipes
Zustand style method actions
When I look to Zustand for the API it provides, it seems like a lot of their API (as much as I admire it) is based around supporting the
functional context. But the output of @dpsignal/react
is very openly dynamic and writing to it inside or outside of a component
ends up being the same. So you can take Zustand's basic example...
1import create from "zustand"; 2 3export const useBearStore = create((set) => ({ 4 bears: 0, 5 increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), 6 removeAllBears: () => set({ bears: 0 }), 7}))
...and create an effectively equivalent version with @dpsignal/react
like this...
1import { deepSignal } from "@dpsignal/react"; 2 3export const bearStore = { 4 data: deepSignal({ 5 bears: 0 6 }), 7 increasePopulation() { 8 this.data.bears.value++; 9 }, 10 removeAllBears() { 11 this.data.bears.value = 0 12 } 13};
Storing and fetching from localStorage
Because the value
getter on a DeepSignal
effectively calls the value
getter on each underlying Signal
, calling the DeepSignal
's
getter will properly subscribe to each underlying signal. So if you wanted to manage the side effects of any level of a DeepSignal
you
just need to call effect
from @preact/signals-react
and call DeepSignal.value
1import { deepSignal } from "@dpsignal/react"; 2import { effect } from "@preact/signals"; 3 4type UserStore = { 5 name: { 6 first: string; 7 last: string; 8 }; 9 email: string; 10} 11 12const getInitialUserStore = (): UserStore => { 13 const storedUserStore = localStorage.getItem("USER_STORE_KEY"); 14 if (storedUserStore) { 15 // you should probably validate this 🤷♂️ 16 return JSON.parse(storedUserStore); 17 } else { 18 return { 19 name: { 20 first: "", 21 last: "" 22 }, 23 email: "" 24 }; 25 } 26} 27 28const userStore = deepSignal(getInitialUserStore()); 29 30effect(() => localStorage.setItem("USER_STORE_KEY", JSON.stringify(userStore.value)));
This would also work for any level of the DeepSignal
.
1import { deepSignal } from "@dpsignal/react"; 2import { effect } from "@preact/signals-react"; 3 4type UserNameStore = { 5 first: string; 6 last: string; 7}; 8 9const getInitialUserNameStore = (): UserNameStore => { 10 const storedUserStore = localStorage.getItem("USER_NAME_STORE_KEY"); 11 12 // you should probably validate this too 🤷♂️ 13 return storedUserStore ? JSON.parse(storedUserStore) : { first: "", last: "" }, 14} 15 16const userStore = deepSignal({ 17 name: getInitialUserNameStore(), 18 email: "" 19}); 20 21effect(() => localStorage.setItem("USER_NAME_STORE_KEY", JSON.stringify(userStore.name.value)));
This should fulfill most needs for middleware or plugins. If this fails to meet your needs, please file an issue and I will address the particular ask.
Modifiable Structure, new in 4.0.0
New in 4.0.0, you can now have DeepSignals that represent modifiable data structures. If you wanted to create a DeepSignal as a record, it could be modified using the following example.
1import { DeepSignalType, useDeepSignal } from "@dpsignal/react"; 2import { memo } from "react"; 3 4export const TodoList = () => { 5 const store = useDeepSignal(() => ({ 6 newItemName: "", 7 items: {} as Record<string, { name: string }>, 8 })); 9 10 return ( 11 <> 12 <form 13 onSubmit={(e) => { 14 e.preventDefault(); 15 store.items.value = { 16 ...store.items.peek(), 17 [Math.random().toString()]: { name: store.newItemName.peek() }, 18 }; 19 store.newItemName.value = ""; 20 }} 21 > 22 <input 23 value={store.newItemName.value} 24 onChange={(e) => (store.newItemName.value = e.currentTarget.value)} 25 /> 26 <button>Add new item</button> 27 </form> 28 <ul> 29 {Object.keys(store.items.value).map((id) => ( 30 <TodoItem key={id} id={id} store={store} /> 31 ))} 32 </ul> 33 </> 34 ); 35}; 36 37const TodoItem = memo( 38 ({ 39 id, 40 store, 41 }: { 42 id: string; 43 store: DeepSignalType<{ items: Record<string, { name: string }> }>; 44 }) => { 45 return ( 46 <li> 47 <>{store.items[id].name}</> 48 <button 49 onClick={() => { 50 const items = { ...store.items.peek() }; 51 delete items[id]; 52 store.items.value = items; 53 }} 54 aria-label="Delete item" 55 > 56 ❌ 57 </button> 58 </li> 59 ); 60 } 61);
Possible footguns
It may appear that if you iterate over Object.keys(store.items)
here, you can avoid unnecessary rerenders. However, you need to rely on rerenders to get updates to iterative mappings so you must map over Object.keys(store.items.value)
and accept the VDOM rerenders. And thus, there isn't necessarily a huge gain in performance by storing data this way unless you only pass the key & store down to the iterated component and then memoize it to avoid unecessary rerenders.
No vulnerabilities found.
No security vulnerabilities found.