Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement refs #29

Merged
merged 3 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion documentation/pages/docs/advanced-topics/operations.mdx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
# Refs
# Ref

Ref (short for reference) is a special helper wrapper that allows you save value in a mutable fashion.
This allows you to persist data across multiple runs of an effects.
Ref (short for reference) is a mutable object that allows you save value without any reactivity.
This allows you to share data across multiple runs of an effects.

```typescript
### Usage

```ts {3}
import { ref } from 'nalanda';

const getValueRef = ref(initialValue);

effect((store) => {
const valueRef = getValueRef(store);
valueRef.current; // to get the value
valueRef.current = 'newValue'; // to set the value
});
```

### Basic Usage
### Sharing ref between effects

Here is a basic example where ref is used to manage the abort a request across multiple effects.
Here is an example where a shared ref is used to manage aborting of a request across multiple effects.

```ts
import { ref } from 'nalanda';

const key = createKey('mySlice', []);
const getAbortRef = ref(new AbortController());

effect(async (store) => {
key.effect(async (store) => {
const abortRef = getAbortRef(store);

if (!abortRef.current.signal.aborted) {
Expand All @@ -37,7 +39,7 @@ effect(async (store) => {
});

// an effect that can cancel the fetch
effect(async (store) => {
key.effect(async (store) => {
const abortRef = getAbortRef(store);
const { isUserLoggedIn } = useSlice.track(store);

Expand All @@ -48,6 +50,26 @@ effect(async (store) => {
});
```

### Typescript

You pass a type argument to `ref` to specify the type of the value you want to store.

```ts /<string |undefined>/
const getUsernameRef = ref<string |undefined>(undefined)

key.effect(async (store) => {
const usernameRef = getUsernameRef(store);

// fetch username if not set
if (!usernameRef.current) {
const username = await fetchUsername();
usernameRef.current = username;
}
})
```



### When to use them?

Refs share a lot of similarities with state. However, they are not the same. By default its a **good** idea to use slice state whever possible.
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { createStore, createKey } from './vanilla/index';
export { createStore, createKey, ref, cleanup } from './vanilla/index';
2 changes: 1 addition & 1 deletion src/vanilla/__tests__/effect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import waitForExpect from 'wait-for-expect';
import { createKey } from '../slice/key';
import { createStore } from '../store';
import { EffectScheduler, effect } from '../effect/effect';
import { cleanup } from '../cleanup';
import { cleanup } from '../effect/cleanup';

beforeEach(() => {
testCleanup();
Expand Down
153 changes: 153 additions & 0 deletions src/vanilla/__tests__/ref.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Store, createStore } from '../store';
import waitForExpect from 'wait-for-expect';
import { testCleanup } from '../helpers/test-cleanup';
import { createKey } from '../slice/key';
import { ref } from '../effect/ref';
import { EffectStore } from '../effect/effect';

const sliceAKey = createKey('sliceA', []);
const sliceAField1 = sliceAKey.field('value:sliceAField1');
const sliceAField2 = sliceAKey.field('value:sliceAField2');

const sliceA = sliceAKey.slice({
fields: {
sliceAField1,
sliceAField2,
},
});

const sliceBKey = createKey('sliceB', []);
const sliceBField1 = sliceBKey.field('value:sliceBField1');

const sliceB = sliceBKey.slice({
fields: {
sliceBField1,
},
});

beforeEach(() => {
testCleanup();
});

test('ref works', async () => {
const myStore = createStore({
name: 'myStore',
slices: [sliceA, sliceB],
});

const getMyRef = ref<{ foo: { counter?: number } }>(() => ({
foo: {},
}));

let derStore: EffectStore | undefined;

myStore.effect((store) => {
derStore = store;
const val = sliceA.track(store);
const myRef = getMyRef(store);

myRef.current.foo.counter = 1;
});

expect(getMyRef(myStore).current).toEqual({
foo: {},
});

// effect is deferred so we need to wait for it to run
await waitForExpect(() => {
expect(getMyRef(derStore!).current.foo.counter).toBe(1);
});
});

test('creating another store does not reuse the ref value', async () => {
const myStore = createStore({
name: 'myStore',
slices: [sliceA, sliceB],
});

const myStore2 = createStore({
name: 'myStore2',
slices: [sliceA, sliceB],
});

const getMyRef = ref<{ foo: { counter?: number } }>(() => ({
foo: {},
}));

let derStore: EffectStore | undefined;
let derStore2: EffectStore | undefined;

myStore.effect((store) => {
derStore = store;
const val = sliceA.track(store);

const myRef = getMyRef(store);

myRef.current.foo.counter = 1;
});

myStore2.effect((store) => {
derStore2 = store;
const myRef = getMyRef(store);

myRef.current.foo.counter = 99;
});

expect(getMyRef(myStore).current).toEqual({
foo: {},
});

// effect is deferred so we need to wait for it to run
await waitForExpect(() => {
expect(getMyRef(derStore!).current.foo.counter).toBe(1);
});

// effect is deferred so we need to wait for it to run
await waitForExpect(() => {
expect(getMyRef(derStore2!).current.foo.counter).toBe(99);
});
});

test('multiple effects can share the ref value', async () => {
const myStore = createStore({
name: 'myStore',
slices: [sliceA, sliceB],
});

const getMyRef = ref<{ foo: { counter?: number } }>(() => ({
foo: {},
}));

let derStore: EffectStore | undefined;
let derStore2: EffectStore | undefined;

myStore.effect((store) => {
derStore = store;
const val = sliceA.track(store).sliceAField1;

const myRef = getMyRef(store);

myRef.current.foo.counter = 1;
});

sliceBKey.effect((store) => {
derStore2 = store;
const val = sliceB.track(store).sliceBField1;

const myRef = getMyRef(store);

if (myRef.current.foo.counter === 1) {
myRef.current.foo.counter = 2;
}
});

expect(getMyRef(myStore).current).toEqual({
foo: {},
});

await waitForExpect(() => {
expect(getMyRef(myStore).current.foo.counter).toBe(2);
expect(getMyRef(derStore2!).current.foo.counter).toBe(2);
expect(getMyRef(derStore!).current.foo.counter).toBe(2);
});
});
3 changes: 3 additions & 0 deletions src/vanilla/base-store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Operation } from './effect/operation';
import type { Store } from './store';
import type { StoreState } from './store-state';
import type { Transaction } from './transaction';

Expand All @@ -12,5 +13,7 @@ export type Dispatch = (
export abstract class BaseStore {
abstract readonly state: StoreState;

abstract _rootStore: Store;

abstract dispatch(txn: Transaction | Operation): void;
}
6 changes: 3 additions & 3 deletions src/vanilla/cleanup.ts → src/vanilla/effect/cleanup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EffectStore } from './effect/effect';
import { OperationStore } from './effect/operation';
import { throwValidationError } from './helpers/throw-error';
import { EffectStore } from './effect';
import { OperationStore } from './operation';
import { throwValidationError } from '../helpers/throw-error';

export type CleanupCallback = () => void | Promise<void>;

Expand Down
2 changes: 1 addition & 1 deletion src/vanilla/effect/effect-run.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CleanupCallback } from '../cleanup';
import type { CleanupCallback } from './cleanup';
import type { BaseField } from '../slice/field';
import type { Store } from '../store';

Expand Down
5 changes: 1 addition & 4 deletions src/vanilla/effect/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ export type EffectOpts = {

export class EffectStore extends BaseStore {
constructor(
/**
* @internal
*/
private _rootStore: Store,
public _rootStore: Store,
public readonly name: string,
/**
* @internal
Expand Down
4 changes: 3 additions & 1 deletion src/vanilla/effect/operation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BaseStore } from '../base-store';
import type { CleanupCallback } from '../cleanup';
import type { CleanupCallback } from './cleanup';
import { Store } from '../store';
import type { Transaction } from '../transaction';

Expand All @@ -14,12 +14,14 @@ export class OperationStore extends BaseStore {
private cleanupRan = false;
private readonly _cleanupCallbacks: Set<CleanupCallback> = new Set();

_rootStore: Store;
constructor(
private rootStore: Store,
public readonly name: string,
private readonly opts: OperationOpts,
) {
super();
this._rootStore = rootStore;
}

get state() {
Expand Down
27 changes: 27 additions & 0 deletions src/vanilla/effect/ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { BaseStore } from '../base-store';
import { Store } from '../store';

export type RefObject<T> = {
current: T;
};

export function ref<T>(
init: () => T,
): (store: Store | BaseStore) => RefObject<T> {
const cache = new WeakMap<Store, RefObject<T>>();

return (store) => {
const rootStore: Store = store instanceof Store ? store : store._rootStore;

let existing = cache.get(rootStore);

if (!existing) {
existing = {
current: init(),
};
cache.set(rootStore, existing);
}

return existing;
};
}
2 changes: 2 additions & 0 deletions src/vanilla/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { cleanup } from './effect/cleanup';
export { createKey } from './slice/key';
export { createStore } from './store';
export { ref } from './effect/ref';
5 changes: 5 additions & 0 deletions src/vanilla/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export function createStore(config: StoreOptions) {

export class Store extends BaseStore {
private _state: StoreState;

_rootStore: Store;
public readonly initialState: StoreState;

private effectsManager: EffectManager;
Expand All @@ -77,6 +79,7 @@ export class Store extends BaseStore {

constructor(public readonly options: StoreOptions) {
super();

this._state = StoreState.create({
slices: options.slices,
});
Expand All @@ -90,6 +93,8 @@ export class Store extends BaseStore {
debug: this.options.debug,
});

this._rootStore = this;

// do it a bit later so that all effects are registered
queueMicrotask(() => {
this.options.slices.forEach((slice) => {
Expand Down