Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 馃И #3

Merged
merged 11 commits into from
Sep 22, 2020
Prev Previous commit
Next Next commit
feat: relay and core development
  • Loading branch information
enisdenjo committed Sep 22, 2020
commit 664eae303eeaacf26012e0c2b8aacea2f424375f
28 changes: 28 additions & 0 deletions heltin-app/src/lib/useOnValueChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
*
* useOnValueChange
*
*/

import { useEffect, DependencyList, useRef } from 'react';
import { useDeepMemoOnValue } from '@domonda/react-plumb';

/** When the value changes, the callback will be triggered with the latest value. */
export function useOnValueChange<T>(
value: T,
callback: (value: T) => void,
deps: DependencyList = [],
) {
const memoValue = useDeepMemoOnValue(value);
const stateRef = useRef({ callback, isCalledOnce: false });
if (stateRef.current.callback !== callback) {
stateRef.current.callback = callback;
}
useEffect(() => {
if (stateRef.current.isCalledOnce) {
stateRef.current.callback(memoValue);
} else {
stateRef.current.isCalledOnce = true;
}
}, [memoValue, ...deps]);
}
67 changes: 67 additions & 0 deletions heltin-app/src/lib/usePanic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
*
* usePanic
*
* Throws the error in the React DOM. Useful when you have nested uncaught callbacks
* but you want to throw the error to be catched by the first error boundary.
*
*/

import React, { createContext, useState, useCallback, useContext, useRef, useEffect } from 'react';

const PanicContext = createContext<(error: Error) => void>(() => {
/**/
});

/**
* In cases where the usePanic hook gets unmounted before reporting an error
* this `Panicer` comes useful. Putting it somewhere in the root, will throw
* from there if the usePanic hook is unmounted but an error needs reporting.
*/
export const Panicer: React.FC = (props) => {
const { children } = props;
const [err, setErr] = useState<Error | null>(null);
if (err) {
throw err;
}

const setError = useCallback((error: Error) => setErr(error), []);
return <PanicContext.Provider value={setError}>{children}</PanicContext.Provider>;
};

export function usePanic(): [(err: Error) => void, (func: () => void | Promise<void>) => void] {
const mountedRef = useRef(true);
useEffect(
() => () => {
mountedRef.current = false;
},
[],
);

const [err, setErr] = useState<Error | null>(null);
if (err) {
throw err;
}

const setContextError = useContext(PanicContext);
const setError = useCallback((error: Error) => {
if (mountedRef.current) {
// inline error if component is mounted
setErr(error);
} else {
// send error to panicer context when unmounted
setContextError(error);
}
}, []);

return [
setError,
useCallback(async (func: () => void) => {
try {
await func();
} catch (err) {
setError(err instanceof Error ? err : new Error(err));
}
}, []),
];
}
38 changes: 38 additions & 0 deletions heltin-app/src/lib/useSuspend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
*
* useSuspend
*
*/

import { useState, useCallback } from 'react';
import { usePanic } from './usePanic';

interface Sink {
reject(error: Error): void;
resolve(): void;
complete(error?: Error): void;
}

export function useSuspend() {
const [panic] = usePanic();
const [promise, setPromise] = useState<Promise<void> | null>(null);
if (promise) {
throw promise;
}
return useCallback((promise: (() => Promise<void>) | ((sink: Sink) => void)) => {
setPromise(
new Promise((resolve, reject) => {
const maybePromise = promise({
reject,
resolve,
complete: (err) => (err ? reject(err) : resolve()),
});
if (maybePromise instanceof Promise) {
maybePromise.catch(reject).then(resolve);
}
})
.catch(panic)
.then(() => setPromise(null)),
);
}, []);
}
6 changes: 5 additions & 1 deletion heltin-app/src/relay/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { network } from './network';
const recordSource = new RecordSource();

// The source of truth for an instance of `RelayRuntime`, holding the canonical set of records in the form of a `RecordSource`.
const store = new Store(recordSource, { gcReleaseBufferSize: 10 });
const store = new Store(recordSource, { gcReleaseBufferSize: 25 });

// Environment providing a high-level API for interacting with both the `Store` and the `Network`.
export const environment = new Environment({
Expand Down Expand Up @@ -59,6 +59,7 @@ export const environment = new Environment({
if (arg.kind === 'Literal') {
return (arg as any).value; // value does exist on literal arguments
}

return variables[(arg as any).variableName]; // variableName does exist on variable arguments
}
return undefined;
Expand All @@ -67,6 +68,9 @@ export const environment = new Environment({
],
});

// Enable the "precise type refinement" feature flag. Relay v10+ only.
require('relay-runtime').RelayFeatureFlags.ENABLE_PRECISE_TYPE_REFINEMENT = true;

// Initialization of client side states.
import { init as sessionInit } from './client/session';
sessionInit(environment);
4 changes: 3 additions & 1 deletion heltin-app/src/relay/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { graphql, fetchQuery, useRelayEnvironment } from 'react-relay/hooks';
export * from './useValueForRefetch';
export * from './useValueForPagination';
export * from './useSubscription';
export { graphql } from 'react-relay';
export * from './useLoadableFragment';
export * from './useSuspendMutation';
44 changes: 44 additions & 0 deletions heltin-app/src/relay/hooks/useLoadableFragment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
*
* relay/useLoadableFragment
*
*/

import { unstable_useTransition } from 'react';
import { GraphQLTaggedNode, OperationType } from 'relay-runtime';
import { useRefetchableFragment, usePaginationFragment } from 'react-relay/hooks';

interface KeyType {
readonly ' $data'?: unknown;
}

export function useLoadableRefetchableFragment<O extends OperationType, K extends KeyType>(
fragmentInput: GraphQLTaggedNode,
parentFragmentRef: K,
) {
const [startTransition, isRefetching] = unstable_useTransition({ timeoutMs: Infinity });
const [data, refetch] = useRefetchableFragment<O, K>(fragmentInput, parentFragmentRef);
return {
data,
isRefetching,
refetch: (((...args: any[]) =>
startTransition(() => (refetch as any)(...args))) as any) as typeof refetch,
};
}

export function useLoadablePaginationFragment<O extends OperationType, K extends KeyType>(
fragmentInput: GraphQLTaggedNode,
parentFragmentRef: K,
) {
const [startTransition, isRefetching] = unstable_useTransition({ timeoutMs: Infinity });
const { refetch, ...fragment } = usePaginationFragment<O, K>(fragmentInput, parentFragmentRef);
return {
...fragment,
isRefetching,
refetch: ((...args: any[]) => {
startTransition(() => {
(refetch as any)(...args);
});
}) as typeof refetch,
};
}
12 changes: 10 additions & 2 deletions heltin-app/src/relay/hooks/useSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@
import { useSubscription as rUseSubscription } from 'react-relay/hooks';
import { GraphQLSubscriptionConfig, OperationType } from 'relay-runtime';
import { useDeepMemoOnValue } from '@domonda/react-plumb';
import { usePanic } from 'lib/usePanic';

export function useSubscription<O extends OperationType>(config: GraphQLSubscriptionConfig<O>) {
const memoConfig = useDeepMemoOnValue(config);
export function useSubscription<O extends OperationType>({
onError,
...config
}: GraphQLSubscriptionConfig<O>) {
const [panic] = usePanic();
const memoConfig = useDeepMemoOnValue({
...config,
onError: onError || panic,
});
return rUseSubscription(memoConfig);
}
123 changes: 123 additions & 0 deletions heltin-app/src/relay/hooks/useSuspendMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
*
* useSuspendMutation
*
*/

import { unstable_useTransition, useCallback, DependencyList } from 'react';
import { GraphQLTaggedNode, MutationParameters, Disposable, commitMutation } from 'relay-runtime';
import { useMutation } from 'react-relay/hooks';
import { UseMutationConfig } from 'react-relay/lib/relay-experimental/useMutation';
import { useSuspend } from 'lib/useSuspend';

const noop = () => {
/**/
};

export type UseSuspendMutationConfig<M extends MutationParameters> = Omit<
UseMutationConfig<M>,
'onError' | 'onUnsubscribe'
>;

export function useSuspendMutation<M extends MutationParameters>(
mutation: GraphQLTaggedNode,
config?: Omit<UseSuspendMutationConfig<M>, 'variables'>,
): (config: UseSuspendMutationConfig<M>) => Disposable {
const suspend = useSuspend();
const [commit] = useMutation<M>(mutation, (environment, commitConfig) => {
let disposed = false,
inFlightDisposable: Disposable,
inFlightResolve: () => void;

suspend(({ resolve, reject }) => {
const sink = {
onError: disposed ? noop : reject,
onCompleted: (result: M['response'], error: unknown) => {
if (commitConfig.onCompleted) {
commitConfig.onCompleted(result, error as any);
}
if (config?.onCompleted) {
config?.onCompleted(result, error as any);
}
disposed ? noop : error ? reject(error as any) : resolve();
},
};

inFlightResolve = resolve;
inFlightDisposable = commitMutation(
environment,
config
? {
...config,
...commitConfig,
...sink,
}
: {
...commitConfig,
...sink,
},
);
});

return {
dispose: () => {
disposed = true;
if (inFlightDisposable) {
inFlightDisposable.dispose();
}
if (inFlightResolve) {
inFlightResolve();
}
},
};
});
return commit;
}

export function useTransitionMutation<M extends MutationParameters>(
mutation: GraphQLTaggedNode,
config?: Omit<UseSuspendMutationConfig<M>, 'variables'>,
deps?: DependencyList,
): [(config: UseSuspendMutationConfig<M>) => Disposable, boolean] {
const [startTransition, isInFlight] = unstable_useTransition({ timeoutMs: Infinity });
const commit = useSuspendMutation<M>(mutation, config);
const transitionCommit = useCallback<(config: UseSuspendMutationConfig<M>) => Disposable>(
(commitConfig) => {
const disposableRef = { current: null as Disposable | null };
startTransition(() => {
disposableRef.current = commit(commitConfig);
});
return {
dispose: () => {
if (disposableRef.current) {
disposableRef.current.dispose();
}
},
};
},
[commit, ...(deps ?? [])],
);
return [transitionCommit, isInFlight];
}

export function usePromiseMutation<M extends MutationParameters>(
mutation: GraphQLTaggedNode,
config?: Omit<UseMutationConfig<M>, 'variables' | 'onError' | 'onUnsubscribe' | 'onCompleted'>,
deps?: DependencyList,
): (
config: Omit<UseMutationConfig<M>, 'onError' | 'onUnsubscribe' | 'onCompleted'>,
) => Promise<M['response']> {
const [commit] = useMutation<M>(mutation);
return useCallback(
(commitConfig: Omit<UseMutationConfig<M>, 'onError' | 'onUnsubscribe' | 'onCompleted'>) =>
new Promise<M['response']>((resolve, reject) => {
commit({
...config,
...commitConfig,
onError: reject,
onCompleted: (response) => resolve(response),
});
}),
[commit, ...(deps ?? [])],
);
}
13 changes: 13 additions & 0 deletions heltin-app/src/relay/hooks/useValueForPagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ import { useDeepMemoOnValue, useSafeState } from '@domonda/react-plumb';
export function useValueForPagination<T extends { count: number }>(
value: T,
relay: RelayPaginationProp,
options: {
onRefetch?: (value: T) => void;
onLoadMore?: () => void;
} = {},
): [() => void, boolean, Error | null] {
const memoValue = useDeepMemoOnValue(value);
const [{ loading, error }, setState] = useSafeState<{ loading: boolean; error: Error | null }>({
loading: false,
error: null,
});
const onRefetchRef = useRef(options.onRefetch);
const onLoadMoreRef = useRef(options.onLoadMore);

// we ignore refetching on init because that is handled by the `QueryRenderer`
const initRef = useRef(true);
Expand All @@ -35,6 +41,10 @@ export function useValueForPagination<T extends { count: number }>(
memoValue,
);

if (onRefetchRef.current) {
onRefetchRef.current(memoValue);
}

return () => {
if (disposable) {
disposable.dispose();
Expand All @@ -46,6 +56,9 @@ export function useValueForPagination<T extends { count: number }>(
if (relay.hasMore() && !relay.isLoading()) {
setState({ loading: true, error: null });
relay.loadMore(memoValue.count, (err) => setState({ loading: false, error: err || null }));
if (onLoadMoreRef.current) {
onLoadMoreRef.current();
}
}
}, [memoValue]);

Expand Down
Loading