Skip to content

Commit

Permalink
feat: hook api method ready to use
Browse files Browse the repository at this point in the history
  • Loading branch information
betula committed Dec 23, 2023
1 parent 76d8664 commit 2c0324a
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 31 deletions.
31 changes: 21 additions & 10 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,23 +383,34 @@ Each isolated instance will be destroyed at the end of the isolated asynchronous
### Describe component logic in OOP-style

```typescript
import { hook, un } from "ya-signals";

class RecipeForm {
constructor(
//(params proposal)
//private signalOfParam1,
//private signalOfParam2
) {
import { hook, un, type SignalReadonly } from "ya-signals";

export class RecipeForm {
constructor(signalParams: SignalReadonly<[number, string]>) {
un(() => {
// destroy
})

signalParams.sync((params) => {
console.log('Current params values', params);
});
}
}

useRecipeForm = hook(RecipeForm)
export const useRecipeForm = hook(RecipeForm)
```

const form = useRecipeForm(/*(params proposal) param1, param2*/)
Somewhere inside React component function

```typescript
import { useRecipeForm } from './recipe-form.ts';

function Form() {
const form = useRecipeForm([10, 'hello']); // params available here

return <>
// ...
}
```

## License
Expand Down
33 changes: 22 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ npm install ya-signals
### On demand services

```typescript
import { service, un } from "ya-signals";
import { service } from "ya-signals";

class UserServer {
constructor() {}
Expand Down Expand Up @@ -52,23 +52,34 @@ service.destroy(userService);
### Describe component logic in OOP-style

```typescript
import { hook, un } from "ya-signals";

class RecipeForm {
constructor(
//(params proposal)
//private signalOfParam1,
//private signalOfParam2
) {
import { hook, un, type SignalReadonly } from "ya-signals";

export class RecipeForm {
constructor(signalParams: SignalReadonly<[number, string]>) {
un(() => {
// destroy
})

signalParams.sync((params) => {
console.log('Current params values', params);
});
}
}

useRecipeForm = hook(RecipeForm)
export const useRecipeForm = hook(RecipeForm)
```

Somewhere inside React component function

const form = useRecipeForm(/*(params proposal) param1, param2*/)
```typescript
import { useRecipeForm } from './recipe-form.ts';

function Form() {
const form = useRecipeForm([10, 'hello']); // params available here

return <>
// ...
}
```

## API Reference
Expand Down
11 changes: 6 additions & 5 deletions src/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ import React from 'react';
import { collect, unsubscriber, run } from 'unsubscriber';
import { signal, wrap, SignalReadonly } from './signal';

export const hook = <T>(Class: ((params?: SignalReadonly<any>) => T) | (new (params?: SignalReadonly<any>) => T)): ((params?: any) => T) => {

return (params: any) => {
export function hook<T>(Class: (() => T) | (new () => T)): (() => T);
export function hook<T, M>(Class: ((params: SignalReadonly<M>) => T) | (new (params: SignalReadonly<M>) => T)): ((params: M) => T);
export function hook<T, M>(Class: ((params?: SignalReadonly<M>) => T) | (new (params?: SignalReadonly<M>) => T)): ((params?: M) => T) {
return (params: M) => {
const [instance, unsubs, signalParams] = React.useMemo(() => {
const signalParams = signal(params);
const wrapped = wrap(signalParams.get);
const unsubs = unsubscriber();
return [
collect(unsubs, () => (
Class.prototype === void 0
? (Class as (params?: SignalReadonly<any>) => T)(wrapped)
: new (Class as new (params?: SignalReadonly<any>) => T)(wrapped)
? (Class as (params?: SignalReadonly<M>) => T)(wrapped)
: new (Class as new (params?: SignalReadonly<M>) => T)(wrapped)
)),
unsubs,
signalParams
Expand Down
4 changes: 2 additions & 2 deletions src/reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ export const autorun = (expression: () => void) => (
un(autorunOrigin(expression))
);

export const reaction = <T>(expression: () => T, listener: (value: T, previous: T) => void) => (
export const reaction = <T>(expression: () => T, listener: (value: T, prev: T) => void) => (
un(reactionOrigin(expression, listener))
);

export const sync = <T>(expression: () => T, listener: (value: T) => void) => (
export const sync = <T>(expression: () => T, listener: (value: T, prev: T | undefined) => void) => (
un(reactionOrigin(expression, listener, { fireImmediately: true }))
);

Expand Down
88 changes: 85 additions & 3 deletions test/hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React from 'react';
import { scope } from 'unsubscriber';
import { autorun, hook, signal, un } from "../src";
import { SignalReadonly, autorun, hook, signal, un } from "../src";

let useMemoCache;
let unmount;
let unsubs;

beforeAll(() => {
jest.spyOn(React, 'useMemo')
.mockImplementation((fn, deps) => {
expect(deps).toStrictEqual([]);
return fn();

if (useMemoCache) return useMemoCache;
return useMemoCache = fn();
});

jest.spyOn(React, 'useEffect')
Expand All @@ -19,10 +22,15 @@ beforeAll(() => {
});
});

afterEach(() => {
useMemoCache = void 0;
});

afterAll(() => {
jest.restoreAllMocks()
})


it('hook works', () => {
const create_spy = jest.fn();
const destroy_spy = jest.fn();
Expand All @@ -47,10 +55,84 @@ it('hook works', () => {
expect(inst.b).toBe(10);
inst.a(10);
expect(inst.b).toBe(20);

expect(create_spy).toBeCalled();
expect(destroy_spy).not.toBeCalled();

unmount();
expect(destroy_spy).toBeCalled();
});

it('hook array params works', () => {
const create_spy = jest.fn();
const destroy_spy = jest.fn();
const params_spy = jest.fn();

class A {
constructor(params: SignalReadonly<[number, string]>) {
unsubs = scope();
create_spy();
un(destroy_spy);

params.sync((state) => {
params_spy(state);
});
}
}

const useA = hook(A);
const inst = useA([10, 'a']);

expect(params_spy).toBeCalledWith([10, 'a']); params_spy.mockClear();
expect(inst).toBe(useA([10, 'a']));
expect(params_spy).not.toBeCalled();

expect(inst).toBe(useA([10, 'b']));
expect(params_spy).toBeCalledWith([10, 'b']); params_spy.mockClear();

expect(inst).toBe(useA([10, 'b']));
expect(params_spy).not.toBeCalled();

expect(create_spy).toBeCalled();
expect(destroy_spy).not.toBeCalled();

unmount();
expect(destroy_spy).toBeCalled();
});

it('hook struct params works', () => {
const create_spy = jest.fn();
const destroy_spy = jest.fn();
const params_spy = jest.fn();

class A {
constructor(params: SignalReadonly<{ a: number; b: string }>) {
unsubs = scope();
create_spy();
un(destroy_spy);

params.sync((state) => {
params_spy(state);
});
}
}

const useA = hook(A);
const inst = useA({a: 10, b: 'a'});

expect(params_spy).toBeCalledWith({a: 10, b: 'a'}); params_spy.mockClear();
expect(inst).toBe(useA({a: 10, b: 'a'}));
expect(params_spy).not.toBeCalled();

expect(inst).toBe(useA({a: 10, b: 'b'}));
expect(params_spy).toBeCalledWith({a: 10, b: 'b'}); params_spy.mockClear();

expect(inst).toBe(useA({a: 10, b: 'b'}));
expect(params_spy).not.toBeCalled();

expect(create_spy).toBeCalled();
expect(destroy_spy).not.toBeCalled();

unmount();
expect(destroy_spy).toBeCalled();
});

0 comments on commit 2c0324a

Please sign in to comment.