Skip to content

Instantly share code, notes, and snippets.

@jenya239
Created August 6, 2021 08:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jenya239/544e19702fbc8d7f66dd7130d5c14252 to your computer and use it in GitHub Desktop.
Save jenya239/544e19702fbc8d7f66dd7130d5c14252 to your computer and use it in GitHub Desktop.
import React, { Context, Reducer, useContext, useMemo, useReducer } from 'react'
type ItemState<Item> =
| { status: 'loading'; id: number; item?: Item }
| { status: 'error'; id: number; error: string; item?: Item }
| { status: 'success'; id: number; item: Item }
interface IActions {
fetchItem: (id: number) => Promise<void>
reset: () => void
}
interface IViews<Item> {
getById: (id: number) => ItemState<Item> | undefined
}
type Action<Item> =
| { type: 'request'; id: number }
| { type: 'success'; item: Item }
| { type: 'failure'; id: number; error: string }
| { type: 'reset' }
const reducer = <Item extends { id: number }>(
state: ItemState<Item>[],
action: Action<Item>
): ItemState<Item>[] => {
let index
switch (action.type) {
case 'request':
index = state.findIndex((is) => is.id === action.id)
if (index >= 0) {
state[index] = Object.freeze({
status: 'loading',
id: action.id,
item: state[index].item,
})
return [...state]
}
return [
...state,
Object.freeze({
id: action.id,
status: 'loading',
}),
]
case 'success':
index = state.findIndex((is) => is.id === action.item.id)
if (index >= 0) {
state[index] = Object.freeze({
status: 'success',
id: action.item.id,
item: action.item,
})
return [...state]
}
return [
...state,
Object.freeze({
id: action.item.id,
item: action.item,
status: 'success',
}),
]
case 'failure':
index = state.findIndex((is) => is.id === action.id)
if (index >= 0) {
state[index] = Object.freeze({
status: 'error',
id: action.id,
error: action.error,
item: state[index].item,
})
return [...state]
}
return [
...state,
Object.freeze({
status: 'error',
id: action.id,
error: action.error,
}),
]
case 'reset':
return []
}
}
interface CollectionContextValue<Item> {
state: ItemState<Item>[]
actions?: IActions
views?: IViews<Item>
}
export const createContext = <Item extends { id: number }>(): Context<
CollectionContextValue<Item>
> => React.createContext<CollectionContextValue<Item>>({ state: [] })
type Dispatch<Item> = (action: Action<Item>) => void
export const useCollectionReducer = <Item extends { id: number }>(): [
ItemState<Item>[],
Dispatch<Item>
] => useReducer<Reducer<ItemState<Item>[], Action<Item>>>(reducer, [])
export const useCollectionActions = <Item extends { id: number }>(
dispatch: Dispatch<Item>,
getItem?: (id: number) => Promise<Item>
): IActions | undefined =>
useMemo(
() =>
!getItem
? undefined
: {
reset: () => dispatch({ type: 'reset' }),
fetchItem: async (id: number) => {
dispatch({ type: 'request', id })
try {
dispatch({ type: 'success', item: await getItem(id) })
} catch (e) {
console.error('error', e)
dispatch({ type: 'failure', error: e, id })
}
},
},
[getItem]
)
export const useCollectionViews = <Item extends { id: number }>(
state: ItemState<Item>[]
): IViews<Item> =>
useMemo(
() =>
Object.freeze({
getById: (id: number) => state.find((is) => is.id === id),
}),
[state]
)
type Hook<Item> = { isFetchable: false } | ({ isFetchable: true } & IActions & IViews<Item>)
export const useCollectionContext = <Item extends { id: number }>(
Context: Context<CollectionContextValue<Item>>,
name: string
): Hook<Item> => {
const c = useContext(Context)
if (c === undefined) {
throw new Error(`useContext must be used within a ${name}CollectionProvider`)
}
if (!c.actions || !c.views) return { isFetchable: false }
return { isFetchable: true, ...c.actions, ...c.views }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment