>;
43 | }
44 |
45 | export type ContextProviderProps = {
46 | value: P;
47 | children: T;
48 | };
49 |
50 | export type FunctionComponent
= (props: P) => T;
51 |
52 | export type Factory
= {
53 | Component: FunctionComponent
;
54 | [DEMOCRAT_COMPONENT]: true;
55 | createElement: P extends void
56 | ? (props?: undefined | Record, key?: Key) => ElementComponent
57 | : (props: P, key?: Key) => ElementComponent;
58 | useChildren: P extends void
59 | ? (props?: undefined | Record, key?: Key) => T
60 | : (props: P, key?: Key) => T;
61 | };
62 |
63 | export type GenericFactory> = {
64 | Component: Fn;
65 | [DEMOCRAT_COMPONENT]: true;
66 | createElement: (runner: (create: Fn) => R, key?: Key) => ElementComponent;
67 | useChildren: (runner: (create: Fn) => R, key?: Key) => R;
68 | };
69 |
70 | export type AnyProps = { [key: string]: any };
71 |
72 | export interface ElementComponent {
73 | [DEMOCRAT_ELEMENT]: true;
74 | type: FunctionComponent;
75 | props: AnyProps;
76 | key: Key;
77 | }
78 |
79 | export interface ElementProvider {
80 | [DEMOCRAT_ELEMENT]: true;
81 | type: DemocratContextProvider;
82 | props: ContextProviderProps;
83 | key: Key;
84 | }
85 |
86 | export type Element = ElementComponent | ElementProvider;
87 |
88 | export interface DemocratRootElement {
89 | [DEMOCRAT_ELEMENT]: true;
90 | [DEMOCRAT_ROOT]: true;
91 | children: Children;
92 | }
93 |
94 | export interface Context {
95 | [DEMOCRAT_CONTEXT]: {
96 | hasDefault: HasDefault;
97 | defaultValue: T;
98 | };
99 | Provider: DemocratContextProvider;
100 | }
101 |
102 | export type Children = Element | null | Array | Map | { [key: string]: Children };
103 |
104 | export type ResolveType =
105 | C extends Element
106 | ? T
107 | : C extends null
108 | ? null
109 | : C extends Array
110 | ? Array>
111 | : C extends Map
112 | ? Map>
113 | : C extends { [key: string]: Children }
114 | ? { [K in keyof C]: ResolveType }
115 | : never;
116 |
117 | export interface StateHookData {
118 | type: 'STATE';
119 | value: any;
120 | setValue: Dispatch>;
121 | }
122 |
123 | export interface ReducerHookData {
124 | type: 'REDUCER';
125 | value: any;
126 | dispatch: Dispatch>;
127 | reducer: Reducer;
128 | }
129 |
130 | export interface ChildrenHookData {
131 | type: 'CHILDREN';
132 | tree: TreeElement;
133 | path: TreeElementPath<'CHILD'>;
134 | }
135 |
136 | export type RefHookData = {
137 | type: 'REF';
138 | ref: MutableRefObject;
139 | };
140 |
141 | export type EffectHookData = {
142 | type: 'EFFECT';
143 | effect: EffectCallback;
144 | cleanup: undefined | EffectCleanup;
145 | deps: DependencyList | undefined;
146 | dirty: boolean;
147 | };
148 |
149 | export type LayoutEffectHookData = {
150 | type: 'LAYOUT_EFFECT';
151 | effect: EffectCallback;
152 | cleanup: undefined | EffectCleanup;
153 | deps: DependencyList | undefined;
154 | dirty: boolean;
155 | };
156 |
157 | export type MemoHookData = {
158 | type: 'MEMO';
159 | value: any;
160 | deps: DependencyList | undefined;
161 | };
162 |
163 | export type ContextHookData = {
164 | type: 'CONTEXT';
165 | context: Context;
166 | provider: TreeElement<'PROVIDER'> | null;
167 | value: any;
168 | };
169 |
170 | export type HooksData =
171 | | StateHookData
172 | | ReducerHookData
173 | | ChildrenHookData
174 | | EffectHookData
175 | | MemoHookData
176 | | LayoutEffectHookData
177 | | RefHookData
178 | | ContextHookData;
179 |
180 | export type OnIdleExec = () => void;
181 | export type OnIdle = (exec: OnIdleExec) => void;
182 |
183 | export type TreeElementState = 'created' | 'stable' | 'updated' | 'removed';
184 |
185 | export type TreeElementCommon = {
186 | id: number;
187 | parent: TreeElement;
188 | path: TreeElementPath;
189 | // when structure change we keep the previous one to cleanup
190 | previous: TreeElement | null;
191 | value: any;
192 | state: TreeElementState;
193 | root: TreeElement<'ROOT'>;
194 | };
195 |
196 | export type TreeElementData = {
197 | ROOT: {
198 | onIdle: OnIdle;
199 | mounted: boolean;
200 | passiveMode: boolean;
201 | children: TreeElement;
202 | context: Map, Set>>;
203 | requestRender: (pathch: Patch | null) => void;
204 | supportReactHooks: (ReactInstance: any, Hooks: any) => void;
205 | isRendering: () => boolean;
206 | applyPatches: (patches: Patches) => void;
207 | findProvider: (context: Context) => TreeElement<'PROVIDER'> | null;
208 | markDirty: (instance: TreeElement<'CHILD'>, limit?: TreeElement | null) => void;
209 | withGlobalRenderingInstance: (current: TreeElement, exec: () => T) => T;
210 | getCurrentRenderingChildInstance: () => TreeElement<'CHILD'>;
211 | getCurrentHook: () => HooksData | null;
212 | getCurrentHookIndex: () => number;
213 | setCurrentHook: (hook: HooksData) => void;
214 | };
215 | NULL: {};
216 | PROVIDER: {
217 | element: ElementProvider;
218 | children: TreeElement;
219 | };
220 | CHILD: {
221 | snapshot: TreeElementSnapshot<'CHILD'> | undefined;
222 | element: ElementComponent;
223 | hooks: Array | null;
224 | nextHooks: Array;
225 | // is set to true when the component or one of it's children has a new state
226 | // and thus need to be rendered even if props are equal
227 | dirty: boolean;
228 | };
229 | OBJECT: { children: { [key: string]: TreeElement } };
230 | ARRAY: { children: Array };
231 | MAP: {
232 | children: Map;
233 | };
234 | };
235 |
236 | export type TreeElementType = keyof TreeElementData;
237 |
238 | type TreeElementResolved = {
239 | [K in keyof TreeElementData]: TreeElementCommon & {
240 | type: K;
241 | } & TreeElementData[K];
242 | };
243 |
244 | export type TreeElement = TreeElementResolved[K];
245 |
246 | type CreateTreeElementMap = T;
247 |
248 | export type TreeElementRaw = CreateTreeElementMap<{
249 | ROOT: DemocratRootElement;
250 | NULL: null;
251 | CHILD: ElementComponent;
252 | PROVIDER: ElementProvider;
253 | ARRAY: Array;
254 | OBJECT: { [key: string]: any };
255 | MAP: Map;
256 | }>;
257 |
258 | type TreeElementPathData = CreateTreeElementMap<{
259 | ROOT: {};
260 | NULL: {};
261 | CHILD: {
262 | hookIndex: number;
263 | };
264 | PROVIDER: {};
265 | ARRAY: { index: number };
266 | OBJECT: { objectKey: string };
267 | MAP: { mapKey: any };
268 | }>;
269 |
270 | type TreeElementPathResolved = {
271 | [K in keyof TreeElementData]: {
272 | type: K;
273 | } & TreeElementPathData[K];
274 | };
275 |
276 | export type TreeElementPath = TreeElementPathResolved[K];
277 |
278 | export type HookSnapshot =
279 | | { type: 'CHILDREN'; child: TreeElementSnapshot }
280 | | { type: 'STATE'; value: any }
281 | | { type: 'REDUCER'; value: any }
282 | | null;
283 |
284 | type TreeElementSnapshotData = CreateTreeElementMap<{
285 | ROOT: {
286 | children: TreeElementSnapshot;
287 | };
288 | NULL: {};
289 | CHILD: {
290 | hooks: Array;
291 | };
292 | PROVIDER: {
293 | children: TreeElementSnapshot;
294 | };
295 | ARRAY: { children: Array };
296 | OBJECT: { children: { [key: string]: TreeElementSnapshot } };
297 | MAP: { children: Map };
298 | }>;
299 |
300 | type TreeElementSnapshotResolved = {
301 | [K in keyof TreeElementData]: {
302 | type: K;
303 | } & TreeElementSnapshotData[K];
304 | };
305 |
306 | export type TreeElementSnapshot = TreeElementSnapshotResolved[K];
307 |
308 | export type Snapshot = TreeElementSnapshot<'ROOT'>;
309 |
310 | export type ReducerWithoutAction = (prevState: S) => S;
311 | export type ReducerStateWithoutAction> =
312 | R extends ReducerWithoutAction ? S : never;
313 | export type DispatchWithoutAction = () => void;
314 | export type Reducer = (prevState: S, action: A) => S;
315 | export type ReducerState> = R extends Reducer ? S : never;
316 | export type ReducerAction> = R extends Reducer ? A : never;
317 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { DEMOCRAT_CONTEXT, DEMOCRAT_ELEMENT, DEMOCRAT_ROOT } from './symbols';
2 | import type {
3 | AnyFn,
4 | Context,
5 | ContextProviderProps,
6 | DemocratContextProvider,
7 | DemocratRootElement,
8 | DependencyList,
9 | Element,
10 | ElementComponent,
11 | ElementProvider,
12 | FunctionComponent,
13 | HooksData,
14 | OnIdle,
15 | Patch,
16 | Patches,
17 | ResolveType,
18 | TreeElement,
19 | TreeElementCommon,
20 | TreeElementData,
21 | TreeElementPath,
22 | TreeElementType,
23 | } from './types';
24 |
25 | export function isValidElement(maybe: unknown): maybe is Element {
26 | return Boolean(maybe && (maybe as any)[DEMOCRAT_ELEMENT] === true);
27 | }
28 |
29 | export function isRootElement(maybe: unknown): maybe is DemocratRootElement {
30 | return Boolean(maybe && (maybe as any)[DEMOCRAT_ELEMENT] === true && (maybe as any)[DEMOCRAT_ROOT] === true);
31 | }
32 |
33 | /**
34 | * Create a Democrat element
35 | * This function is not strictly typed,
36 | * To safely create element use createFactory(component).createElement
37 | */
38 | export function createElement(
39 | component: FunctionComponent
,
40 | props: P,
41 | key?: string | number | undefined,
42 | ): Element;
43 | export function createElement(
44 | context: DemocratContextProvider
,
45 | props: ContextProviderProps
,
46 | key?: string | number | undefined,
47 | ): Element>;
48 | export function createElement(
49 | component: FunctionComponent
| DemocratContextProvider
,
50 | props: P = {} as any,
51 | key?: string | number | undefined,
52 | ): Element {
53 | const element: Element = {
54 | [DEMOCRAT_ELEMENT]: true,
55 | type: component as any,
56 | props: props as any,
57 | key,
58 | };
59 | return element as any;
60 | }
61 |
62 | export function objectShallowEqual(
63 | deps1: { [key: string]: any } | undefined,
64 | deps2: { [key: string]: any } | undefined,
65 | ): boolean {
66 | if (deps1 === deps2) {
67 | return true;
68 | }
69 | if (deps1 === undefined || deps2 === undefined) {
70 | return false;
71 | }
72 | const keys = Object.keys(deps1);
73 | if (!arrayShallowEqual(keys, Object.keys(deps2))) {
74 | return false;
75 | }
76 | for (let i = 0; i < keys.length; i++) {
77 | const key = keys[i];
78 | if (!Object.is(deps1[key], deps2[key])) {
79 | return false;
80 | }
81 | }
82 | return true;
83 | }
84 |
85 | export function sameObjectKeys(obj1: { [key: string]: any }, obj2: { [key: string]: any }): boolean {
86 | return arrayShallowEqual(Object.keys(obj1).sort(), Object.keys(obj2).sort());
87 | }
88 |
89 | export function depsChanged(deps1: DependencyList | undefined, deps2: DependencyList | undefined): boolean {
90 | if (deps1 === undefined || deps2 === undefined) {
91 | return true;
92 | }
93 | if (deps1.length !== deps2.length) {
94 | return true;
95 | }
96 | return !arrayShallowEqual(deps1, deps2);
97 | }
98 |
99 | export function mapObject(
100 | obj: T,
101 | mapper: (v: T[keyof T], key: string) => U,
102 | ): { [K in keyof T]: U } {
103 | return Object.keys(obj).reduce(
104 | (acc, key) => {
105 | (acc as any)[key] = mapper(obj[key], key);
106 | return acc;
107 | },
108 | {} as { [K in keyof T]: U },
109 | );
110 | }
111 |
112 | export function mapMap(source: Map, mapper: (v: V, k: K) => U): Map {
113 | const result = new Map();
114 | source.forEach((v, k) => {
115 | result.set(k, mapper(v, k));
116 | });
117 | return result;
118 | }
119 |
120 | export function createContext(): Context;
121 | export function createContext(defaultValue: T): Context;
122 | export function createContext(defaultValue?: T): Context {
123 | const Provider: DemocratContextProvider = {
124 | [DEMOCRAT_CONTEXT]: 'PROVIDER',
125 | context: null as any,
126 | createElement: (props, key) => createElement(Provider, props as any, key) as any,
127 | };
128 | const context: Context = {
129 | [DEMOCRAT_CONTEXT]: {
130 | hasDefault: defaultValue !== undefined && arguments.length === 1,
131 | defaultValue: defaultValue as any, // force undefined when there a no default value
132 | },
133 | Provider,
134 | };
135 | context.Provider.context = context;
136 | return context;
137 | }
138 |
139 | export function isComponentElement(element: Element): element is ElementComponent {
140 | return typeof element.type === 'function';
141 | }
142 |
143 | export function isProviderElement(element: Element): element is ElementProvider {
144 | return typeof element.type !== 'function' && element.type[DEMOCRAT_CONTEXT] === 'PROVIDER';
145 | }
146 |
147 | const nextId = (() => {
148 | let id = 0;
149 | return () => id++;
150 | })();
151 |
152 | export function createTreeElement(
153 | type: T,
154 | parent: TreeElement,
155 | path: TreeElementPath,
156 | data: Omit & TreeElementData[T],
157 | ): TreeElement {
158 | const id = nextId();
159 | return {
160 | type,
161 | id,
162 | state: 'created',
163 | path,
164 | parent,
165 | root: parent.type === 'ROOT' ? parent : parent.root,
166 | ...data,
167 | } as any;
168 | }
169 |
170 | export function createRootTreeElement(data: {
171 | onIdle: OnIdle;
172 | passiveMode: boolean;
173 | requestRender: (patch: Patch | null) => void;
174 | applyPatches: (patches: Patches) => void;
175 | }): TreeElement<'ROOT'> {
176 | let reactHooksSupported: boolean = false;
177 | const renderingStack: Array = [];
178 |
179 | const rootTreeElement: TreeElement<'ROOT'> = {
180 | type: 'ROOT',
181 | id: nextId(),
182 | mounted: false,
183 | state: 'created',
184 | root: null as any,
185 | path: null as any,
186 | parent: null as any,
187 | value: null,
188 | previous: null,
189 | children: null as any,
190 | context: new Map(),
191 | supportReactHooks,
192 | findProvider,
193 | isRendering,
194 | markDirty,
195 | withGlobalRenderingInstance,
196 | getCurrentRenderingChildInstance,
197 | getCurrentHook,
198 | getCurrentHookIndex,
199 | setCurrentHook,
200 | ...data,
201 | };
202 |
203 | rootTreeElement.root = rootTreeElement;
204 |
205 | return rootTreeElement;
206 |
207 | function markDirty(instance: TreeElement<'CHILD'>, limit: TreeElement | null = null) {
208 | let current: TreeElement | null = instance;
209 | while (current !== null && current !== limit) {
210 | if (current.type === 'CHILD') {
211 | if (current.dirty === true) {
212 | break;
213 | }
214 | current.dirty = true;
215 | }
216 | current = current.parent;
217 | }
218 | }
219 |
220 | function findProvider(context: Context): TreeElement<'PROVIDER'> | null {
221 | for (let i = renderingStack.length - 1; i >= 0; i--) {
222 | const instance = renderingStack[i];
223 | if (instance.type === 'PROVIDER' && instance.element.type.context === context) {
224 | return instance;
225 | }
226 | }
227 | return null;
228 | }
229 |
230 | function isRendering(): boolean {
231 | return renderingStack.length > 0;
232 | }
233 |
234 | function supportReactHooks(ReactInstance: any, hooks: any) {
235 | if (reactHooksSupported === false) {
236 | reactHooksSupported = true;
237 | const methods = ['useState', 'useReducer', 'useEffect', 'useMemo', 'useCallback', 'useLayoutEffect', 'useRef'];
238 | methods.forEach((name) => {
239 | const originalFn = ReactInstance[name] as AnyFn;
240 | ReactInstance[name] = (...args: Array) => {
241 | if (isRendering()) {
242 | return (hooks[name] as AnyFn)(...args);
243 | }
244 | return originalFn(...args);
245 | };
246 | });
247 | }
248 | }
249 |
250 | function withGlobalRenderingInstance(current: TreeElement, exec: () => T): T {
251 | renderingStack.push(current);
252 | const result = exec();
253 | renderingStack.pop();
254 | return result;
255 | }
256 |
257 | function getCurrentRenderingChildInstance(): TreeElement<'CHILD'> {
258 | if (renderingStack.length === 0) {
259 | throw new Error(`Hooks used outside of render !`);
260 | }
261 | const currentInstance = renderingStack[renderingStack.length - 1];
262 | if (currentInstance.type !== 'CHILD') {
263 | throw new Error(`Current rendering instance is not of type CHILD`);
264 | }
265 | return currentInstance;
266 | }
267 |
268 | function getCurrentHook(): HooksData | null {
269 | const instance = getCurrentRenderingChildInstance();
270 | if (instance.hooks && instance.hooks.length > 0) {
271 | return instance.hooks[instance.nextHooks.length] || null;
272 | }
273 | return null;
274 | }
275 |
276 | function getCurrentHookIndex(): number {
277 | const instance = getCurrentRenderingChildInstance();
278 | if (instance.nextHooks) {
279 | return instance.nextHooks.length;
280 | }
281 | return 0;
282 | }
283 |
284 | function setCurrentHook(hook: HooksData) {
285 | const instance = getCurrentRenderingChildInstance();
286 | instance.nextHooks.push(hook);
287 | }
288 | }
289 |
290 | /**
291 | * Array have the same structure if
292 | * - they have the same length
293 | * - the keys have not moved
294 | */
295 | export function sameArrayStructure(prev: Array, children: Array): boolean {
296 | if (prev.length !== children.length) {
297 | return false;
298 | }
299 | const prevKeys = prev.map((item) => (item.type === 'CHILD' ? item.element.key : undefined));
300 | const childrenKeys = children.map((item) => (isValidElement(item) ? item.key : undefined));
301 | return arrayShallowEqual(prevKeys, childrenKeys);
302 | }
303 |
304 | export function sameMapStructure(prev: Map, children: Map): boolean {
305 | if (prev.size !== children.size) {
306 | return false;
307 | }
308 | let allIn = true;
309 | prev.forEach((_v, k) => {
310 | if (allIn === true && children.has(k) === false) {
311 | allIn = false;
312 | }
313 | });
314 | return allIn;
315 | }
316 |
317 | export function registerContextSub(instance: TreeElement<'CHILD'>, context: Context): void {
318 | const root = instance.root;
319 | if (!root.context.has(context)) {
320 | root.context.set(context, new Set());
321 | }
322 | root.context.get(context)!.add(instance);
323 | }
324 |
325 | export function unregisterContextSub(instance: TreeElement<'CHILD'>, context: Context): void {
326 | const root = instance.root;
327 | if (!root.context.has(context)) {
328 | return;
329 | }
330 | const ctx = root.context.get(context)!;
331 | ctx.delete(instance);
332 | if (ctx.size === 0) {
333 | root.context.delete(context);
334 | }
335 | }
336 |
337 | export function markContextSubDirty(instance: TreeElement, context: Context): void {
338 | const root = instance.root;
339 | const ctx = root.context.get(context);
340 | if (ctx) {
341 | ctx.forEach((c) => {
342 | if (isDescendantOf(c, instance)) {
343 | root.markDirty(c, instance);
344 | }
345 | });
346 | }
347 | }
348 |
349 | export function isElementInstance(instance: TreeElement): instance is TreeElement<'CHILD' | 'PROVIDER'> {
350 | if (instance.type === 'CHILD' || instance.type === 'PROVIDER') {
351 | return true;
352 | }
353 | return false;
354 | }
355 |
356 | export function getInstanceKey(instance: TreeElement): string | number | undefined {
357 | return isElementInstance(instance) ? instance.element.key : undefined;
358 | }
359 |
360 | export function getPatchPath(instance: TreeElement): Array {
361 | const path: Array = [];
362 | let current: TreeElement | null = instance;
363 | while (current !== null && current.type !== 'ROOT') {
364 | path.unshift(current.path);
365 | if (current.parent === undefined) {
366 | console.log(current);
367 | }
368 | current = current.parent;
369 | }
370 | return path;
371 | }
372 |
373 | // eslint-disable-next-line @typescript-eslint/ban-types
374 | export function isPlainObject(o: unknown): o is object {
375 | if (isObjectObject(o) === false) return false;
376 |
377 | // If has modified constructor
378 | const ctor = (o as any).constructor;
379 | if (typeof ctor !== 'function') return false;
380 |
381 | // If has modified prototype
382 | const prot = ctor.prototype;
383 | if (isObjectObject(prot) === false) return false;
384 |
385 | // If constructor does not have an Object-specific method
386 | if (Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOf') === false) {
387 | return false;
388 | }
389 |
390 | // Most likely a plain Object
391 | return true;
392 | }
393 |
394 | function isObject(val: any) {
395 | return val != null && typeof val === 'object' && Array.isArray(val) === false;
396 | }
397 |
398 | function isObjectObject(o: any) {
399 | return isObject(o) === true && Object.prototype.toString.call(o) === '[object Object]';
400 | }
401 |
402 | function arrayShallowEqual(deps1: ReadonlyArray, deps2: ReadonlyArray): boolean {
403 | if (deps1 === deps2) {
404 | return true;
405 | }
406 | if (deps1.length !== deps2.length) {
407 | return false;
408 | }
409 | for (let i = 0; i < deps1.length; i++) {
410 | const dep = deps1[i];
411 | if (!Object.is(dep, deps2[i])) {
412 | return false;
413 | }
414 | }
415 | return true;
416 | }
417 |
418 | function isDescendantOf(instance: TreeElement, parent: TreeElement) {
419 | let current: TreeElement | null = instance;
420 | while (current !== null && current !== parent) {
421 | current = current.parent;
422 | }
423 | return current === parent;
424 | }
425 |
426 | export type Timer = ReturnType;
427 |
--------------------------------------------------------------------------------
/tests/index.test.tsx:
--------------------------------------------------------------------------------
1 | import { expect, test, vi } from 'vitest';
2 | import * as Democrat from '../src/mod';
3 | import { mapMap, removeFunctionsDeep, waitForNextState, waitForNextTick } from './utils';
4 |
5 | test('basic count state', async () => {
6 | const onRender = vi.fn();
7 | const Counter = Democrat.createFactory(() => {
8 | onRender();
9 | const [count, setCount] = Democrat.useState(0);
10 | return {
11 | count,
12 | setCount,
13 | };
14 | });
15 | const store = Democrat.createStore(Counter.createElement());
16 | expect(store.getState().count).toEqual(0);
17 | await waitForNextTick();
18 | store.getState().setCount(42);
19 | await waitForNextState(store);
20 | expect(store.getState().count).toEqual(42);
21 | expect(onRender).toHaveBeenCalledTimes(2);
22 | });
23 |
24 | test('Destroy just after create', async () => {
25 | const effect = vi.fn();
26 | const Counter = Democrat.createFactory(() => {
27 | Democrat.useEffect(effect, []);
28 | return null;
29 | });
30 | const store = Democrat.createStore(Counter.createElement());
31 | store.destroy();
32 | expect(effect).not.toHaveBeenCalled();
33 | await waitForNextTick();
34 | expect(effect).not.toHaveBeenCalled();
35 | });
36 |
37 | test('generic component', () => {
38 | const Counter = Democrat.createGenericFactory(function (props: { val: R }): R {
39 | return props.val;
40 | });
41 | const store = Democrat.createStore(Counter.createElement((c) => c({ val: 42 })));
42 | expect(store.getState()).toEqual(42);
43 | });
44 |
45 | test('subscribe', async () => {
46 | const onRender = vi.fn();
47 | const Counter = Democrat.createFactory(() => {
48 | onRender();
49 | const [count, setCount] = Democrat.useState(0);
50 |
51 | return {
52 | count,
53 | setCount,
54 | };
55 | });
56 | const store = Democrat.createStore(Counter.createElement());
57 | const onState = vi.fn();
58 | store.subscribe(onState);
59 | store.getState().setCount(42);
60 | await waitForNextState(store);
61 | store.getState().setCount(0);
62 | await waitForNextState(store);
63 | expect(onState).toHaveBeenCalledTimes(2);
64 | });
65 |
66 | test('useReducer', async () => {
67 | type State = { count: number };
68 | type Action = { type: 'increment' } | { type: 'decrement' };
69 |
70 | const initialState: State = { count: 0 };
71 |
72 | function reducer(state: State, action: Action): State {
73 | switch (action.type) {
74 | case 'increment':
75 | return { count: state.count + 1 };
76 | case 'decrement':
77 | return { count: state.count - 1 };
78 | default:
79 | return state;
80 | }
81 | }
82 | const onRender = vi.fn();
83 | const Counter = Democrat.createFactory(() => {
84 | onRender();
85 | const [count, dispatch] = Democrat.useReducer(reducer, initialState);
86 |
87 | return {
88 | count,
89 | dispatch,
90 | };
91 | });
92 | const store = Democrat.createStore(Counter.createElement());
93 | const onState = vi.fn();
94 | store.subscribe(onState);
95 | store.getState().dispatch({ type: 'increment' });
96 | await waitForNextState(store);
97 | expect(store.getState().count).toEqual({ count: 1 });
98 | store.getState().dispatch({ type: 'decrement' });
99 | await waitForNextState(store);
100 | expect(store.getState().count).toEqual({ count: 0 });
101 | const prevState = store.getState().count;
102 | expect(onState).toHaveBeenCalledTimes(2);
103 | store.getState().dispatch({} as any);
104 | expect(store.getState().count).toBe(prevState);
105 | });
106 |
107 | test('subscribe wit useMemo', async () => {
108 | const onRender = vi.fn();
109 | const Counter = () => {
110 | onRender();
111 | const [count, setCount] = Democrat.useState(0);
112 |
113 | const result = Democrat.useMemo(
114 | () => ({
115 | count,
116 | setCount,
117 | }),
118 | [count, setCount],
119 | );
120 |
121 | return result;
122 | };
123 | const store = Democrat.createStore(Democrat.createElement(Counter, {}));
124 | const onState = vi.fn();
125 | store.subscribe(onState);
126 | store.getState().setCount(42);
127 | await waitForNextState(store);
128 | store.getState().setCount(0);
129 | await waitForNextState(store);
130 | expect(onState).toHaveBeenCalledTimes(2);
131 | });
132 |
133 | test('set two states', async () => {
134 | expect.assertions(3);
135 | const render = vi.fn();
136 | const Counter = () => {
137 | render();
138 | const [countA, setCountA] = Democrat.useState(0);
139 | const [countB, setCountB] = Democrat.useState(0);
140 | const setCount = Democrat.useCallback((v: number) => {
141 | setCountA(v);
142 | setCountB(v);
143 | }, []);
144 |
145 | return {
146 | count: countA + countB,
147 | setCount,
148 | };
149 | };
150 | const store = Democrat.createStore(Democrat.createElement(Counter, {}));
151 | expect(store.getState().count).toEqual(0);
152 | store.getState().setCount(1);
153 | await waitForNextState(store);
154 | expect(render).toHaveBeenCalledTimes(2);
155 | expect(store.getState().count).toEqual(2);
156 | });
157 |
158 | test('effects runs', async () => {
159 | const onLayoutEffect = vi.fn();
160 | const onEffect = vi.fn();
161 |
162 | const Counter = () => {
163 | const [count, setCount] = Democrat.useState(0);
164 |
165 | Democrat.useLayoutEffect(() => {
166 | onLayoutEffect();
167 | }, [count]);
168 |
169 | Democrat.useEffect(() => {
170 | onEffect();
171 | }, [count]);
172 |
173 | return {
174 | count,
175 | setCount,
176 | };
177 | };
178 | Democrat.createStore(Democrat.createElement(Counter, {}));
179 | await waitForNextTick();
180 | expect(onLayoutEffect).toHaveBeenCalled();
181 | expect(onEffect).toHaveBeenCalled();
182 | });
183 |
184 | test('effects cleanup runs', async () => {
185 | const onLayoutEffect = vi.fn();
186 | const onLayoutEffectCleanup = vi.fn();
187 | const onEffect = vi.fn();
188 | const onEffectCleanup = vi.fn();
189 |
190 | const Counter = () => {
191 | const [count, setCount] = Democrat.useState(0);
192 |
193 | Democrat.useLayoutEffect(() => {
194 | onLayoutEffect();
195 | return onLayoutEffectCleanup;
196 | }, [count]);
197 |
198 | Democrat.useEffect(() => {
199 | onEffect();
200 | return onEffectCleanup;
201 | }, [count]);
202 |
203 | return {
204 | count,
205 | setCount,
206 | };
207 | };
208 | const store = Democrat.createStore(Democrat.createElement(Counter, {}));
209 | await waitForNextTick();
210 | expect(onLayoutEffect).toBeCalledTimes(1);
211 | expect(onEffect).toBeCalledTimes(1);
212 | store.getState().setCount(42);
213 | await waitForNextState(store);
214 | await waitForNextTick();
215 | expect(onLayoutEffect).toBeCalledTimes(2);
216 | expect(onEffect).toBeCalledTimes(2);
217 | expect(onLayoutEffectCleanup).toHaveBeenCalled();
218 | expect(onEffectCleanup).toHaveBeenCalled();
219 | });
220 |
221 | test('runs cleanup only once', async () => {
222 | const onLayoutEffect = vi.fn();
223 | const onLayoutEffectCleanup = vi.fn();
224 |
225 | const Child = () => {
226 | Democrat.useLayoutEffect(() => {
227 | onLayoutEffect();
228 | return onLayoutEffectCleanup;
229 | }, []);
230 | };
231 |
232 | const Counter = () => {
233 | const [count, setCount] = Democrat.useState(0);
234 |
235 | const child = Democrat.useChildren(count < 10 ? Democrat.createElement(Child, {}) : null);
236 |
237 | return {
238 | child,
239 | count,
240 | setCount,
241 | };
242 | };
243 | const store = Democrat.createStore(Democrat.createElement(Counter, {}));
244 | await waitForNextTick();
245 | expect(onLayoutEffectCleanup).not.toHaveBeenCalled();
246 | expect(onLayoutEffect).toBeCalledTimes(1);
247 | // should unmount
248 | store.getState().setCount(12);
249 | await waitForNextState(store);
250 | expect(onLayoutEffectCleanup).toBeCalledTimes(1);
251 | expect(onLayoutEffect).toBeCalledTimes(1);
252 | // stay unmounted
253 | store.getState().setCount(13);
254 | await waitForNextState(store);
255 | expect(onLayoutEffectCleanup).toBeCalledTimes(1);
256 | expect(onLayoutEffect).toBeCalledTimes(1);
257 | });
258 |
259 | test('use effect when re-render', async () => {
260 | const onUseEffect = vi.fn();
261 | const Counter = () => {
262 | const [count, setCount] = Democrat.useState(0);
263 |
264 | Democrat.useEffect(() => {
265 | onUseEffect();
266 | if (count === 0) {
267 | setCount(42);
268 | }
269 | }, [count]);
270 |
271 | return {
272 | count,
273 | setCount,
274 | };
275 | };
276 |
277 | const store = Democrat.createStore(Democrat.createElement(Counter, {}));
278 | await waitForNextState(store);
279 | expect(onUseEffect).toHaveBeenCalledTimes(1);
280 | expect(store.getState().count).toBe(42);
281 | await waitForNextTick();
282 | expect(onUseEffect).toHaveBeenCalledTimes(2);
283 | });
284 |
285 | test('multiple counters (array children)', async () => {
286 | const Counter = () => {
287 | const [count, setCount] = Democrat.useState(0);
288 | return {
289 | count,
290 | setCount,
291 | };
292 | };
293 | const Counters = () => {
294 | const [numberOfCounter, setNumberOfCounter] = Democrat.useState(3);
295 |
296 | const counters = Democrat.useChildren(
297 | new Array(numberOfCounter).fill(null).map(() => Democrat.createElement(Counter, {})),
298 | );
299 |
300 | const addCounter = Democrat.useCallback(() => {
301 | setNumberOfCounter((v) => v + 1);
302 | }, []);
303 |
304 | return {
305 | counters,
306 | addCounter,
307 | };
308 | };
309 |
310 | const store = Democrat.createStore(Democrat.createElement(Counters, {}));
311 | expect(store.getState()).toMatchInlineSnapshot(`
312 | {
313 | "addCounter": [Function],
314 | "counters": [
315 | {
316 | "count": 0,
317 | "setCount": [Function],
318 | },
319 | {
320 | "count": 0,
321 | "setCount": [Function],
322 | },
323 | {
324 | "count": 0,
325 | "setCount": [Function],
326 | },
327 | ],
328 | }
329 | `);
330 | expect(store.getState().counters.length).toBe(3);
331 | expect(store.getState().counters[0].count).toBe(0);
332 | store.getState().counters[0].setCount(1);
333 | await waitForNextState(store);
334 | expect(store.getState().counters[0].count).toBe(1);
335 | store.getState().addCounter();
336 | await waitForNextState(store);
337 | expect(store.getState().counters.length).toEqual(4);
338 | expect(store.getState().counters[3].count).toBe(0);
339 | });
340 |
341 | test('multiple counters (object children)', () => {
342 | const Counter = ({ initialCount = 0 }: { initialCount?: number }) => {
343 | const [count, setCount] = Democrat.useState(initialCount);
344 |
345 | return {
346 | count,
347 | setCount,
348 | };
349 | };
350 | const Counters = () => {
351 | const counters = Democrat.useChildren({
352 | counterA: Democrat.createElement(Counter, { initialCount: 2 }),
353 | counterB: Democrat.createElement(Counter, {}),
354 | });
355 |
356 | const sum = counters.counterA.count + counters.counterB.count;
357 |
358 | return { counters, sum };
359 | };
360 |
361 | const store = Democrat.createStore(Democrat.createElement(Counters, {}));
362 | expect(store.getState()).toMatchInlineSnapshot(`
363 | {
364 | "counters": {
365 | "counterA": {
366 | "count": 2,
367 | "setCount": [Function],
368 | },
369 | "counterB": {
370 | "count": 0,
371 | "setCount": [Function],
372 | },
373 | },
374 | "sum": 2,
375 | }
376 | `);
377 | });
378 |
379 | test('multiple counters (object children update)', async () => {
380 | const Counter = ({ initialCount = 0 }: { initialCount?: number }) => {
381 | const [count, setCount] = Democrat.useState(initialCount);
382 |
383 | return {
384 | count,
385 | setCount,
386 | };
387 | };
388 | const Counters = () => {
389 | const [showCounterC, setShowCounterC] = Democrat.useState(false);
390 |
391 | const counters = Democrat.useChildren({
392 | counterA: Democrat.createElement(Counter, { initialCount: 2 }),
393 | counterB: Democrat.createElement(Counter, {}),
394 | counterC: showCounterC ? Democrat.createElement(Counter, {}) : null,
395 | });
396 |
397 | const sum = counters.counterA.count + counters.counterB.count;
398 |
399 | const toggle = Democrat.useCallback(() => setShowCounterC((prev) => !prev), []);
400 |
401 | return { counters, sum, toggle };
402 | };
403 |
404 | const store = Democrat.createStore(Democrat.createElement(Counters, {}));
405 | expect(store.getState()).toMatchInlineSnapshot(`
406 | {
407 | "counters": {
408 | "counterA": {
409 | "count": 2,
410 | "setCount": [Function],
411 | },
412 | "counterB": {
413 | "count": 0,
414 | "setCount": [Function],
415 | },
416 | "counterC": null,
417 | },
418 | "sum": 2,
419 | "toggle": [Function],
420 | }
421 | `);
422 | store.getState().toggle();
423 | await waitForNextState(store);
424 | expect(store.getState()).toMatchInlineSnapshot(`
425 | {
426 | "counters": {
427 | "counterA": {
428 | "count": 2,
429 | "setCount": [Function],
430 | },
431 | "counterB": {
432 | "count": 0,
433 | "setCount": [Function],
434 | },
435 | "counterC": {
436 | "count": 0,
437 | "setCount": [Function],
438 | },
439 | },
440 | "sum": 2,
441 | "toggle": [Function],
442 | }
443 | `);
444 | });
445 |
446 | test('render a context', async () => {
447 | const NumCtx = Democrat.createContext(10);
448 |
449 | const Store = () => {
450 | const num = Democrat.useContext(NumCtx);
451 | const [count, setCount] = Democrat.useState(0);
452 |
453 | return {
454 | count: count + num,
455 | setCount,
456 | };
457 | };
458 | const store = Democrat.createStore(
459 | Democrat.createElement(NumCtx.Provider, {
460 | value: 42,
461 | children: Democrat.createElement(Store, {}),
462 | }),
463 | );
464 | expect(store.getState().count).toEqual(42);
465 | store.getState().setCount(1);
466 | await waitForNextState(store);
467 | expect(store.getState().count).toEqual(43);
468 | });
469 |
470 | test('render a context and update it', async () => {
471 | const NumCtx = Democrat.createContext(10);
472 |
473 | const Child = () => {
474 | const num = Democrat.useContext(NumCtx);
475 | const [count, setCount] = Democrat.useState(0);
476 |
477 | return {
478 | count: count + num,
479 | setCount,
480 | };
481 | };
482 |
483 | const Parent = () => {
484 | const [num, setNum] = Democrat.useState(0);
485 |
486 | const { count, setCount } = Democrat.useChildren(
487 | Democrat.createElement(NumCtx.Provider, {
488 | value: num,
489 | children: Democrat.createElement(Child, {}),
490 | }),
491 | );
492 |
493 | return {
494 | count,
495 | setCount,
496 | setNum,
497 | };
498 | };
499 |
500 | const store = Democrat.createStore(Democrat.createElement(Parent, {}));
501 | expect(store.getState().count).toEqual(0);
502 | store.getState().setCount(1);
503 | await waitForNextState(store);
504 | expect(store.getState().count).toEqual(1);
505 | store.getState().setNum(1);
506 | await waitForNextState(store);
507 | expect(store.getState().count).toEqual(2);
508 | });
509 |
510 | test('read a context with no provider', () => {
511 | const NumCtx = Democrat.createContext(10);
512 |
513 | const Store = () => {
514 | const num = Democrat.useContext(NumCtx);
515 | const [count, setCount] = Democrat.useState(0);
516 |
517 | return {
518 | count: count + num,
519 | setCount,
520 | };
521 | };
522 | const store = Democrat.createStore(Democrat.createElement(Store, {}));
523 | expect(store.getState().count).toEqual(10);
524 | });
525 |
526 | test('conditionnaly use a children', async () => {
527 | const Child = () => {
528 | return 42;
529 | };
530 |
531 | const Store = () => {
532 | const [show, setShow] = Democrat.useState(false);
533 |
534 | const child = Democrat.useChildren(show ? Democrat.createElement(Child, {}) : null);
535 |
536 | return Democrat.useMemo(
537 | () => ({
538 | setShow,
539 | child,
540 | }),
541 | [setShow, child],
542 | );
543 | };
544 | const store = Democrat.createStore(Democrat.createElement(Store, {}));
545 | expect(store.getState().child).toEqual(null);
546 | store.getState().setShow(true);
547 | await waitForNextState(store);
548 | expect(store.getState().child).toEqual(42);
549 | });
550 |
551 | test('render a children', async () => {
552 | const Child = () => {
553 | const [count, setCount] = Democrat.useState(0);
554 | return Democrat.useMemo(
555 | () => ({
556 | count,
557 | setCount,
558 | }),
559 | [count, setCount],
560 | );
561 | };
562 |
563 | const Store = () => {
564 | const [count, setCount] = Democrat.useState(0);
565 | const child = Democrat.useChildren(Democrat.createElement(Child, {}));
566 | return Democrat.useMemo(
567 | () => ({
568 | count,
569 | setCount,
570 | child,
571 | }),
572 | [count, setCount, child],
573 | );
574 | };
575 | const store = Democrat.createStore(Democrat.createElement(Store, {}));
576 | expect(store.getState().child.count).toEqual(0);
577 | store.getState().child.setCount(42);
578 | await waitForNextState(store);
579 | expect(store.getState().child.count).toEqual(42);
580 | });
581 |
582 | test('subscribe when children change', async () => {
583 | const Child = () => {
584 | const [count, setCount] = Democrat.useState(0);
585 | return Democrat.useMemo(
586 | () => ({
587 | count,
588 | setCount,
589 | }),
590 | [count, setCount],
591 | );
592 | };
593 |
594 | const Store = () => {
595 | const [count, setCount] = Democrat.useState(0);
596 | const child = Democrat.useChildren(Democrat.createElement(Child, {}));
597 | return Democrat.useMemo(
598 | () => ({
599 | count,
600 | setCount,
601 | child,
602 | }),
603 | [count, setCount, child],
604 | );
605 | };
606 | const store = Democrat.createStore(Democrat.createElement(Store, {}));
607 | const onState = vi.fn();
608 | store.subscribe(onState);
609 | store.getState().child.setCount(42);
610 | await waitForNextState(store);
611 | store.getState().child.setCount(1);
612 | await waitForNextState(store);
613 | expect(store.getState().child.count).toEqual(1);
614 | expect(onState).toHaveBeenCalledTimes(2);
615 | });
616 |
617 | test('useLayoutEffect', async () => {
618 | const Store = () => {
619 | const [count, setCount] = Democrat.useState(0);
620 |
621 | Democrat.useLayoutEffect(() => {
622 | if (count !== 0) {
623 | setCount(0);
624 | }
625 | }, [count]);
626 |
627 | return Democrat.useMemo(
628 | () => ({
629 | count,
630 | setCount,
631 | }),
632 | [count, setCount],
633 | );
634 | };
635 | const store = Democrat.createStore(Democrat.createElement(Store, {}));
636 | const onState = vi.fn();
637 | store.subscribe(onState);
638 | store.getState().setCount(42);
639 | await waitForNextState(store);
640 | expect(store.getState().count).toEqual(0);
641 | expect(onState).toHaveBeenCalledTimes(1);
642 | });
643 |
644 | test('useEffect in loop', async () => {
645 | const Store = () => {
646 | const [count, setCount] = Democrat.useState(0);
647 |
648 | Democrat.useEffect(() => {
649 | if (count !== 0) {
650 | setCount(count - 1);
651 | }
652 | }, [count]);
653 |
654 | return Democrat.useMemo(
655 | () => ({
656 | count,
657 | setCount,
658 | }),
659 | [count, setCount],
660 | );
661 | };
662 | const store = Democrat.createStore(Democrat.createElement(Store, {}));
663 | const onState = vi.fn();
664 | store.subscribe(() => {
665 | onState(store.getState().count);
666 | });
667 | store.getState().setCount(3);
668 | await waitForNextState(store);
669 | await waitForNextState(store);
670 | await waitForNextState(store);
671 | await waitForNextState(store);
672 | expect(store.getState().count).toEqual(0);
673 | expect(onState).toHaveBeenCalledTimes(4);
674 | expect(onState.mock.calls).toEqual([[3], [2], [1], [0]]);
675 | });
676 |
677 | test('useLayoutEffect in loop', async () => {
678 | const Store = () => {
679 | const [count, setCount] = Democrat.useState(0);
680 |
681 | Democrat.useLayoutEffect(() => {
682 | if (count !== 0) {
683 | setCount(count - 1);
684 | }
685 | }, [count]);
686 |
687 | return Democrat.useMemo(
688 | () => ({
689 | count,
690 | setCount,
691 | }),
692 | [count, setCount],
693 | );
694 | };
695 | const store = Democrat.createStore(Democrat.createElement(Store, {}));
696 | const onState = vi.fn();
697 | store.subscribe(onState);
698 | store.getState().setCount(3);
699 | await waitForNextState(store);
700 | expect(store.getState().count).toEqual(0);
701 | expect(onState).toHaveBeenCalledTimes(1);
702 | });
703 |
704 | test('useLayoutEffect & useEffect in loop (should run useEffect sync)', async () => {
705 | const Store = () => {
706 | const [count, setCount] = Democrat.useState(0);
707 |
708 | Democrat.useLayoutEffect(() => {
709 | if (count !== 0) {
710 | setCount(count - 1);
711 | }
712 | }, [count]);
713 |
714 | Democrat.useEffect(() => {
715 | if (count !== 0) {
716 | setCount(count - 1);
717 | }
718 | }, [count]);
719 |
720 | return Democrat.useMemo(
721 | () => ({
722 | count,
723 | setCount,
724 | }),
725 | [count, setCount],
726 | );
727 | };
728 | const store = Democrat.createStore(Democrat.createElement(Store, {}));
729 | const onState = vi.fn();
730 | store.subscribe(onState);
731 | store.getState().setCount(3);
732 | await waitForNextState(store);
733 | expect(store.getState().count).toEqual(0);
734 | expect(onState).toHaveBeenCalledTimes(1);
735 | });
736 |
737 | test('array of children', async () => {
738 | const Child = ({ val }: { val: number }) => {
739 | return val * 2;
740 | };
741 |
742 | const Store = () => {
743 | const [items, setItems] = Democrat.useState([23, 5, 7]);
744 |
745 | const addItem = Democrat.useCallback((item: number) => {
746 | setItems((prev) => [...prev, item]);
747 | }, []);
748 |
749 | const child = Democrat.useChildren(items.map((v) => Democrat.createElement(Child, { val: v })));
750 |
751 | return Democrat.useMemo(
752 | () => ({
753 | addItem,
754 | child,
755 | }),
756 | [addItem, child],
757 | );
758 | };
759 | const store = Democrat.createStore(Democrat.createElement(Store, {}));
760 | expect(store.getState().child).toEqual([46, 10, 14]);
761 | store.getState().addItem(6);
762 | await waitForNextState(store);
763 | expect(store.getState().child).toEqual([46, 10, 14, 12]);
764 | });
765 |
766 | test('array of children with keys', async () => {
767 | const Child = ({ val }: { val: number }) => {
768 | return val * 2;
769 | };
770 |
771 | const Store = () => {
772 | const [items, setItems] = Democrat.useState([23, 5, 7]);
773 |
774 | const addItem = Democrat.useCallback((item: number) => {
775 | setItems((prev) => [item, ...prev]);
776 | }, []);
777 |
778 | const child = Democrat.useChildren(items.map((v) => Democrat.createElement(Child, { val: v }, v)));
779 |
780 | return Democrat.useMemo(
781 | () => ({
782 | addItem,
783 | child,
784 | }),
785 | [addItem, child],
786 | );
787 | };
788 | const store = Democrat.createStore(Democrat.createElement(Store, {}));
789 | expect(store.getState().child).toEqual([46, 10, 14]);
790 | store.getState().addItem(6);
791 | await waitForNextState(store);
792 | expect(store.getState().child).toEqual([12, 46, 10, 14]);
793 | });
794 |
795 | test('remove key of array child', async () => {
796 | const onRender = vi.fn();
797 |
798 | const Child = () => {
799 | return Math.random();
800 | };
801 |
802 | const Store = () => {
803 | onRender();
804 | const [withKey, setWithKey] = Democrat.useState(true);
805 |
806 | const child = Democrat.useChildren([
807 | withKey ? Democrat.createElement(Child, {}, 42) : Democrat.createElement(Child, {}),
808 | ]);
809 |
810 | return Democrat.useMemo(
811 | () => ({
812 | setWithKey,
813 | child,
814 | }),
815 | [setWithKey, child],
816 | );
817 | };
818 | const store = Democrat.createStore(Democrat.createElement(Store, {}));
819 | const out1 = store.getState().child[0];
820 | await waitForNextTick();
821 | store.getState().setWithKey(false);
822 | await waitForNextState(store);
823 | expect(onRender).toHaveBeenCalledTimes(2);
824 | const out2 = store.getState().child[0];
825 | expect(out2).not.toEqual(out1);
826 | });
827 |
828 | test('render an array as root', () => {
829 | const Child = () => {
830 | return 42;
831 | };
832 | expect(() =>
833 | Democrat.createStore([Democrat.createElement(Child, {}), Democrat.createElement(Child, {})]),
834 | ).not.toThrow();
835 | const store = Democrat.createStore([Democrat.createElement(Child, {}), Democrat.createElement(Child, {})]);
836 | expect(store.getState()).toEqual([42, 42]);
837 | });
838 |
839 | test('throw when render invalid element', () => {
840 | expect(() => Democrat.createStore(new Date() as any)).toThrow('Invalid children type');
841 | });
842 |
843 | test('throw when render a Set', () => {
844 | expect(() => Democrat.createStore(new Set() as any)).toThrow('Set are not supported');
845 | });
846 |
847 | test('render a Map', () => {
848 | expect(() => Democrat.createStore(new Map())).not.toThrow();
849 | });
850 |
851 | test('update a Map', async () => {
852 | const Child = () => {
853 | const [count, setCount] = Democrat.useState(0);
854 |
855 | return Democrat.useMemo(
856 | () => ({
857 | count,
858 | setCount,
859 | }),
860 | [count, setCount],
861 | );
862 | };
863 |
864 | const Store = () => {
865 | const [ids, setIds] = Democrat.useState