use-entity 0.0.1-alpha → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,271 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { act, renderHook } from "@testing-library/react";
3
- import { createEntityStore } from "../useEntity.ts";
4
-
5
- type TestEntity = { id: string; name: string };
6
-
7
- describe("useEntity", () => {
8
- test("exposes selectors for the initial state", () => {
9
- const initial: TestEntity[] = [
10
- { id: "1", name: "Alpha" },
11
- { id: "2", name: "Beta" },
12
- ];
13
- const { useEntity } = createEntityStore<TestEntity>(initial);
14
-
15
- const { result } = renderHook(() => useEntity());
16
-
17
- expect(result.current[0].all).toEqual(initial);
18
- expect(result.current[0].ids).toEqual(["1", "2"]);
19
- expect(result.current[0].byId("2")).toEqual({
20
- id: "2",
21
- name: "Beta",
22
- });
23
- });
24
-
25
- test("supports add and remove operations", () => {
26
- const { useEntity } = createEntityStore<TestEntity>();
27
- const { result } = renderHook(() => useEntity());
28
- const [, actions] = result.current;
29
-
30
- act(() => {
31
- actions.addOne({ id: "1", name: "Alpha" });
32
- actions.addMany([
33
- { id: "2", name: "Beta" },
34
- { id: "3", name: "Gamma" },
35
- ]);
36
- });
37
-
38
- expect(result.current[0].all).toHaveLength(3);
39
- expect([...result.current[0].ids].sort()).toEqual(["1", "2", "3"]);
40
-
41
- act(() => {
42
- actions.removeOne("2");
43
- actions.removeMany(["1", "3"]);
44
- });
45
-
46
- expect(result.current[0].all).toEqual([]);
47
-
48
- act(() => {
49
- actions.addOne({ id: "4", name: "Delta" });
50
- actions.removeAll();
51
- });
52
-
53
- expect(result.current[0].all).toEqual([]);
54
- });
55
-
56
- test("supports update and upsert operations", () => {
57
- const { useEntity } = createEntityStore<TestEntity>([
58
- { id: "1", name: "Alpha" },
59
- ]);
60
- const { result } = renderHook(() => useEntity());
61
- const [, actions] = result.current;
62
-
63
- act(() => {
64
- actions.updateOne({ id: "1", changes: { name: "Alpha+" } });
65
- });
66
-
67
- expect(result.current[0].byId("1")).toEqual({
68
- id: "1",
69
- name: "Alpha+",
70
- });
71
-
72
- act(() => {
73
- actions.upsertOne({ id: "2", name: "Beta" });
74
- actions.updateMany([
75
- { id: "1", changes: { name: "Alpha++" } },
76
- { id: "2", changes: { name: "Beta+" } },
77
- ]);
78
- actions.upsertMany({
79
- "2": { id: "2", name: "Beta++" },
80
- "3": { id: "3", name: "Gamma" },
81
- });
82
- });
83
-
84
- expect(result.current[0].byId("1")).toEqual({
85
- id: "1",
86
- name: "Alpha++",
87
- });
88
- expect(result.current[0].byId("2")).toEqual({
89
- id: "2",
90
- name: "Beta++",
91
- });
92
- expect(result.current[0].byId("3")).toEqual({
93
- id: "3",
94
- name: "Gamma",
95
- });
96
- });
97
-
98
- test("supports set operations and total/entities selectors", () => {
99
- const { useEntity } = createEntityStore<TestEntity>();
100
- const { result } = renderHook(() => useEntity());
101
- const [, actions] = result.current;
102
-
103
- act(() => {
104
- actions.setOne({ id: "1", name: "Alpha" });
105
- actions.setMany([
106
- { id: "2", name: "Beta" },
107
- { id: "3", name: "Gamma" },
108
- ]);
109
- });
110
-
111
- expect(result.current[0].total).toBe(3);
112
- expect(result.current[0].entities).toEqual({
113
- "1": { id: "1", name: "Alpha" },
114
- "2": { id: "2", name: "Beta" },
115
- "3": { id: "3", name: "Gamma" },
116
- });
117
-
118
- act(() => {
119
- actions.setAll([{ id: "4", name: "Delta" }]);
120
- });
121
-
122
- expect(result.current[0].all).toEqual([{ id: "4", name: "Delta" }]);
123
- expect(result.current[0].total).toBe(1);
124
- });
125
-
126
- test("returns undefined for missing entities", () => {
127
- const { useEntity } = createEntityStore<TestEntity>();
128
- const { result } = renderHook(() => useEntity());
129
-
130
- expect(result.current[0].byId("missing")).toBeUndefined();
131
- });
132
-
133
- test("ignores remove/update of missing entities", () => {
134
- const { useEntity } = createEntityStore<TestEntity>([
135
- { id: "1", name: "Alpha" },
136
- ]);
137
- const { result } = renderHook(() => useEntity());
138
- const [, actions] = result.current;
139
-
140
- act(() => {
141
- actions.removeOne("missing");
142
- actions.removeMany(["missing-a", "missing-b"]);
143
- actions.updateOne({ id: "missing", changes: { name: "Nope" } });
144
- actions.updateMany([{ id: "missing-2", changes: { name: "Nope 2" } }]);
145
- });
146
-
147
- expect(result.current[0].all).toEqual([{ id: "1", name: "Alpha" }]);
148
- });
149
-
150
- test("keeps selectors in sync after multiple operations", () => {
151
- const { useEntity } = createEntityStore<TestEntity>();
152
- const { result } = renderHook(() => useEntity());
153
- const [, actions] = result.current;
154
-
155
- act(() => {
156
- actions.addMany([
157
- { id: "1", name: "Alpha" },
158
- { id: "2", name: "Beta" },
159
- ]);
160
- actions.removeOne("1");
161
- actions.upsertOne({ id: "2", name: "Beta+" });
162
- actions.upsertMany({
163
- "3": { id: "3", name: "Gamma" },
164
- "4": { id: "4", name: "Delta" },
165
- });
166
- });
167
-
168
- expect([...result.current[0].ids].sort()).toEqual(["2", "3", "4"]);
169
- expect(result.current[0].byId("2")).toEqual({
170
- id: "2",
171
- name: "Beta+",
172
- });
173
- });
174
-
175
- test("selector full returns the full selector object", () => {
176
- const { useEntity } = createEntityStore<TestEntity>([
177
- { id: "1", name: "Alpha" },
178
- { id: "2", name: "Beta" },
179
- ]);
180
- const { result } = renderHook(() => useEntity("full"));
181
-
182
- expect(result.current[0].all).toEqual([
183
- { id: "1", name: "Alpha" },
184
- { id: "2", name: "Beta" },
185
- ]);
186
- expect(result.current[0].byId("2")).toEqual({ id: "2", name: "Beta" });
187
- });
188
-
189
- test("selector all returns array of entities", () => {
190
- const { useEntity } = createEntityStore<TestEntity>([
191
- { id: "1", name: "Alpha" },
192
- ]);
193
- const { result } = renderHook(() => useEntity("all"));
194
- const [, actions] = result.current;
195
-
196
- expect(result.current[0]).toEqual([{ id: "1", name: "Alpha" }]);
197
-
198
- act(() => {
199
- actions.addOne({ id: "2", name: "Beta" });
200
- });
201
-
202
- expect(result.current[0]).toEqual([
203
- { id: "1", name: "Alpha" },
204
- { id: "2", name: "Beta" },
205
- ]);
206
- });
207
-
208
- test("selector ids returns array of entity ids", () => {
209
- const { useEntity } = createEntityStore<TestEntity>([
210
- { id: "1", name: "Alpha" },
211
- { id: "2", name: "Beta" },
212
- ]);
213
- const { result } = renderHook(() => useEntity("ids"));
214
- const [, actions] = result.current;
215
-
216
- expect([...result.current[0]].sort()).toEqual(["1", "2"]);
217
-
218
- act(() => {
219
- actions.addOne({ id: "3", name: "Gamma" });
220
- });
221
-
222
- expect([...result.current[0]].sort()).toEqual(["1", "2", "3"]);
223
- });
224
-
225
- test("selector entities returns entity map", () => {
226
- const { useEntity } = createEntityStore<TestEntity>([
227
- { id: "1", name: "Alpha" },
228
- ]);
229
- const { result } = renderHook(() => useEntity("entities"));
230
- const [, actions] = result.current;
231
-
232
- expect(result.current[0]).toEqual({
233
- "1": { id: "1", name: "Alpha" },
234
- });
235
-
236
- act(() => {
237
- actions.addOne({ id: "2", name: "Beta" });
238
- });
239
-
240
- expect(result.current[0]).toEqual({
241
- "1": { id: "1", name: "Alpha" },
242
- "2": { id: "2", name: "Beta" },
243
- });
244
- });
245
-
246
- test("selector total returns total entity count", () => {
247
- const { useEntity } = createEntityStore<TestEntity>([
248
- { id: "1", name: "Alpha" },
249
- { id: "2", name: "Beta" },
250
- ]);
251
- const { result } = renderHook(() => useEntity("total"));
252
- const [, actions] = result.current;
253
-
254
- expect(result.current[0]).toBe(2);
255
-
256
- act(() => {
257
- actions.removeOne("1");
258
- });
259
-
260
- expect(result.current[0]).toBe(1);
261
- });
262
-
263
- test("defaults to full selector when no selector is provided", () => {
264
- const { useEntity } = createEntityStore<TestEntity>([
265
- { id: "1", name: "Alpha" },
266
- ]);
267
- const { result } = renderHook(() => useEntity());
268
-
269
- expect(result.current[0].all).toEqual([{ id: "1", name: "Alpha" }]);
270
- });
271
- });
package/src/types.ts DELETED
@@ -1,48 +0,0 @@
1
- import type { EntityId, Update } from "@reduxjs/toolkit";
2
- import type { UncheckedIndexedAccess } from "./uncheckedindexed.ts";
3
-
4
- export interface EntityStateAdapter<T, Id extends EntityId> {
5
- addOne(entity: T): void;
6
-
7
- addMany(entities: readonly T[] | Record<Id, T>): void;
8
-
9
- setOne(entity: T): void;
10
-
11
- setMany(entities: readonly T[] | Record<Id, T>): void;
12
-
13
- setAll(entities: readonly T[] | Record<Id, T>): void;
14
-
15
- removeOne(key: Id): void;
16
-
17
- removeMany(keys: readonly Id[]): void;
18
-
19
- removeAll(): void;
20
-
21
- updateOne(update: Update<T, Id>): void;
22
-
23
- updateMany(updates: ReadonlyArray<Update<T, Id>>): void;
24
-
25
- upsertOne(entity: T): void;
26
-
27
- upsertMany(entities: readonly T[] | Record<Id, T>): void;
28
- }
29
-
30
- type Id<T> = {
31
- [K in keyof T]: T[K];
32
- } & {};
33
-
34
- export interface EntitySelectors<T, IdType extends EntityId> {
35
- selectIds: () => IdType[];
36
- selectEntities: () => Record<IdType, T>;
37
- selectAll: () => T[];
38
- selectTotal: () => number;
39
- selectById: (id: IdType) => Id<UncheckedIndexedAccess<T>>;
40
- }
41
-
42
- export interface EntitySelectorsData<T, IdType extends EntityId> {
43
- ids: IdType[];
44
- entities: Record<IdType, T>;
45
- all: T[];
46
- total: number;
47
- byId: (id: IdType) => Id<UncheckedIndexedAccess<T>>;
48
- }
@@ -1,16 +0,0 @@
1
- // inlined from https://github.com/EskiMojo14/uncheckedindexed
2
- // relies on remaining as a TS file, not .d.ts
3
- type IfMaybeUndefined<T, True, False> = [undefined] extends [T] ? True : False;
4
-
5
- const testAccess = ({} as Record<string, 0>).a;
6
-
7
- export type IfUncheckedIndexedAccess<True, False> = IfMaybeUndefined<
8
- typeof testAccess,
9
- True,
10
- False
11
- >;
12
-
13
- export type UncheckedIndexedAccess<T> = IfUncheckedIndexedAccess<
14
- T | undefined,
15
- T
16
- >;
package/src/useEntity.ts DELETED
@@ -1,128 +0,0 @@
1
- import {
2
- createEntityAdapter,
3
- type EntityAdapter,
4
- type EntitySelectors,
5
- type EntityState,
6
- } from "@reduxjs/toolkit";
7
- import { Store, useStore } from "@tanstack/react-store";
8
- import { useMemo } from "react";
9
- import type { EntitySelectorsData, EntityStateAdapter } from "./types.ts";
10
-
11
- const getEntityActionsTanstack = <
12
- T extends {
13
- id: string;
14
- },
15
- >(
16
- store: Store<EntityState<T, T["id"]>>,
17
- adapter: EntityAdapter<T, T["id"]>,
18
- ): EntityStateAdapter<T, T["id"]> => ({
19
- setOne: (entity) => store.setState((prev) => adapter.setOne(prev, entity)),
20
- setMany: (entities) =>
21
- store.setState((prev) => adapter.setMany(prev, entities)),
22
- setAll: (entities) =>
23
- store.setState((prev) => adapter.setAll(prev, entities)),
24
-
25
- addOne: (entity) => store.setState((prev) => adapter.addOne(prev, entity)),
26
- addMany: (entities) =>
27
- store.setState((prev) => adapter.addMany(prev, entities)),
28
-
29
- removeOne: (entityId) =>
30
- store.setState((prev) => adapter.removeOne(prev, entityId)),
31
- removeMany: (entityIds) =>
32
- store.setState((prev) => adapter.removeMany(prev, entityIds)),
33
- removeAll: () => store.setState((prev) => adapter.removeAll(prev)),
34
-
35
- updateOne: (update) =>
36
- store.setState((prev) => adapter.updateOne(prev, update)),
37
- updateMany: (updates) =>
38
- store.setState((prev) => adapter.updateMany(prev, updates)),
39
-
40
- upsertOne: (entity) =>
41
- store.setState((prev) => adapter.upsertOne(prev, entity)),
42
- upsertMany: (entities) =>
43
- store.setState((prev) => adapter.upsertMany(prev, entities)),
44
- });
45
-
46
- const getEntityStateTanstack = <
47
- T extends {
48
- id: string;
49
- },
50
- >(
51
- entityState: EntityState<T, T["id"]>,
52
- selectors: EntitySelectors<T, EntityState<T, T["id"]>, T["id"]>,
53
- ): EntitySelectorsData<T, T["id"]> => ({
54
- all: selectors.selectAll(entityState),
55
- byId: (id) => selectors.selectById(entityState, id),
56
- ids: selectors.selectIds(entityState) as T["id"][],
57
- entities: selectors.selectEntities(entityState),
58
- total: selectors.selectTotal(entityState),
59
- });
60
- const getSelectors = <
61
- T extends {
62
- id: string;
63
- },
64
- >(
65
- baseSelectors: EntitySelectors<T, EntityState<T, T["id"]>, T["id"]>,
66
- ) =>
67
- ({
68
- all: baseSelectors.selectAll,
69
- entities: baseSelectors.selectEntities,
70
- ids: baseSelectors.selectIds,
71
- total: baseSelectors.selectTotal,
72
- full: (state) => getEntityStateTanstack(state, baseSelectors),
73
- }) satisfies Record<
74
- keyof Omit<EntitySelectorsData<T, T["id"]>, "byId"> | "full",
75
- (state: EntityState<T, T["id"]>) => unknown
76
- >;
77
-
78
- export const createEntityStore = <
79
- T extends {
80
- id: string;
81
- },
82
- >(
83
- initialState?: T[],
84
- ) => {
85
- const adapter = createEntityAdapter<T>();
86
- const store = new Store(
87
- initialState
88
- ? adapter.setAll(adapter.getInitialState(), initialState)
89
- : adapter.getInitialState(),
90
- );
91
- const baseSelectors = adapter.getSelectors<EntityState<T, T["id"]>>(
92
- (input) => input,
93
- );
94
- const MySelectors = getSelectors(baseSelectors);
95
-
96
- type SelectorKey = keyof typeof MySelectors;
97
- type SelectorReturn<K extends SelectorKey> = ReturnType<
98
- (typeof MySelectors)[K]
99
- >;
100
-
101
- function useEntity(): [
102
- SelectorReturn<"full">,
103
- EntityStateAdapter<T, T["id"]>,
104
- ];
105
- function useEntity(
106
- selector: "full",
107
- ): [SelectorReturn<"full">, EntityStateAdapter<T, T["id"]>];
108
- function useEntity<K extends Exclude<SelectorKey, "full">>(
109
- selector: K,
110
- ): [SelectorReturn<K>, EntityStateAdapter<T, T["id"]>];
111
-
112
- function useEntity<K extends SelectorKey>(selector: K = "full" as K) {
113
- const entityState = useStore(
114
- store,
115
- MySelectors[selector] as () => SelectorReturn<K>,
116
- );
117
- const actions = useMemo<EntityStateAdapter<T, T["id"]>>(
118
- () => getEntityActionsTanstack(store, adapter),
119
- [],
120
- );
121
- return [entityState, actions] satisfies [
122
- SelectorReturn<K>,
123
- EntityStateAdapter<T, T["id"]>,
124
- ];
125
- }
126
-
127
- return { useEntity, store, adapter };
128
- };
@@ -1,10 +0,0 @@
1
- import { afterEach, expect } from "bun:test";
2
- import * as matchers from "@testing-library/jest-dom/matchers";
3
- import { cleanup } from "@testing-library/react";
4
-
5
- expect.extend(matchers);
6
-
7
- // Optional: cleans up `render` after each test
8
- afterEach(() => {
9
- cleanup();
10
- });
package/tsconfig.json DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- // Environment setup & latest features
4
- "lib": ["ESNext"],
5
- "target": "ESNext",
6
- "module": "Preserve",
7
- "moduleDetection": "force",
8
- "jsx": "react-jsx",
9
- "allowJs": true,
10
-
11
- // Bundler mode
12
- "moduleResolution": "bundler",
13
- "allowImportingTsExtensions": true,
14
- "verbatimModuleSyntax": true,
15
- "noEmit": true,
16
-
17
- // Best practices
18
- "strict": true,
19
- "skipLibCheck": true,
20
- "noFallthroughCasesInSwitch": true,
21
- "noUncheckedIndexedAccess": true,
22
- "noImplicitOverride": true,
23
-
24
- // Some stricter flags (disabled by default)
25
- "noUnusedLocals": false,
26
- "noUnusedParameters": false,
27
- "noPropertyAccessFromIndexSignature": false
28
- }
29
- }