@zetql/core
v0.1.2
Published
No boilerplate<br /> Light and Easy state management library. <br/> Inspired by Zustand and react-query. (and some naming from rxjs)
Downloads
1
Readme
@zetql/core (vanila)
No boilerplate Light and Easy state management library. Inspired by Zustand and react-query. (and some naming from rxjs)
Feature
- Simple state manager (subject)
- Easy cache management and Share (cache)
- Fetch and Refetch System for query management (query)
- infinite paging management (infiniteQuery)
- pure vanilla & no boiler plate
- Easy implementation
Subject
Simple State Manager
createSubject takes object
or function returning object
as its argument to create init data
const subject = createSubject({isOpen: true})
const subject = createSubject( (set, get, subscribe) => ({ isOpen: true}) )
getter
const state = subject() // { isOpen: true }
// or you can take projector to select the state you only want
const isOpen = subject((s) => s.isOpen )
1. static setter
import { createSubject } from '@zetql/core';
export const detailModalSubject = createSubject<{
isOpen: boolean;
}>({ isOpen: false });
const { isOpen } = detailModalSubject();
// also you can use projector like the below.
// const isOpen = detailModalSubject((s) => s.isOpen)
const closeModal = () => detailModalSubject.setState({ isOpen: false });
const unsubscribe = detailModalSubject.subscribe(({ isOpen }) => {
alert(isOpen ? 'modal is open' : 'modal is closed');
});
2. method setter
import { createSubject } from '@zetql/core';
export const detailModalSubject = createSubject<{
isOpen: boolean;
openModal: () => void;
closeModal: () => void;
toggleModal: () => void;
}>((set) => {
return {
isOpen: false,
openModal: () => {
set({ isOpen: true });
},
closeModal: () => {
set({ isOpen: false });
},
toggleModal: () => {
set((state) => {
return { ...state, isOpen: !state.isOpen };
});
},
};
});
const { isOpen, openModal, closeModal, toggleModal } = detailModalSubject();
QuerySubject
Fetch and Refetch System of query management
fetchQuery & refetchQuery
- fetchQuery: fetchQuery if the cache is stale
- refetchQuery: calls fetchQuery with lately executed params (regardless its result whether it succeeded or failed)
import { createQuery } from '@zetql/core';
import { CouponInterface } from '...'
interface CouponListState {
coupons: Array<CouponInterface>;
}
export const couponQuery = createQuery<CouponListState, void>({
query: () => {
return fetch('/coupons')
.then(({ data }) => {
return { coupons: data };
})
},
initData: { coupons: [] },
});
const state1 = couponQuery();
// {
// error: null,
// data: { coupons: [] },
// dataDeps: undefined,
// lastDeps: undefined,
// isFetching: false,
// isLoading: false,
// isRefetching: false,
// initiated: false
// }
couponQuery.fetchQuery();
const fetchingStatus = couponQuery();
// { ...,
// isFetching: true, // query fetching it
// isRefetching: false,
// ...}
...
couponQuery.refetchQuery();
const refetchingStatus = couponQuery();
// { ...,
// isFetching: true,
// isRefetching: true,
// ...}
QueryState
type QueryState<State, Deps = any> = {
/**
* only when the latest one has error
*/
error: Error | null;
/**
* data from successful query
*/
data: State;
/**
* lastly successes query parameter
*/
dataDeps: Deps | undefined;
/**
* lastly requested query arguments, it can be in progress
* lastDeps === dataDeps means
* no query is in process & last one was successful
* lastDeps !== dataDeps means
*/
lastDeps: Deps | undefined;
/**
* query function is being called
*/
isFetching: boolean;
/**
* no cache found and query function is being called
* isLoading === false && isFetching === true
* means you have proper api for current deps to show before new query finishes
*
* isLoading === true && isFetching === true
* means you do not have proper api for current deps to show before new query finishes
*/
isLoading: boolean;
/**
* refetching flag
* */
isRefetching: boolean;
/**
* query function has been called
* */
initiated: boolean;
};
subscribe To State
subscribe
: subscribe to every change of the statesubscribeData
: subscribe to only datasubscribeError
: subscribe to only error
couponQuery.subscribe((state) => {
console.log(state);
});
couponQuery.subscribeData((state) => {
// only subscribe to data update
console.log(state);
// { data: {coupons: [....]},
// deps: undefined,
// queryId: '...',
// fromCache: false,
// }
});
couponQuery.subscribeError((state) => {
// only subscribe to error update
console.log(state);
// { error: Error; deps: undefined; queryId: '...' }
});
const { fetchQuery } = couponQuery;
const queryId = fetchQuery();
couponQuery.isActiveQuery(queryId)
// true
Query Retry Strategy
- retryCount: query retry count when the query is failed
- retryInterval: intervalTime between retries(ms)
export const couponQuery = createQuery<CouponListState, void>({
query: () => {
//....
},
initData: { coupons: [] },
retryCount: 2, // another 2times, default is 0
retryInterval: 300, // 300ms
});
Query Refetch Strategy
export const couponQuery = createQuery<CouponListState, void>({
query: () => {
//...
},
initData: { coupons: [] },
refetchOnReconnect: true,
refetchOnVisibilityChange: true,
});
// refetchOnReconnect & refetchOnVisibilityChange
// check cache is stale, only when cache is stale refetch is processing
// looping refetch
const { startInterval, stopInterval } = couponQuery
startInterval(3000)
startInterval(5000) // only keep one interval
stopInterval()
Query Cache
querySubject takes cache options to create cache
- staleTime(ms): span of time that one query data is valid
- extraCacheTime(ms): how long cache can be kept once the data are stale
Even a cache is stale, you can use it as placeholder before new query is executed.
if there is no cache from data, previous data would be displayed.
If we found the cache, it turns isLoading
flag on as well as isFetching
import { couponQuery } from "examples/src/subjects/coupon/coupon.query";
export const couponQuery = createQuery<CouponListState, undefined>({
query: fetchCoupons,
initData: { coupons: [] },
}, {
staleTime: 10 * 60_000, // 10minutes
extraCacheTime: 10 * 60_000 // another 10minutes to keep it
});
/**
* the cache is stale but extraCacheTime is still valid
*/
couponQuery.fetchQuery()
console.log(couponQuery())
// {..., isFetching: true, isLoading: false}
/**
* both statleTime & extraCacheTime have passed
*/
couponQuery.fetchQuery()
console.log(couponQuery())
// { isFetching: true, isLoading: true }
- isFetching: query is activated with/out stale cache available
- isLoading: query is activated without stale cache available
Query Cache Key
cache can be grouped by cacheGroupBy
it should return string.
Also cacheKey can be anything and is compared by shallowEqual.
For the sakes of performance, better to set cacheKeyBy
.
export const couponQuery = createQuery<CouponListState, {size: number}>({
query: fetchCoupons,
initData: { coupons: [] },
cacheKeyBy: (a) => a,
cacheGroupBy: ({ size }) => size.toString()
}, {
staleTime: 10 * 60_000, // stale cache for 10minutes
extraCacheTime: 10 * 60_000 // 10minutes cacheing for stale cache
});
Query Cache Share
cache can be shared among querySubjects. instead of cache options, you can create cacheDB and pass it as cache option
import { createCacheDB } from '@zetql/core'
const projectCache = createCacheDB(
{
staleTime: 10 * 60_000,
extraCacheTime: 10 * 60_000
}
)
const fetchProject = ({ projectName }) => {
//....
}
export const panelProject = createQuery<ProjectData, { projectName: string }>({
query: fetchCoupons,
cacheKeyBy: ({projectName}) => projectName,
initData: { data: null },
}, projectCache );
export const mainProject = createQuery<ProjectData, { projectName: string}>({
query: fetchCoupons,
cacheKeyBy: ({projectName}) => projectName,
initData: { data: null },
}, projectCache);
Infinite Query Subject
InfiniteQuery is working for infinitePagination.
It takes cursor(which means param of the query and should have form of object) to call
and takes getNextCursor
and getPrevCursor
to execute pagination.
To initiate the query use fetchQuery
and fetchNext
to call next(fetchPrev
to call prev).
If getNextCursor
or getPrevCursor
is not provided, fetchNext
and fetchPrev
would not work respectfully.
import { createInfiniteQuery } from '@zetql/core';
interface StockList {
stocks: Array<StockModel>;
nextCursor: number | undefined;
}
const fetchStocks: (param?: {
offset: number | undefined;
}) => Promise<StockList> = (offset) => {
const url =`/stocks-relative-cursor${
typeof offset?.offset === 'number' ? `?offset=${offset?.offset}` : ''
}`
return fetch(url).then(({ data, nextCursor }) => {
return { stocks: data, nextCursor };
});
};
const stockQuery = createInfiniteQuery<
StockList,
{ offset: number | undefined }
>({
query: fetchStocks,
normalize: (pages) => {
return pages.reduce((p: StockModel[], c) => {
return p.concat(c.data.stocks);
}, []);
},
cacheKeyBy(cursor){
return cursor.offset
},
getNextCursor: ({ cursor, data }) => {
return data.nextCursor ? { offset: data.nextCursor } : null;
}
});
const { fetchNext, fetchQuery } = stockQuery;
fetchQuery({ offset: 0 });
fetchNext();
getNextCursor | getPrevQuery
CursorCreator is function to provide prev or next cursor. It gets endcursor(the last success data at the edge) and error if error occurred. For instance, getNextCursor gets the last success data of forwards direction.
type CursorCreator<QData, Cursor> = (
endCursor: { data: QData; cursor: Cursor },
errorData: { error: Error; cursor: Cursor } | null
) => Cursor | null;
CursorMode
cursorMode can be static
or relative
.
static
means parameter for the query is already decided depends on which page is requested like the typical pagination.
Also you are sure that the response is pretty identical.
// this is static cursor
fetchPage({ size: 30, page: 2 })
fetchPage({ size: 30, page: 3 })
// it is always 3 after 2
relative
means depends on the response the request would be different, and response can be vary time to time and it is critical.
// relative
const response = await fetchPage({ cursor: 0 });
// { data: [...], nextCursor: 10 }
fetchPage({ cursor: response.nextCursor });
// cursor can be different depend on the response
cursor mode can be critical to call refetchQuery
and refetchStale
.
In static
cursorMode, refetching query would work in parallel and regardless of status of the prev query(fail or success), it keeps refetching to the end.
Also if
refetchErrorSettleMode
on error while refetching queries
partial
: it just update partial response (works only withcursorMode: static
)all-or-none
: it drops every response
if you'd like to use it for react and want to use hooks, use @zetql/react
instead
Using PaginationGroup
Grouping is supported with name passed on fetchQuery
fetchQuery({ offset: 0 }, 'size:20');
fetchQuery({ offset: 0 }, 'size:30');
fetchQuery({ offset: 0 }, 'q=la&size=30');
fetchQuery({ offset: 0 }, 'q=la');
when you switch groups, if previous query exists, cache can be used by passing function return the cursor it gets, instead of new cursor. argument cursor can be null, should return cursor always.
/**
* use cache on switching group
* */
fetchQuery( (cursor) => {
if (cursor) {
return cursor;
}
return { page: 1, category: '1' };
}, 'category=1');
/**
* do not use cache on switching group
* */
fetchQuery({ page: 1, category: '1' }, 'category=1');
Data Normalize
data from infiniteQuery is stored as array Array<{ cursor, data }> normalize is projector to change these forms.
/**
* without normalize
* */
const stockQuery = createInfiniteQuery<StockModel>({
// ...
});
stockQuery().data
// [ { stocks: ... }, ... ]
/**
* with normalize
* */
const noramlizedStockQuery = createInfiniteQuery({
normalize: (pages) => {
return pages.reduce((p: StockModel[], c: StockModel) => {
return p.concat(c.data.stocks);
}, []);
},
// ...
});
noramlizedStockQuery().data
// [...]
Infinite Query State
interface InfiniteQueryState<QData, Cursor, Normalized> {
data: Normalized;
isFetching: boolean;
isFetchingNext: boolean;
isFetchingPrev: boolean;
isRefetching: boolean;
groupKey: string; // current group key
lastCursor: Cursor | null;
nextCursor: Cursor | null;
prevCursor: Cursor | null;
error: CursorErrorData<Cursor> | null;
}