Skip to content

Commit

Permalink
feat(primitives): add withAssign, deprecate withReducers
Browse files Browse the repository at this point in the history
* feat(primitives): add withAssign, deprecate withReducers

* refactor(primitives): withReducers -> withAssign

* chore(primitives): cleanup

* refactor(primitives): type -> interface

* refactor(primitives): ditch withReducers

* refactor(primitives): codestyle

---------

Co-authored-by: krulod <chaurka.noyan@gmail.com>
  • Loading branch information
artalar and krulod authored Apr 11, 2024
1 parent 2f5875e commit 3ac66fc
Show file tree
Hide file tree
Showing 15 changed files with 365 additions and 244 deletions.
21 changes: 15 additions & 6 deletions packages/async/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,18 +585,27 @@ export const fetchList = reatomAsync(
withRetry({
onReject: (ctx, error, retries) => 100 * Math.min(200, retries ** 3),
}),
)
export const isFetchListLoading = atom(
(ctx) =>
ctx.spy(fetchList.pendingAtom) > 0 || ctx.spy(fetchList.retriesAtom) > 0,
'isFetchListLoading',
withAssign((target, name) => ({
loadingAtom: atom(
(ctx) =>
ctx.spy(target.pendingAtom) > 0 ||
ctx.spy(target.retriesAtom) > 0,
`${name}.loadingAtom`,
),
})),
)
```

Note that `retriesAtom` will drop to `0` when any promise resolves successfully or when you return `undefined` or a negative number. So, it is good practice to avoid calling multiple async actions in parallel. If you are using `withRetry`, it is recommended to always use it with [withAbort](#withabort) (with the default 'last-in-win' strategy).

```ts
import { atom, reatomAsync, withAbort, withErrorAtom, withRetry } from '@reatom/async'
import {
atom,
reatomAsync,
withAbort,
withErrorAtom,
withRetry,
} from '@reatom/async'

export const fetchList = reatomAsync(
(ctx) => request('api/list', ctx.controller),
Expand Down
47 changes: 44 additions & 3 deletions packages/primitives/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,49 @@ const inputAtom = reatomString()
inputAtom.reset(ctx)
```
## `withAssign`
An operator that makes it easier to attach properties such as computed atoms, reducer actions etc.
```ts
import {
atom,
withAssign,
action,
reatomResource,
withRetry,
} from '@reatom/framework'

const pageAtom = atom(1).pipe(
withAssign((pageAtom, name) => ({
prev: action(
(ctx) => pageAtom(ctx, (prev) => Math.max(1, prev - 1)),
`${name}.prev`,
),
next: action((ctx) => pageAtom(ctx, (prev) => prev + 1), `${name}.next`),
})),
)

const list = reatomResource(async (ctx) => {
const page = ctx.spy(pageAtom)
return await ctx.schedule(() => request(`/api/list/${page}`))
}, 'fetchList').pipe(
withRetry({
onReject: (ctx, error, retries) => 100 * Math.min(200, retries ** 3),
}),
withAssign((list, name) => ({
loadingAtom: atom(
(ctx) => ctx.spy(list.pendingAtom) > 0 || ctx.spy(list.retriesAtom) > 0,
`${name}.loadingAtom`,
),
})),
)
```
## `withReducers`
Deprecated, use [`withAssign`](#withassign) instead.
```ts
import { atom } from '@reatom/core'
import { withReducers } from '@reatom/primitives'
Expand All @@ -140,7 +181,7 @@ const pageAtom = atom(1).pipe(
}),
)

// built-in actions:
pageAtom.next(ctx)
pageAtom.prev(ctx)
// `prev` and `next` actions are added automatically
pageAtom.next(ctx) // => 2
pageAtom.prev(ctx) // => 1
```
2 changes: 2 additions & 0 deletions packages/primitives/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
import './reatomArray.test'
import './reatomRecord.test'
import './reatomEnum.test'
import './reatomString.test'
79 changes: 41 additions & 38 deletions packages/primitives/src/reatomArray.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,46 @@
import { atom, AtomMut } from '@reatom/core'
import { withReducers, WithReducers } from './withReducers'
import { Action, AtomMut, action, atom } from '@reatom/core'
import { withAssign } from './withAssign'

export type ArrayAtom<T> = WithReducers<
AtomMut<Array<T>>,
{
toReversed(state: Array<T>): Array<T>
toSorted(state: Array<T>, compareFn?: (a: T, b: T) => number): Array<T>
toSpliced(
state: Array<T>,
start: number,
deleteCount: number,
...items: T[]
): Array<T>
with(state: Array<T>, index: number, value: T): Array<T>
}
>
export interface ArrayAtom<T> extends AtomMut<Array<T>> {
toReversed: Action<[], T[]>
toSorted: Action<[compareFn?: (a: T, b: T) => number], T[]>
toSpliced: Action<[start: number, deleteCount: number, ...items: T[]], T[]>
with: Action<[i: number, value: T], T[]>
}

export const reatomArray = <T>(
initState = new Array<T>(),
initState = [] as T[],
name?: string,
): ArrayAtom<T> => {
return atom(initState, name).pipe(
withReducers({
toReversed: (state) => state.slice(0).reverse(),
toSorted: (state, compareFn) => state.slice(0).sort(compareFn),
toSpliced: (state, start, deleteCount, ...items) => {
state = state.slice(0)
state.splice(start, deleteCount, ...items)

return state
},
with: (state, index, value) => {
if (Object.is(state.at(index), value)) return state

state = state.slice(0)
state[index] = value

return state
},
}),
): ArrayAtom<T> =>
atom(initState, name).pipe(
withAssign((target, name) => ({
toReversed: action(
(ctx) => target(ctx, (prev) => prev.slice().reverse()),
`${name}.toReversed`,
),
toSorted: action(
(ctx, compareFn?: (a: T, b: T) => number) =>
target(ctx, (prev) => prev.slice().sort(compareFn)),
`${name}.toSorted`,
),
toSpliced: action(
(ctx, start: number, deleteCount: number, ...items: T[]) =>
target(ctx, (state) => {
state = state.slice()
state.splice(start, deleteCount, ...items)
return state
}),
`${name}.toSpliced`,
),
with: action(
(ctx, i: number, value: T) =>
target(ctx, (state) => {
if (Object.is(state.at(i), value)) return state
state = state.slice()
state[i] = value
return state
}),
`${name}.with`,
),
})),
)
}
35 changes: 18 additions & 17 deletions packages/primitives/src/reatomBoolean.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { atom, AtomMut } from '@reatom/core'
import { withReducers, WithReducers } from './withReducers'
import { Action, AtomMut, action, atom } from '@reatom/core'
import { withAssign } from './withAssign'

export type BooleanReducers = {
toggle: () => boolean
setTrue: () => boolean
setFalse: () => boolean
reset: () => boolean
export interface BooleanAtom extends AtomMut<boolean> {
toggle: Action<[], boolean>
setTrue: Action<[], true>
setFalse: Action<[], false>
reset: Action<[], boolean>
}

export type BooleanAtom = WithReducers<AtomMut<boolean>, BooleanReducers>

export const reatomBoolean = (initState = false, name?: string): BooleanAtom =>
atom(initState, name).pipe(
withReducers({
toggle: (state) => !state,
setTrue: () => true,
setFalse: () => false,
reset: () => initState,
}),
export const reatomBoolean = (init = false, name?: string): BooleanAtom =>
atom(init, name).pipe(
withAssign((target, name) => ({
toggle: action((ctx) => target(ctx, (prev) => !prev), `${name}.toggle`),
setTrue: action((ctx) => target(ctx, true) as true, `${name}.setTrue`),
setFalse: action(
(ctx) => target(ctx, false) as false,
`${name}.setFalse`,
),
reset: action((ctx) => target(ctx, init), `${name}.reset`),
})),
)
67 changes: 35 additions & 32 deletions packages/primitives/src/reatomEnum.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,71 @@
import { atom, AtomMut, Rec } from '@reatom/core'
import { withReducers, WithReducers } from './withReducers'
import { action, Action, atom, AtomMut, Ctx, throwReatomError } from '@reatom/core'

export type EnumFormat = 'camelCase' | 'snake_case'

export type EnumAtom<
T extends string,
Format extends 'camelCase' | 'snake_case' = 'camelCase',
> = WithReducers<
AtomMut<T>,
{
[K in T as Format extends 'camelCase'
? `set${Capitalize<K>}`
: Format extends 'snake_case'
? `set_${K}`
: never]: () => K
} & {
reset: () => T
}
> & {
Format extends EnumFormat = 'camelCase',
> = AtomMut<T> & {
[Variant in T as Format extends 'camelCase'
? `set${Capitalize<Variant>}`
: Format extends 'snake_case'
? `set_${Variant}`
: never]: Action<[], Variant>
} & {
reset: Action<[], T>
enum: { [K in T]: K }
}

export type EnumAtomOptions<
T extends string,
Format extends 'camelCase' | 'snake_case' = 'camelCase',
Format extends EnumFormat = 'camelCase',
> = {
name?: string
format?: Format
initState?: T extends any ? T : never
initState?: T
}

export const reatomEnum = <
T extends string,
const T extends string,
Format extends 'camelCase' | 'snake_case' = 'camelCase',
>(
variants: ReadonlyArray<T>,
options: string | EnumAtomOptions<T, Format> = {},
): EnumAtom<T, Format> => {
) => {
const {
name,
format = 'camelCase' as Format,
initState = variants[0],
}: EnumAtomOptions<T, Format> = typeof options === 'string'
? { name: options }
: options
const cases = {} as Rec
const reducers = {} as Rec
? { name: options }
: options

const stateAtom = atom(initState, name) as EnumAtom<T, Format>
const enumAtom: typeof stateAtom = Object.assign((ctx: Ctx, update: any) => {
const state = stateAtom(ctx, update)
throwReatomError(!variants.includes(state), `invalid enum value "${state}" for "${name}" enum`)
return state
}, stateAtom)
const cases = (enumAtom.enum = {} as { [K in T]: K })

enumAtom.reset = action((ctx) => enumAtom(ctx, initState!), `${name}.reset`)

for (const variant of variants) {
cases[variant] = variant

const reducerName = variant.replace(
const setterName = variant.replace(
/^./,
(firstLetter) =>
'set' +
(format === 'camelCase'
? firstLetter.toUpperCase()
: `_${firstLetter}`),
)
reducers[reducerName] = () => variant
}

reducers.reset = () => initState
; (enumAtom as any)[setterName] = action(
(ctx) => enumAtom(ctx, variant)!,
`${name}.${setterName}`,
)
}

// @ts-expect-error
return Object.assign(atom(initState, name).pipe(withReducers(reducers)), {
enum: cases,
})
return enumAtom as EnumAtom<T, Format>
}
Loading

0 comments on commit 3ac66fc

Please sign in to comment.