;
+}
+
+export interface RquiredMoonContextValue {
+ client: MoonClient;
+ store: QueryClient;
+}
+
+interface IMoonProviderProps {
+ // The links ( HTTP clients config)
+ links: ILink[];
+ // The global Moon client factory (like the moon-axios Axios client for moon https://github.com/dktunited/moon-axios)
+ clientFactory?: ClientFactory;
+ // eslint-disable-next-line no-undef
+ children: JSX.Element;
+ // The react-query cache object
+ store?: QueryClient;
+ // The react-query initial cache state (please see https://react-query.tanstack.com/docs/api#hydrationdehydrate for more details)
+ hydrate?: HydrateProps;
+}
+
+export type PropsWithoutMoon = Omit
;
+
+export type PropsWithMoon
= P & { client: MoonClient };
+
+export const MoonContext: React.Context = React.createContext({
+ client: null,
+ store: null
+});
+
+class MoonProvider extends React.Component {
+ readonly client: MoonClient;
+
+ constructor(props: IMoonProviderProps) {
+ super(props);
+ const { links, clientFactory } = this.props;
+ this.client = new MoonClient(links, clientFactory);
+ }
+
+ render() {
+ const { children, hydrate, store } = this.props;
+ const queryClient = getMoonStore(store);
+ return (
+
+
+ {children}
+
+
+ );
+ }
+}
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export function withMoon = any>(
+ WrappedComponent: React.ComponentClass | React.FunctionComponent
+) {
+ type WrappedComponentInstance = typeof WrappedComponent extends React.ComponentClass
+ ? InstanceType>
+ : ReturnType>;
+ type WrappedComponentPropsWithoutMoon = PropsWithoutMoon;
+
+ const WithMoonComponent: React.FunctionComponent,
+ WrappedComponentInstance
+ >> = ({ forwardedRef, ...rest }) => {
+ return (
+
+ {({ client, store }) => {
+ const componentProps = { client, store, ...rest } as Props;
+ return ;
+ }}
+
+ );
+ };
+
+ return React.forwardRef((props, ref) => {
+ // @ts-ignore I don't know how to implement this without breaking out of the types.
+ return ;
+ });
+}
+
+export default MoonProvider;
diff --git a/packages/moon/src/mutation.tsx b/packages/moon/src/mutation.tsx
new file mode 100644
index 0000000..dde9248
--- /dev/null
+++ b/packages/moon/src/mutation.tsx
@@ -0,0 +1,41 @@
+import { UseMutationResult } from "react-query";
+
+import { MutateType } from "./moonClient";
+import { Nullable } from "./typing";
+import useMutation, { IMutationProps } from "./useMutation";
+
+export interface IMutationChildrenProps
+ extends Omit<
+ UseMutationResult,
+ "mutate" | "mutateAsync" | "reset"
+ > {
+ actions: Pick, "reset"> & {
+ mutate: () => void;
+ mutateAsync: () => Promise;
+ };
+}
+
+export type MutationChildren = (
+ props: IMutationChildrenProps
+ // eslint-disable-next-line no-undef
+) => Nullable;
+
+export interface IMutationComponentProps
+ extends IMutationProps {
+ children?: MutationChildren;
+}
+
+function Mutation(
+ props: IMutationComponentProps
+ // eslint-disable-next-line no-undef
+): Nullable {
+ const { children, ...mutationProps } = props;
+ const [state, actions] = useMutation(mutationProps);
+ return children ? children({ ...state, actions }) : null;
+}
+
+Mutation.defaultProps = {
+ type: MutateType.Post
+};
+
+export default Mutation;
diff --git a/packages/moon/src/query.tsx b/packages/moon/src/query.tsx
new file mode 100644
index 0000000..914a7eb
--- /dev/null
+++ b/packages/moon/src/query.tsx
@@ -0,0 +1,104 @@
+import * as React from "react";
+import { InfiniteData, UseQueryResult } from "react-query";
+
+import { PropsWithForwardRef, Nullable } from "./typing";
+import useQuery, { FetchPolicy, IQueryProps } from "./useQuery";
+import { useQueriesResults, ResultProps, useQueryResult, QueriesResults } from "./hooks";
+import { ClientConfig } from "./utils";
+
+export interface IQueryChildrenProps
+ extends Omit, "refetch" | "remove"> {
+ actions: Pick, "refetch" | "remove">;
+}
+
+export type QueryChildren = (
+ props: IQueryChildrenProps
+ // eslint-disable-next-line no-undef
+) => Nullable;
+
+export interface IQueryComponentProps
+ extends IQueryProps {
+ children?: QueryChildren;
+}
+
+function Query<
+ QueryVariables = any,
+ QueryResponse = any,
+ QueryData = QueryResponse,
+ QueryError = any,
+ QueryConfig extends ClientConfig = any
+>(
+ props: IQueryComponentProps
+ // eslint-disable-next-line no-undef
+): Nullable {
+ const { children, ...queryProps } = props;
+ const [state, actions] = useQuery(queryProps);
+ return children ? children({ ...state, actions }) : null;
+}
+
+Query.defaultProps = {
+ fetchPolicy: FetchPolicy.CacheAndNetwork
+};
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export function withQueryResult(
+ queryId: string,
+ resutToProps?: (result?: Data | InfiniteData) => QueryResultProps
+) {
+ type QueryProps = QueryResultProps | { queryResult: Data };
+ type WrappedComponentPropsWithoutQuery = Pick>;
+ return (WrappedComponent: React.ComponentClass | React.FunctionComponent) => {
+ type WrappedComponentInstance = typeof WrappedComponent extends React.ComponentClass
+ ? InstanceType>
+ : ReturnType>;
+ const WithQueryComponent: React.FunctionComponent,
+ WrappedComponentInstance
+ >> = props => {
+ const { forwardedRef, ...rest } = props;
+ const queryResult = useQueryResult(queryId, resutToProps);
+ const queryProps: QueryProps = resutToProps ? (queryResult as QueryResultProps) : { queryResult: queryResult as Data };
+ const componentProps = ({ ...queryProps, ...((rest as unknown) as WrappedComponentPropsWithoutQuery) } as unknown) as Props;
+ return ;
+ };
+
+ return React.forwardRef((props, ref) => {
+ // @ts-ignore I don't know how to implement this without breaking out of the types.
+ return ;
+ });
+ };
+}
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export function withQueriesResults(
+ queriesIds: string[],
+ resultsToProps?: (results: QueriesResults) => QueryResultProps
+) {
+ type QueryProps = QueryResultProps | { queriesResults: QueriesResults };
+ type WrappedComponentPropsWithoutQuery = Pick>;
+
+ return (WrappedComponent: React.ComponentClass | React.FunctionComponent) => {
+ type WrappedComponentInstance = typeof WrappedComponent extends React.ComponentClass
+ ? InstanceType>
+ : ReturnType>;
+ const WithQueryComponent: React.FunctionComponent> = props => {
+ const { forwardedRef, ...rest } = props;
+ const queriesResults = useQueriesResults(queriesIds, resultsToProps);
+ const componentProps = ({
+ queriesResults,
+ ...((rest as unknown) as WrappedComponentPropsWithoutQuery)
+ } as unknown) as Props;
+ return ;
+ };
+
+ return React.forwardRef((props, ref) => {
+ // @ts-ignore I don't know how to implement this without breaking out of the types.
+ return ;
+ });
+ };
+}
+
+export default Query;
diff --git a/src/typing.ts b/packages/moon/src/typing.ts
similarity index 78%
rename from src/typing.ts
rename to packages/moon/src/typing.ts
index 0c5e9c8..a350c9c 100644
--- a/src/typing.ts
+++ b/packages/moon/src/typing.ts
@@ -1,3 +1,5 @@
+import * as React from "react";
+
export type Nullable = P | null;
export type PropsWithForwardRef
= P & { forwardedRef?: React.RefObject };
diff --git a/packages/moon/src/useInfiniteQuery.tsx b/packages/moon/src/useInfiniteQuery.tsx
new file mode 100644
index 0000000..70f9ca1
--- /dev/null
+++ b/packages/moon/src/useInfiniteQuery.tsx
@@ -0,0 +1,104 @@
+import * as React from "react";
+import {
+ useInfiniteQuery as useInfiniteReactQuery,
+ UseInfiniteQueryResult,
+ UseInfiniteQueryOptions as ReactQueryConfig,
+ QueryFunctionContext,
+ QueryKey
+} from "react-query";
+
+import { useMoon, usePrevValue } from "./hooks";
+import { getQueryId } from "./useQuery";
+import { ClientConfig } from "./utils";
+
+export type IInfiniteQueryResultProps = [
+ Omit, "fetchNextPage" | "fetchPreviousPage" | "refetch" | "remove">,
+ Pick, "fetchNextPage" | "fetchPreviousPage" | "refetch" | "remove"> & {
+ cancel: () => void;
+ }
+];
+
+export interface IInfiniteQueryProps<
+ QueryVariables = any,
+ QueryResponse = any,
+ QueryData = QueryResponse,
+ QueryError = any,
+ QueryConfig = any
+> {
+ id?: string;
+ /** The Link id of the http client. */
+ source?: string;
+ /** The REST end point. */
+ endPoint?: string;
+ /** The variables of your query. */
+ variables?: QueryVariables;
+ /** The http client options of your query. */
+ options?: QueryConfig;
+ /** The react-query config. Please see the react-query QueryConfig for more details. */
+ queryConfig?: ReactQueryConfig;
+}
+
+export default function useInfiniteQuery<
+ QueryVariables = any,
+ QueryPageVariables = any,
+ QueryResponse = any,
+ QueryData = QueryResponse,
+ QueryError = any,
+ QueryConfig extends ClientConfig = any
+>({
+ id,
+ source,
+ endPoint,
+ variables,
+ options,
+ queryConfig
+}: IInfiniteQueryProps<
+ QueryVariables & QueryPageVariables,
+ QueryResponse,
+ QueryData,
+ QueryError,
+ QueryConfig
+>): IInfiniteQueryResultProps {
+ const { client, store } = useMoon();
+ const isInitialMount = React.useRef(true);
+
+ const clientProps = { source, endPoint, variables };
+ const queryId = getQueryId({ id, ...clientProps });
+ const { value, prevValue } = usePrevValue({ queryId, clientProps });
+
+ const queryOptions: ReactQueryConfig = React.useMemo(
+ () => store.defaultQueryObserverOptions(queryConfig),
+ [queryConfig, store]
+ );
+
+ function fetch({ pageParam }: QueryFunctionContext) {
+ const queryVariables = { ...variables, ...pageParam } as QueryVariables & QueryPageVariables;
+ return client.query(
+ source,
+ endPoint,
+ queryVariables,
+ options
+ );
+ }
+
+ const queryResult = useInfiniteReactQuery(queryId, fetch, queryOptions);
+
+ const { fetchNextPage, fetchPreviousPage, refetch, remove, ...others } = queryResult;
+
+ React.useEffect(() => {
+ if (prevValue.queryId === value.queryId && !isInitialMount.current && queryOptions?.enabled) {
+ // refetch on update and when only client options have been changed
+ refetch();
+ }
+ }, [value.clientProps]);
+
+ React.useEffect(() => {
+ isInitialMount.current = false;
+ }, []);
+
+ function cancel() {
+ store.cancelQueries(queryId, { exact: true });
+ }
+
+ return [others, { fetchNextPage, fetchPreviousPage, remove, refetch, cancel }];
+}
diff --git a/packages/moon/src/useMutation.tsx b/packages/moon/src/useMutation.tsx
new file mode 100644
index 0000000..19987ed
--- /dev/null
+++ b/packages/moon/src/useMutation.tsx
@@ -0,0 +1,77 @@
+import * as React from "react";
+import { useMutation as useReactMutation, UseMutationResult, MutationOptions } from "react-query";
+
+import { MutateType } from "./moonClient";
+import { useMoon } from "./hooks";
+import { ClientConfig } from "./utils";
+
+export type IMutationResultProps = [
+ Omit, "mutate" | "mutateAsync" | "reset">,
+ Pick, "reset"> & {
+ mutate: () => void;
+ mutateAsync: () => Promise;
+ dynamicMutate: UseMutationResult["mutate"];
+ dynamicMutateAsync: UseMutationResult["mutateAsync"];
+ }
+];
+
+export interface IMutationProps<
+ MutationVariables = any,
+ MutationResponse = any,
+ MutationError = any,
+ MutationClientConfig = any
+> {
+ /** The link id of the http client */
+ source?: string;
+ /** The REST end point */
+ endPoint?: string;
+ /** The variables of your mutation */
+ variables?: MutationVariables;
+ /** The mutation method. Default value: MutateType.Post */
+ type?: MutateType;
+ /** The http client options of your mutation. */
+ options?: MutationClientConfig;
+ /** The react-query config. Please see the react-query MutationConfig for more details. */
+ mutationConfig?: MutationOptions;
+}
+
+export default function useMutation<
+ MutationVariables = any,
+ MutationResponse = any,
+ MutationError = any,
+ MutationClientConfig extends ClientConfig = any
+>({
+ source,
+ endPoint,
+ type,
+ variables,
+ options,
+ mutationConfig
+}: IMutationProps): IMutationResultProps<
+ MutationResponse | undefined,
+ MutationError,
+ MutationVariables | undefined
+> {
+ const { client } = useMoon();
+ function mutation(variables: MutationVariables | undefined) {
+ return client.mutate(source, endPoint, type, variables, options);
+ }
+
+ const mutationFn = mutationConfig?.mutationFn || mutation;
+
+ const { mutate: reactQueryMutate, mutateAsync: reactQueryMutateAsync, reset, ...others } = useReactMutation<
+ MutationResponse | undefined,
+ MutationError,
+ MutationVariables | undefined,
+ unknown
+ >(mutationFn, mutationConfig);
+ const mutate = React.useCallback(() => {
+ return reactQueryMutate(variables);
+ }, [variables]);
+
+ const mutateAsync = React.useCallback(() => {
+ return reactQueryMutateAsync(variables);
+ }, [variables]);
+
+ return [others, { mutate, mutateAsync, reset, dynamicMutate: reactQueryMutate, dynamicMutateAsync: reactQueryMutateAsync }];
+}
diff --git a/packages/moon/src/usePrefetchQuery.tsx b/packages/moon/src/usePrefetchQuery.tsx
new file mode 100644
index 0000000..7fe801b
--- /dev/null
+++ b/packages/moon/src/usePrefetchQuery.tsx
@@ -0,0 +1,37 @@
+import * as React from "react";
+import { UseQueryOptions as ReactQueryConfig } from "react-query";
+
+import { useMoon } from "./hooks";
+import { getQueryId, IQueryProps } from "./useQuery";
+import { ClientConfig } from "./utils";
+
+export default function usePrefetchQuery<
+ QueryVariables = any,
+ QueryResponse = any,
+ QueryData = QueryResponse,
+ QueryError = any,
+ QueryConfig extends ClientConfig = any
+>({
+ id,
+ source,
+ endPoint,
+ variables,
+ options,
+ queryConfig
+}: IQueryProps): () => Promise {
+ const { client, store } = useMoon();
+ const queryId = getQueryId({ id, source, endPoint, variables });
+
+ const queryOptions: ReactQueryConfig = React.useMemo(
+ () => store.defaultQueryObserverOptions(queryConfig),
+ [queryConfig, store]
+ );
+
+ function fetch() {
+ return client.query(source, endPoint, variables, options);
+ }
+ async function prefetch() {
+ await store.prefetchQuery(queryId, fetch, queryOptions);
+ }
+ return prefetch;
+}
diff --git a/packages/moon/src/useQuery.tsx b/packages/moon/src/useQuery.tsx
new file mode 100644
index 0000000..57eb1b9
--- /dev/null
+++ b/packages/moon/src/useQuery.tsx
@@ -0,0 +1,125 @@
+import * as React from "react";
+import { useQuery as useReactQuery, UseQueryResult, UseQueryOptions as ReactQueryConfig } from "react-query";
+
+import { useMoon, usePrevValue } from "./hooks";
+import { ClientConfig, getId } from "./utils";
+
+export enum FetchPolicy {
+ // always try reading data from your cache first
+ CacheFirst = "cache-first",
+ // first trying to read data from your cache
+ CacheAndNetwork = "cache-and-network",
+ // never return you initial data from the cache
+ NetworkOnly = "network-only"
+}
+
+export type IQueryResultProps = [
+ Omit, "refetch" | "remove">,
+ Pick, "refetch" | "remove"> & {
+ cancel: () => void;
+ }
+];
+
+export interface IQueryProps<
+ QueryVariables = any,
+ QueryResponse = any,
+ QueryData = QueryResponse,
+ QueryError = any,
+ QueryConfig = any
+> {
+ id?: string;
+ /** The Link id of the http client. */
+ source?: string;
+ /** The REST end point. */
+ endPoint?: string;
+ /** The variables of your query. */
+ variables?: QueryVariables;
+ /**
+ * The fetch policy is an option which allows you to
+ * specify how you want your component to interact with
+ * the Moon data cache. Default value: FetchPolicy.CacheAndNetwork */
+ fetchPolicy?: FetchPolicy;
+ /** The http client options of your query. */
+ options?: QueryConfig;
+ /** The react-query config. Please see the react-query QueryConfig for more details. */
+ queryConfig?: ReactQueryConfig;
+}
+
+export const getQueryId = (queryProps: Pick): string => {
+ return getId(queryProps);
+};
+
+export default function useQuery<
+ QueryVariables = any,
+ QueryResponse = any,
+ QueryData = QueryResponse,
+ QueryError = any,
+ QueryConfig extends ClientConfig = any
+>({
+ id,
+ source,
+ endPoint,
+ variables,
+ options,
+ fetchPolicy = FetchPolicy.CacheAndNetwork,
+ queryConfig
+}: IQueryProps): IQueryResultProps {
+ const { client, store } = useMoon();
+ const isInitialMount = React.useRef(true);
+
+ const clientProps = { source, endPoint, variables };
+ const queryId = getQueryId({ id, ...clientProps });
+ const { value, prevValue } = usePrevValue({ queryId, clientProps });
+
+ const cacheOnly = fetchPolicy === FetchPolicy.CacheFirst;
+ const networkOnly = fetchPolicy === FetchPolicy.NetworkOnly;
+
+ const queryOptions: ReactQueryConfig = React.useMemo(
+ () =>
+ store.defaultQueryObserverOptions({
+ ...queryConfig,
+ // to fix (react-query)
+ cacheTime: networkOnly ? 0 : queryConfig?.cacheTime
+ }),
+ [queryConfig, networkOnly, store]
+ );
+
+ if (isInitialMount.current && networkOnly) {
+ // remove cache if networkOnly
+ // @ts-ignore @react-query must update to undefined
+ store.setQueryData(queryId, queryConfig?.initialData);
+ }
+
+ function fetch() {
+ const cachedResult = store.getQueryData(queryId, { exact: true });
+ return cacheOnly && cachedResult
+ ? cachedResult
+ : client.query(source, endPoint, variables, options);
+ }
+
+ const queryResult = useReactQuery(queryId, fetch, queryOptions);
+
+ const { refetch, remove, ...others } = queryResult;
+
+ function cancel() {
+ return store.cancelQueries(queryId, { exact: true });
+ }
+
+ React.useEffect(() => {
+ if (prevValue.queryId === value.queryId && !isInitialMount.current && (queryOptions.enabled ?? true)) {
+ // cancel the prev query then refetch on update and when only client options have been changed
+ cancel().then(() => refetch());
+ }
+ }, [value.clientProps]);
+
+ React.useEffect(() => {
+ isInitialMount.current = false;
+ }, []);
+
+ const data = networkOnly && others.isFetching ? undefined : others.data;
+
+ return [
+ { ...others, data },
+ { refetch, remove, cancel }
+ ];
+}
diff --git a/packages/moon/src/utils/client.ts b/packages/moon/src/utils/client.ts
new file mode 100644
index 0000000..f79dbf8
--- /dev/null
+++ b/packages/moon/src/utils/client.ts
@@ -0,0 +1,87 @@
+/* eslint-disable import/prefer-default-export */
+
+import { QueryCache, QueryClient, MutationCache } from "react-query";
+
+interface InterceptorManagerUseParams {
+ onFulfilled?: (value: V) => V | Promise;
+ onRejected?: (error: any) => any;
+}
+
+export interface IInterceptors {
+ request?: InterceptorManagerUseParams[];
+ response?: InterceptorManagerUseParams[];
+}
+
+export interface IClients {
+ [id: string]: I;
+}
+
+export type ClientFactory = (
+ config?: C,
+ interceptors?: IInterceptors