Skip to content

Instantly share code, notes, and snippets.

@artalar
Last active November 27, 2020 00:42
Show Gist options
  • Save artalar/55633a46b8a69146a31a053bdc9630eb to your computer and use it in GitHub Desktop.
Save artalar/55633a46b8a69146a31a053bdc9630eb to your computer and use it in GitHub Desktop.
declare const IS_ACTION_FETCH: unique symbol
/**
* Fetch fabric with statuses actions
* @param {(payload: Payload) => Promise<Result>} fetcher
* @param {{
* onDone?: (result: Result, store: Store) => void
* onFail?: (error: unknown, store: Store) => void
* }} hooks
* @returns {{
* (payload: Payload): {
* payload: Payload
* type: string
* }
* done: PayloadActionCreator<Result>
* fail: PayloadActionCreator<unknown>
* isPending: Atom<boolean>
* cast(store: Store, payload: Payload): Promise<Result>
* }}
*
* @example
* ```ts
* const fetchUser = declareActionFetch<User, string>((id: string) => fetch(`/api/user/${id}`))
* // ...
* export const initUser = declareAction(async (id, store) => {
* try {
* const user = await fetchUser.cast(store, id) // user: User
* } catch(e) {
* if (e.code === 403) store.dispatch(goTo(ROUTES.signIn))
* }
* })
*
* export const userAtom = declareAtom<null | User>(null, on => [
* on(fetchUser.done, (state, user) => user),
* ])
* ```
*/
export function declareActionFetch<Result, Payload = undefined>(
fetcher: (payload: Payload) => Promise<Result>,
{
onDone = noop,
onFail = noop,
}: {
onDone?: (result: Result, store: Store) => void
onFail?: (error: unknown, store: Store) => void
} = {
onDone: noop,
onFail: noop,
},
): {
(payload: Payload): {
payload: Payload
type: string
}
done: PayloadActionCreator<Result>
fail: PayloadActionCreator<unknown>
isPending: Atom<boolean>
cast(store: Store, payload: Payload): Promise<Result>
[IS_ACTION_FETCH]: true
} {
const fetchAction: any = declareAction<Payload>(
'fetch call',
(payload, store) =>
fetcher(payload)
.then(response => {
store.dispatch(fetchAction.done(response))
return response
})
.catch(error => {
store.dispatch(fetchAction.fail(error))
throw error
}),
)
const done = declareAction<Result>('fetch done', onDone)
const fail = declareAction<unknown>('fetch fail', onFail)
const isPending = declareAtom(false, on => [
on(fetchAction, () => true),
on(done, () => false),
on(fail, () => false),
])
const cast = (store: Store, payload: Payload) => {
let res: any
let rej: any
const action = fetchAction(payload)
store.dispatch({
...action,
reactions: [
(...a) =>
action.reactions[0](...a).then(
payload => res(payload),
error => rej(error),
),
],
})
return new Promise((_res, _rej) => {
res = _res
rej = _rej
})
}
fetchAction.done = done
fetchAction.fail = fail
fetchAction.isPending = isPending
fetchAction.cast = cast
fetchAction[IS_ACTION_FETCH] = true
return fetchAction
}
// NORMALIZATION FABRIC EXAMPLE
import { declareAction, declareAtom } from '@reatom/core';
/**
*
* @param key
* @example
* ```ts
* // here `updateElement` will NOT trigger List rerender
* // that is good for performance
*
* function Element({ id }) {
* const el = useAtom(mapAtom, (map) => map[id], [id]);
*
* return <Component {...el} />;
* }
*
* function List() {
* const ids = useAtom(idsAtom);
*
* return ids.map((id) => <Element id={id} />);
* }
* ```
*/
export function declareNormAtoms<
T extends Record<string, any>,
Key extends keyof T
>(key: Key) {
const update = declareAction<T[]>();
const updateElement = declareAction<{
id: string;
element: Partial<T>;
}>();
const deleteElement = declareAction<string>();
const idsAtom = declareAtom<string[]>([], (on) => [
on(update, (state, payload) => payload.map((el) => el[key])),
on(deleteElement, (state, id) => state.filter((_id) => _id !== id)),
]);
const mapAtom = declareAtom<Record<string, T>>({}, (on) => [
on(update, (state, payload) =>
payload.reduce<Record<string, T>>((acc, el) => {
const id = el[key];
acc[id] = el;
return acc;
}, {})
),
on(updateElement, (state, { id, element }) => ({
...state,
[id]: {
...state[id],
...element,
},
})),
on(deleteElement, (state, id) => {
const { [id]: _deleted, ...newState } = state;
return newState;
}),
]);
return {
update,
updateElement,
deleteElement,
idsAtom,
mapAtom,
};
}
import { Atom, declareAtom, getState } from '@reatom/core'
export function noop() {}
/**
* @example
* ```js
* const listIdsAtom = filter(
* map(listAtom, list => list.map(({id}) => id)),
* (ids, nextIds) => ids.some(
* (id, i) => id !== nextIds[i]
* )
* )
* ```
*/
export function filter<T>(
atom: Atom<T>,
comparator: (state: T, nextState: T) => boolean,
): Atom<T> {
return declareAtom(getState(atom(), atom)!, on => [
on(atom, (state, nextState) =>
// @ts-ignore
comparator(state, nextState) ? nextState : state,
),
])
}
import { Atom, declareAtom, combine, getState, map } from '@reatom/core'
export function noop() {}
export function prev<T>(atom: Atom<T>): Atom<T>
export function prev<I, O>(atom: Atom<I>, mapper: (state: I) => O): Atom<O>
export function prev<T>(atom: Atom<T>, mapper: (state: T) => T = v => v) {
const initState = getState(atom(), atom)
return map(
declareAtom([initState, initState], on => [
on(atom, (state, value) => [state[1], value]),
]),
cache => mapper(cache[0]!),
)
}
/* model.js */
// static workflow
export const changeInput = declareAction()
export const inputAtom = declareAtom('', on => [
on(changeInput, (_, input) => input),
])
// lifetime workflow
export const init = declareAction(store => {
const ws = new Socket(url)
const initInput = store.getState(inputAtom)
ws.on('@init', input => {
if (store.getState(inputAtom) === initInput)
store.dispatch(changeInput(input))
})
const unsubscribe = store.subscribe(inputAtom, input =>
ws.send('input', input),
)
store.subscribe(cleanup, () => {
ws.disconnect()
unsubscribe()
})
})
export const cleanup = declareAction()
/* view.js */
const onInit = useAction(init)
const onCleanup = useAction(cleanup)
useEffect(() => {
onInit()
return () => onCleanup()
}, [])
/**
* Adds the possibility of waiting the action dispatch
* @param store
* @param actionCreator | atom
* @example
* const result = await take(store, myAction)
*/
export function take<T>(
store: Store,
target: PayloadActionCreator<T> | Atom<T>,
): Promise<T> {
let res: Function
const promise = new Promise(r => (res = r))
const unsubscribe = store.subscribe(target, value => {
unsubscribe()
res(value)
})
return promise as any
}
import { declareAtom, getTree } from "@reatom/core";
/**
* the same as effector.sample
* @param source atom for receive it data
* @param reason atom or action thats trigger data receiving
* @returns atom with source atom state
*
* @example
* ```js
* const validationAtom = declareAtom(
* on(trigger(formAtom, onBlur), (state, formData) => '...')
* )
* ```
*/
export const trigger = (
source, // atom
reason // atom or action
) => {
const defaultState = source()[getTree(source).id];
const accumulatorMetaAtom = declareAtom(
{
data: defaultState,
shouldUpdate: false
},
on => [
on(source, (state, data) => ({ data, shouldUpdate: false })),
on(reason, ({ data }) => ({ data, shouldUpdate: true }))
]
);
return declareAtom(defaultState, on => [
on(accumulatorMetaAtom, (state, { data, shouldUpdate }) =>
shouldUpdate ? data : state
)
]);
};
@artalar
Copy link
Author

artalar commented Dec 23, 2019

const union = (on, reducer, targets) => targets.map(target => on(target, reducer))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment