From a9c9f5797a352ef1ee98f7bcae5cd7e905c1b1bc Mon Sep 17 00:00:00 2001 From: Patrick Roberts Date: Fri, 30 Oct 2020 01:21:45 -0500 Subject: [PATCH] Changes: - Updated ESLint config to avoid false posititves - Renamed Context to IdContext - Added useServiceState hook - Added reset prop to ServiceProvider - Removed useReducer and useState from public API --- .eslintrc.json | 190 ++++++++++-------- src/Context/index.ts | 56 ------ src/{Context => IdContext}/Consumer/Props.ts | 4 +- src/{Context => IdContext}/Consumer/index.tsx | 17 +- src/{Context => IdContext}/Environment.ts | 2 +- src/{Context => IdContext}/Id.ts | 2 +- src/{Context => IdContext}/Provider/Props.ts | 9 +- src/{Context => IdContext}/Provider/index.tsx | 17 +- src/IdContext/index.ts | 49 +++++ src/Service/Consumer/Props.ts | 10 +- src/Service/Consumer/index.tsx | 28 +-- src/Service/Handler.ts | 39 +++- src/Service/PromiseState.ts | 8 +- src/Service/Provider/Props.ts | 21 +- src/Service/Provider/index.tsx | 36 ++-- src/Service/Resource.ts | 23 --- src/Service/Status.ts | 8 + src/Service/createUseHandler.ts | 35 ---- src/Service/index.ts | 59 ++++-- src/Service/useThenable.ts | 39 ---- src/State/Reset.ts | 9 +- src/State/index.ts | 3 - src/State/is.ts | 12 ++ .../{useReducer.ts => useResetReducer.ts} | 10 +- src/State/{useState.ts => useResetState.ts} | 14 +- src/State/useSync.ts | 19 ++ src/__fixtures__/withContext.tsx | 13 -- src/__fixtures__/withIdContext.tsx | 13 ++ src/__fixtures__/withService.tsx | 2 +- src/__tests__/Environment.test.ts | 2 +- .../{Context.test.tsx => IdContext.test.tsx} | 54 ++--- src/__tests__/Service.test.tsx | 114 +++++------ src/index.ts | 11 +- 33 files changed, 474 insertions(+), 454 deletions(-) delete mode 100644 src/Context/index.ts rename src/{Context => IdContext}/Consumer/Props.ts (68%) rename src/{Context => IdContext}/Consumer/index.tsx (55%) rename src/{Context => IdContext}/Environment.ts (90%) rename src/{Context => IdContext}/Id.ts (68%) rename src/{Context => IdContext}/Provider/Props.ts (55%) rename src/{Context => IdContext}/Provider/index.tsx (59%) create mode 100644 src/IdContext/index.ts create mode 100644 src/Service/Status.ts delete mode 100644 src/Service/createUseHandler.ts delete mode 100644 src/Service/useThenable.ts delete mode 100644 src/State/index.ts create mode 100644 src/State/is.ts rename src/State/{useReducer.ts => useResetReducer.ts} (80%) rename src/State/{useState.ts => useResetState.ts} (51%) create mode 100644 src/State/useSync.ts delete mode 100644 src/__fixtures__/withContext.tsx create mode 100644 src/__fixtures__/withIdContext.tsx rename src/__tests__/{Context.test.tsx => IdContext.test.tsx} (66%) diff --git a/.eslintrc.json b/.eslintrc.json index 817a2e4..001ff56 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,81 +1,109 @@ -{ - "globals": { - "JSX": "readonly" - }, - "env": { - "jest/globals": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", - "plugin:react-hooks/recommended", - "airbnb" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint", - "jest", - "react", - "react-hooks" - ], - "rules": { - "@typescript-eslint/consistent-type-definitions": [ - "error", - "interface" - ], - "@typescript-eslint/member-delimiter-style": "error", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/semi": "error", - "import/extensions": [ - "error", - "never", - { - "json": "always" - } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": [ - "*.js", - "**/*.test.ts", - "**/*.test.tsx" - ] - } - ], - "no-unused-vars": "off", - "react/jsx-filename-extension": [ - "error", - { - "extensions": [ - ".tsx" - ] - } - ], - "react/jsx-props-no-spreading": "off", - "react/state-in-constructor": [ - "error", - "never" - ], - "semi": "off" - }, - "settings": { - "import/resolver": { - "node": { - "extensions": [ - ".js", - ".ts", - ".tsx" - ] - } - } - } -} +{ + "globals": { + "JSX": "readonly" + }, + "env": { + "jest/globals": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "airbnb" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint", + "jest", + "react", + "react-hooks" + ], + "rules": { + "@typescript-eslint/consistent-type-definitions": [ + "error", + "interface" + ], + "@typescript-eslint/member-delimiter-style": "error", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-redeclare": "error", + "@typescript-eslint/no-shadow": "error", + "@typescript-eslint/semi": "error", + "@typescript-eslint/space-before-function-paren": [ + "error", + { + "named": "never", + "asyncArrow": "always" + } + ], + "function-paren-newline": [ + "error", + "consistent" + ], + "import/extensions": [ + "error", + "never", + { + "json": "always" + } + ], + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": [ + "*.js", + "**/*.test.ts", + "**/*.test.tsx" + ] + } + ], + "no-plusplus": [ + "error", + { + "allowForLoopAfterthoughts": true + } + ], + "no-redeclare": "off", + "no-shadow": "off", + "no-unused-vars": "off", + "object-curly-newline": [ + "error", + { + "consistent": true + } + ], + "react/jsx-filename-extension": [ + "error", + { + "extensions": [ + ".tsx" + ] + } + ], + "react/jsx-props-no-spreading": "off", + "react/state-in-constructor": [ + "error", + "never" + ], + "semi": "off", + "space-before-function-paren": "off" + }, + "settings": { + "import/resolver": { + "node": { + "extensions": [ + ".js", + ".ts", + ".tsx" + ] + } + } + } +} diff --git a/src/Context/index.ts b/src/Context/index.ts deleted file mode 100644 index 5157dbb..0000000 --- a/src/Context/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - Context as ReactContext, - createContext as createReactContext, - useContext as useReactContext, -} from 'react'; -import Id from './Id'; -import Environment, { unwrap } from './Environment'; -import ContextConsumer, { createConsumer } from './Consumer'; -import ContextProvider, { createProvider } from './Provider'; - -/** - * A privately scoped unique symbol for accessing Context internal Environment - * @internal - */ -const kEnvironment = Symbol('kEnvironment'); - -/** - * A Context with support for multiple keyed values - */ -export default interface Context { - Consumer: ContextConsumer; - Provider: ContextProvider; - /** @internal */ - [kEnvironment]: ReactContext>; -} - -/** - * Creates a keyed Context allowing multiple nested Providers to be accessible in the same scope - * @param defaultValue the value consumed if no Provider is in scope and the consumer `id` is - * `null` - */ -export function createContext(defaultValue: T): Context { - const EnvironmentContext = createReactContext>( - new Map([[null, defaultValue]]), - ); - - return { - Consumer: createConsumer(EnvironmentContext), - Provider: createProvider(EnvironmentContext), - [kEnvironment]: EnvironmentContext, - }; -} - -/** - * Consumes a value from a ContextProvider - * @param context the Context to use - * @param id the ContextProvider id to use - */ -export function useContext( - context: Context, - id: Id = null, -): T { - const env = useReactContext(context[kEnvironment]); - - return unwrap(env, id); -} diff --git a/src/Context/Consumer/Props.ts b/src/IdContext/Consumer/Props.ts similarity index 68% rename from src/Context/Consumer/Props.ts rename to src/IdContext/Consumer/Props.ts index e65a74a..3ed281f 100644 --- a/src/Context/Consumer/Props.ts +++ b/src/IdContext/Consumer/Props.ts @@ -1,9 +1,9 @@ import { ReactNode } from 'react'; import Id from '../Id'; -export default interface ContextConsumerProps { +export default interface IdContextConsumerProps { /** - * Which ContextProvider to use + * The {@link IdContextProvider} to use * @default null */ id?: Id; diff --git a/src/Context/Consumer/index.tsx b/src/IdContext/Consumer/index.tsx similarity index 55% rename from src/Context/Consumer/index.tsx rename to src/IdContext/Consumer/index.tsx index 29f9c13..2812983 100644 --- a/src/Context/Consumer/index.tsx +++ b/src/IdContext/Consumer/index.tsx @@ -1,18 +1,17 @@ -import React, { - Context, ComponentType, memo, useCallback, useMemo, -} from 'react'; +import React, { Context, ComponentType, memo, useCallback, useMemo } from 'react'; import Environment, { unwrap } from '../Environment'; -import Props, { defaultProps } from './Props'; +import IdContextConsumerProps, { defaultProps } from './Props'; -type ContextConsumer = ComponentType>; +type IdContextConsumer = ComponentType>; -export default ContextConsumer; +export default IdContextConsumer; +export { IdContextConsumerProps }; /** @ignore */ -export function createConsumer( +export function createIdContextConsumer( { Consumer }: Context>, -): ContextConsumer { - const EnvironmentConsumer: ContextConsumer = ({ id, children }) => { +): IdContextConsumer { + const EnvironmentConsumer: IdContextConsumer = ({ id, children }) => { const render = useCallback((env: Environment) => { const value = unwrap(env, id); diff --git a/src/Context/Environment.ts b/src/IdContext/Environment.ts similarity index 90% rename from src/Context/Environment.ts rename to src/IdContext/Environment.ts index ae4eca8..b34d96a 100644 --- a/src/Context/Environment.ts +++ b/src/IdContext/Environment.ts @@ -13,7 +13,7 @@ export function wrap( value: T, id: Id = null, ): Environment { - return new Map([...env, [id, value], [null, value]]); + return new Map(env).set(id, value).set(null, value); } /** @ignore */ diff --git a/src/Context/Id.ts b/src/IdContext/Id.ts similarity index 68% rename from src/Context/Id.ts rename to src/IdContext/Id.ts index c00fdcb..929824b 100644 --- a/src/Context/Id.ts +++ b/src/IdContext/Id.ts @@ -1,5 +1,5 @@ /** - * Provider id type. + * {@link IdContextProviderProps.id | Provider id} type. * Using the id `null` specifies the closest Provider. */ type Id = string | number | symbol | null; diff --git a/src/Context/Provider/Props.ts b/src/IdContext/Provider/Props.ts similarity index 55% rename from src/Context/Provider/Props.ts rename to src/IdContext/Provider/Props.ts index 49fcfcd..f19751b 100644 --- a/src/Context/Provider/Props.ts +++ b/src/IdContext/Provider/Props.ts @@ -1,16 +1,19 @@ import { ReactNode } from 'react'; import Id from '../Id'; -export default interface ContextProviderProps { +export default interface IdContextProviderProps { /** - * A value to provide + * The value to provide */ value: T; /** - * A key that allows nested Providers to be used + * The key that identifies the {@link IdContextProvider} to be consumed * @default null */ id?: Id; + /** + * @default null + */ children?: ReactNode; } diff --git a/src/Context/Provider/index.tsx b/src/IdContext/Provider/index.tsx similarity index 59% rename from src/Context/Provider/index.tsx rename to src/IdContext/Provider/index.tsx index ad40499..9b4d225 100644 --- a/src/Context/Provider/index.tsx +++ b/src/IdContext/Provider/index.tsx @@ -1,19 +1,18 @@ -import React, { - ComponentType, Context, memo, useContext, useMemo, -} from 'react'; +import React, { ComponentType, Context, memo, useContext, useMemo } from 'react'; import Environment, { wrap } from '../Environment'; -import Props, { defaultProps } from './Props'; +import IdContextProviderProps, { defaultProps } from './Props'; -type ContextProvider = ComponentType>; +type IdContextProvider = ComponentType>; -export default ContextProvider; +export default IdContextProvider; +export { IdContextProviderProps }; /** @ignore */ -export function createProvider( +export function createIdContextProvider( EnvironmentContext: Context>, -): ContextProvider { +): IdContextProvider { const { Provider } = EnvironmentContext; - const EnvironmentProvider: ContextProvider = ({ value, id, children }) => { + const EnvironmentProvider: IdContextProvider = ({ value, id, children }) => { const prev = useContext(EnvironmentContext); const next = useMemo(() => ( wrap(prev, value, id) diff --git a/src/IdContext/index.ts b/src/IdContext/index.ts new file mode 100644 index 0000000..2030480 --- /dev/null +++ b/src/IdContext/index.ts @@ -0,0 +1,49 @@ +import { Context, createContext, useContext } from 'react'; +import Id from './Id'; +import Environment, { unwrap } from './Environment'; +import IdContextConsumer, { createIdContextConsumer } from './Consumer'; +import IdContextProvider, { createIdContextProvider } from './Provider'; + +/** + * A privately scoped unique symbol for accessing {@link IdContext} internal {@link Environment} + * @internal + */ +const kEnvironment = Symbol('kEnvironment'); + +/** + * A Context with support for multiple keyed values + */ +export default interface IdContext { + Consumer: IdContextConsumer; + Provider: IdContextProvider; + /** @internal */ + [kEnvironment]: Context>; +} + +/** + * Creates a keyed Context allowing multiple nested Providers to be accessible in the same scope. + * @param defaultValue the value consumed if no {@link IdContextProvider} is in scope and the + * {@link IdContextConsumerProps.id | consumer `id`} is `null` + */ +export function createIdContext(defaultValue: T): IdContext { + const EnvironmentContext = createContext>( + new Map([[null, defaultValue]]), + ); + + return { + Consumer: createIdContextConsumer(EnvironmentContext), + Provider: createIdContextProvider(EnvironmentContext), + [kEnvironment]: EnvironmentContext, + }; +} + +/** + * Consumes a value from a {@link IdContextProvider} + * @param context the {@link IdContext} to use + * @param id the {@link IdContextProviderProps.id | IdContextProvider id} to use + */ +export function useIdContext(context: IdContext, id: Id = null): T { + const env = useContext(context[kEnvironment]); + + return unwrap(env, id); +} diff --git a/src/Service/Consumer/Props.ts b/src/Service/Consumer/Props.ts index 1adf5d5..115b0ad 100644 --- a/src/Service/Consumer/Props.ts +++ b/src/Service/Consumer/Props.ts @@ -1,13 +1,13 @@ -import { ReactNode } from 'react'; -import Id from '../../Context/Id'; +import { Dispatch, ReactNode, SetStateAction } from 'react'; +import Id from '../../IdContext/Id'; -export default interface ServiceConsumerProps { +export default interface ServiceConsumerProps { /** - * Which ServiceProvider to use + * The {@link ServiceProvider} to use * @default null */ id?: Id; - children: (value: TResponse) => ReactNode; + children: (value: TResponse, setState: Dispatch>) => ReactNode; } /** @ignore */ diff --git a/src/Service/Consumer/index.tsx b/src/Service/Consumer/index.tsx index 76e0296..69b8198 100644 --- a/src/Service/Consumer/index.tsx +++ b/src/Service/Consumer/index.tsx @@ -1,23 +1,25 @@ -import React, { - ComponentType, memo, useCallback, useMemo, -} from 'react'; -import Context from '../../Context'; +import React, { ComponentType, Dispatch, SetStateAction, memo, useCallback, useMemo } from 'react'; +import IdContext from '../../IdContext'; import Resource from '../Resource'; -import Props, { defaultProps } from './Props'; +import ServiceConsumerProps, { defaultProps } from './Props'; -type ServiceConsumer = ComponentType>; +type ServiceConsumer = + ComponentType>; export default ServiceConsumer; +export { ServiceConsumerProps }; /** @ignore */ -export function createConsumer( - { Consumer }: Context>, -): ServiceConsumer { - const ResourceConsumer: ServiceConsumer = ({ id, children }) => { - const render = useCallback((resource: Resource) => { - const value = resource(); +export function createServiceConsumer( + { Consumer }: IdContext<[Resource, Dispatch>]>, +): ServiceConsumer { + const ResourceConsumer: ServiceConsumer = ({ id, children }) => { + const render = useCallback(( + [resource, setState]: [Resource, Dispatch>], + ) => { + const response = resource(); - return children(value); + return children(response, setState); }, [children]); return useMemo(() => ( diff --git a/src/Service/Handler.ts b/src/Service/Handler.ts index 303a375..28d4eff 100644 --- a/src/Service/Handler.ts +++ b/src/Service/Handler.ts @@ -1,4 +1,8 @@ -import Id from '../Context/Id'; +import Id from '../IdContext/Id'; +import useSync from '../State/useSync'; +import PromiseState from './PromiseState'; +import Resource from './Resource'; +import Status from './Status'; /** * The type of asynchronous function for fetching data @@ -6,3 +10,36 @@ import Id from '../Context/Id'; type Handler = (request: TRequest, id: Id) => PromiseLike; export default Handler; + +/** @ignore */ +export function createUseHandler( + handler: Handler, +) { + return function useHandler(request: TRequest, id: Id = null): Resource { + return useSync(() => { + let state: PromiseState; + const promise = Promise.resolve(handler(request, id)).then( + (value) => { + state = { value, status: Status.Fulfilled }; + return value; + }, + (reason) => { + state = { reason, status: Status.Rejected }; + throw reason; + }, + ); + + state = { promise, status: Status.Pending }; + return () => { + const { status } = state; + + switch (state.status) { + case Status.Pending: throw state.promise; + case Status.Fulfilled: return state.value; + case Status.Rejected: throw state.reason; + default: throw new Error(`Unexpected status ${status}`); + } + }; + }, [request, id]); + }; +} diff --git a/src/Service/PromiseState.ts b/src/Service/PromiseState.ts index 7d04659..346d734 100644 --- a/src/Service/PromiseState.ts +++ b/src/Service/PromiseState.ts @@ -1,19 +1,21 @@ +import Status from './Status'; + /** @ignore */ interface PromiseStatePending { promise: Promise; - status: 'pending'; + status: Status.Pending; } /** @ignore */ interface PromiseStateFulfilled { value: TResponse; - status: 'fulfilled'; + status: Status.Fulfilled; } /** @ignore */ interface PromiseStateRejected { reason: any; - status: 'rejected'; + status: Status.Rejected; } /** @ignore */ diff --git a/src/Service/Provider/Props.ts b/src/Service/Provider/Props.ts index 7dd6d1d..ca2e0ab 100644 --- a/src/Service/Provider/Props.ts +++ b/src/Service/Provider/Props.ts @@ -1,24 +1,33 @@ import { ReactNode } from 'react'; -import Id from '../../Context/Id'; +import Id from '../../IdContext/Id'; +import Reset from '../../State/Reset'; export default interface ServiceProviderProps { /** - * A request passed to `useHandler()` for fetching an asynchronous resource. + * The request passed to {@link createService | handler} for fetching an asynchronous resource. */ - value: TRequest; + request: TRequest; /** - * A key that allows nested Providers to be consumed + * The key that identifies the {@link ServiceProvider} to be consumed * @default null */ id?: Id; + /** + * @default null + */ children?: ReactNode; /** - * A fallback to render if any children are suspended. + * The fallback to render if any children are suspended. * If the fallback is `null`, `undefined`, or omitted, then a Suspense - * component must be inserted elsewhere between the Provider and Consumer. + * component must be inserted elsewhere between the + * {@link ServiceProvider | Provider} and {@link ServiceConsumer | Consumer}. * @default null */ fallback?: NonNullable | null; + /** + * The reset function when {@link ServiceProviderProps.request | request} updates + */ + reset?: Reset; } /** @ignore */ diff --git a/src/Service/Provider/index.tsx b/src/Service/Provider/index.tsx index 9b05ed1..4eef62d 100644 --- a/src/Service/Provider/index.tsx +++ b/src/Service/Provider/index.tsx @@ -1,43 +1,43 @@ -import React, { - ComponentType, Suspense, isValidElement, memo, useMemo, -} from 'react'; -import Context from '../../Context/index'; -import Handler from '../Handler'; +import React, { ComponentType, Dispatch, SetStateAction, Suspense, isValidElement, memo, useMemo } from 'react'; +import Id from '../../IdContext/Id'; +import IdContext from '../../IdContext'; +import useResetState from '../../State/useResetState'; import Resource from '../Resource'; -import createUseHandler from '../createUseHandler'; -import Props, { defaultProps } from './Props'; +import ServiceProviderProps, { defaultProps } from './Props'; -type ServiceProvider = ComponentType>; +type ServiceProvider = ComponentType>; export default ServiceProvider; +export { ServiceProviderProps }; /** @ignore */ -export function createProvider( - { Provider }: Context>, - handler: Handler, +export function createServiceProvider( + { Provider }: IdContext<[Resource, Dispatch>]>, + useHandler: (request: TRequest, id?: Id) => Resource, ): ServiceProvider { - const useHandler = createUseHandler(handler); const ResourceProvider: ServiceProvider = ({ - value, id, children, fallback, + request, id, children, fallback, reset, }) => { - const resource = useHandler(value, id); + const [state, setState] = useResetState(request, reset); + const resource = useHandler(state, id); const element = useMemo(() => ( isValidElement(fallback) ? {children} - : <>{children} + : children ), [children, fallback]); return useMemo(() => ( - {element} - ), [resource, id, element]); + {element} + ), [resource, setState, id, element]); }; ResourceProvider.defaultProps = defaultProps; return memo(ResourceProvider, (prev, next) => ( - Object.is(prev.value, next.value) + Object.is(prev.request, next.request) && Object.is(prev.id, next.id) && Object.is(prev.children, next.children) && Object.is(prev.fallback, next.fallback) + && Object.is(prev.reset, next.reset) )); } diff --git a/src/Service/Resource.ts b/src/Service/Resource.ts index b6ffa2f..2f8cc4a 100644 --- a/src/Service/Resource.ts +++ b/src/Service/Resource.ts @@ -1,27 +1,4 @@ -import { useCallback } from 'react'; -import useThenable from './useThenable'; - /** @internal */ type Resource = () => TResponse; export default Resource; - -/** @ignore */ -export function useResource( - thenable: PromiseLike, -): Resource { - const ref = useThenable(thenable); - const resource = useCallback(() => { - const state = ref.current!; - const { status } = state; - - switch (state.status) { - case 'pending': throw state.promise; - case 'rejected': throw state.reason; - case 'fulfilled': return state.value; - default: throw new Error(`Unexpected status ${status}`); - } - }, [ref]); - - return resource; -} diff --git a/src/Service/Status.ts b/src/Service/Status.ts new file mode 100644 index 0000000..ed24316 --- /dev/null +++ b/src/Service/Status.ts @@ -0,0 +1,8 @@ +/** @ignore */ +const enum Status { + Pending, + Fulfilled, + Rejected +} + +export default Status; diff --git a/src/Service/createUseHandler.ts b/src/Service/createUseHandler.ts deleted file mode 100644 index 7c437ea..0000000 --- a/src/Service/createUseHandler.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - useCallback, useEffect, useRef, useState, -} from 'react'; -import Id from '../Context/Id'; -import Handler from './Handler'; -import Resource, { useResource } from './Resource'; - -/** @ignore */ -type Callbacks = Required['then']>>; - -/** @ignore */ -export default function createUseHandler( - handler: Handler, -) { - return function useHandler(request: TRequest, id: Id = null): Resource { - const ref = useRef>(); - const createPromise = useCallback(() => ( - new Promise((resolve, reject) => { - ref.current = [resolve, reject]; - }) - ), []); - const [promise, setPromise] = useState(createPromise); - const resource = useResource(promise); - - useEffect(() => { - const [onfulfilled, onrejected] = ref.current!; - - handler(request, id).then(onfulfilled, onrejected); - - return () => setPromise(createPromise); - }, [request, id, createPromise]); - - return resource; - }; -} diff --git a/src/Service/index.ts b/src/Service/index.ts index c45b44b..9b20fc8 100644 --- a/src/Service/index.ts +++ b/src/Service/index.ts @@ -1,12 +1,13 @@ -import Id from '../Context/Id'; -import Context, { createContext, useContext } from '../Context'; -import Handler from './Handler'; +import { Dispatch, SetStateAction } from 'react'; +import Id from '../IdContext/Id'; +import IdContext, { createIdContext, useIdContext } from '../IdContext'; +import Handler, { createUseHandler } from './Handler'; import Resource from './Resource'; -import ServiceConsumer, { createConsumer } from './Consumer'; -import ServiceProvider, { createProvider } from './Provider'; +import ServiceConsumer, { createServiceConsumer } from './Consumer'; +import ServiceProvider, { createServiceProvider } from './Provider'; /** - * A privately scoped unique symbol for accessing Service internal Resource + * A privately scoped unique symbol for accessing {@link Service} internal {@link Resource} * @internal */ const kResource = Symbol('kResource'); @@ -15,12 +16,15 @@ const kResource = Symbol('kResource'); * A Suspense integration for providing asynchronous data through a Context API */ export default interface Service { - Consumer: ServiceConsumer; + Consumer: ServiceConsumer; Provider: ServiceProvider; /** @internal */ - [kResource]: Context>; + [kResource]: IdContext<[Resource, Dispatch>]>; } +const defaultFn = () => { throw new Error('Provider is not in scope'); }; +const defaultValue: [Resource, Dispatch] = [defaultFn, defaultFn]; + /** * Creates a Service Context for providing asynchronous data * @param handler the asynchronous function for fetching data @@ -28,27 +32,38 @@ export default interface Service { export function createService( handler: Handler, ): Service { - const ResourceContext = createContext>(() => { - throw new Error('Provider is not in scope'); - }); + const ResourceContext = createIdContext<[ + Resource, Dispatch> + ]>(defaultValue); return { - Consumer: createConsumer(ResourceContext), - Provider: createProvider(ResourceContext, handler), + Consumer: createServiceConsumer(ResourceContext), + Provider: createServiceProvider(ResourceContext, createUseHandler(handler)), [kResource]: ResourceContext, }; } /** - * Synchronously consumes a response from a ServiceProvider - * @param service the Service to use - * @param id the ServiceProvider id to use + * Synchronously consumes a stateful response from a {@link ServiceProvider} + * @param service the {@link Service} to use + * @param id the {@link ServiceProviderProps.id | ServiceProvider id} to use + */ +export function useServiceState( + service: Service, id: Id = null, +): [TResponse, Dispatch>] { + const [resource, setState] = useIdContext(service[kResource], id); + const response = resource(); + + return [response, setState]; +} + +/** + * Synchronously consumes a response from a {@link ServiceProvider} + * @param service the {@link Service} to use + * @param id the {@link ServiceProviderProps.id | ServiceProvider id} to use */ -export function useService( - service: Service, - id: Id = null, -): TResponse { - const resource = useContext(service[kResource], id); +export function useService(service: Service, id: Id = null): TResponse { + const [response] = useServiceState(service, id); - return resource(); + return response; } diff --git a/src/Service/useThenable.ts b/src/Service/useThenable.ts deleted file mode 100644 index 60e3167..0000000 --- a/src/Service/useThenable.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MutableRefObject, useMemo, useRef } from 'react'; -import PromiseState from './PromiseState'; - -/** @ignore */ -type RefObject = MutableRefObject; - -/** @ignore */ -export default function useThenable( - thenable: PromiseLike, -): RefObject> { - const ref = useRef>(); - - useMemo(() => { - const promise = Promise.resolve(thenable).then( - (value) => { - const state = ref.current!; - - if (state.status === 'pending' && state.promise === promise) { - ref.current = { value, status: 'fulfilled' }; - } - - return value; - }, - (reason) => { - const state = ref.current!; - - if (state.status === 'pending' && state.promise === promise) { - ref.current = { reason, status: 'rejected' }; - } - - throw reason; - }, - ); - - ref.current = { promise, status: 'pending' }; - }, [thenable]); - - return ref; -} diff --git a/src/State/Reset.ts b/src/State/Reset.ts index 4456ba8..c6229d7 100644 --- a/src/State/Reset.ts +++ b/src/State/Reset.ts @@ -5,12 +5,7 @@ type Reset = (prevState: S, newInitialState: S) => S; export default Reset; -/** - * Resets the state with the value of the new initial state - * @internal - * @param prevState the previous state - * @param newInitialState the new initial state - */ -export function defaultReset(prevState: S, newInitialState: S): S { +/** @ignore */ +export function defaultReset(_: S, newInitialState: S): S { return newInitialState; } diff --git a/src/State/index.ts b/src/State/index.ts deleted file mode 100644 index 76d5c4e..0000000 --- a/src/State/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as Reset } from './Reset'; -export { default as useReducer } from './useReducer'; -export { default as useState } from './useState'; diff --git a/src/State/is.ts b/src/State/is.ts new file mode 100644 index 0000000..16100f8 --- /dev/null +++ b/src/State/is.ts @@ -0,0 +1,12 @@ +import { DependencyList } from 'react'; + +/** @ignore */ +export default function is(a: DependencyList | undefined, b: DependencyList | undefined): boolean { + if (!a || !b || a.length !== b.length) return false; + + for (let i = 0; i < a.length; ++i) { + if (!Object.is(a[i], b[i])) return false; + } + + return true; +} diff --git a/src/State/useReducer.ts b/src/State/useResetReducer.ts similarity index 80% rename from src/State/useReducer.ts rename to src/State/useResetReducer.ts index a083ce5..0599e6f 100644 --- a/src/State/useReducer.ts +++ b/src/State/useResetReducer.ts @@ -1,17 +1,17 @@ -import { - Dispatch, Reducer, ReducerAction, ReducerState, useRef, -} from 'react'; +import { Dispatch, Reducer, ReducerAction, ReducerState, useRef } from 'react'; import Reset, { defaultReset } from './Reset'; import useForceUpdate from './useForceUpdate'; /** + * @ignore * An extension of React useReducer that is sensitive to initialState. - * Intended to accept the return value of useContext or useService as initialState. + * Intended to accept the return value of + * {@link useIdContext} or {@link useService} as initialState. * @param reducer the reducer function when dispatch is called * @param initialState the initial state * @param reset the reset function when initialState updates */ -export default function useReducer>( +export default function useResetReducer>( reducer: R, initialState: ReducerState, reset: Reset> = defaultReset, ): [ReducerState, Dispatch>] { const forceUpdate = useForceUpdate(); diff --git a/src/State/useState.ts b/src/State/useResetState.ts similarity index 51% rename from src/State/useState.ts rename to src/State/useResetState.ts index ef12953..093fb0b 100644 --- a/src/State/useState.ts +++ b/src/State/useResetState.ts @@ -1,23 +1,25 @@ import { Dispatch, SetStateAction } from 'react'; import Reset, { defaultReset } from './Reset'; -import useReducer from './useReducer'; +import useResetReducer from './useResetReducer'; /** @ignore */ // eslint-disable-next-line no-undef -function isUpdater(setStateAction: SetStateAction): setStateAction is (prevState: S) => S { +function isFunction(setStateAction: SetStateAction): setStateAction is (prevState: S) => S { return typeof setStateAction === 'function'; } /** + * @ignore * An extension of React useState that is sensitive to initialState. - * Intended to accept the return value of useContext or useService as initialState. + * Intended to accept the return value of + * {@link useIdContext} or {@link useService} as initialState. * @param initialState the initial state * @param reset the reset function when initialState updates */ -export default function useState( +export default function useResetState( initialState: S, reset: Reset = defaultReset, ): [S, Dispatch>] { - return useReducer((prevState: S, setStateAction: SetStateAction) => ( - isUpdater(setStateAction) ? setStateAction(prevState) : setStateAction + return useResetReducer((prevState: S, setStateAction: SetStateAction) => ( + isFunction(setStateAction) ? setStateAction(prevState) : setStateAction ), initialState, reset); } diff --git a/src/State/useSync.ts b/src/State/useSync.ts new file mode 100644 index 0000000..3123882 --- /dev/null +++ b/src/State/useSync.ts @@ -0,0 +1,19 @@ +import { DependencyList, useRef } from 'react'; +import is from './is'; + +/** + * @ignore + * An alternative to React useMemo that provides a semantic guarantee of referential stability. + * @param factory The factory function to compute a referentially stable value + * @param deps The dependencies of the computation + */ +export default function useSync(factory: () => T, deps: DependencyList | undefined): T { + const { current } = useRef<[T?, DependencyList?]>([]); + + if (!is(current[1], deps)) { + current[0] = factory(); + current[1] = deps; + } + + return current[0]!; +} diff --git a/src/__fixtures__/withContext.tsx b/src/__fixtures__/withContext.tsx deleted file mode 100644 index a3def1f..0000000 --- a/src/__fixtures__/withContext.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { ComponentType, ReactNode } from 'react'; -import Id from '../Context/Id'; -import Context, { useContext } from '../Context'; - -const withContext: ( - context: Context -) => ComponentType<{ id?: Id }> = (context) => ({ id = null }) => ( -
- {useContext(context, id)} -
-); - -export default withContext; diff --git a/src/__fixtures__/withIdContext.tsx b/src/__fixtures__/withIdContext.tsx new file mode 100644 index 0000000..76a2281 --- /dev/null +++ b/src/__fixtures__/withIdContext.tsx @@ -0,0 +1,13 @@ +import React, { ComponentType, ReactNode } from 'react'; +import Id from '../IdContext/Id'; +import IdContext, { useIdContext } from '../IdContext'; + +const withIdContext: ( + context: IdContext +) => ComponentType<{ id?: Id }> = (context) => ({ id = null }) => ( +
+ {useIdContext(context, id)} +
+); + +export default withIdContext; diff --git a/src/__fixtures__/withService.tsx b/src/__fixtures__/withService.tsx index 9961986..083a8a0 100644 --- a/src/__fixtures__/withService.tsx +++ b/src/__fixtures__/withService.tsx @@ -1,5 +1,5 @@ import React, { ComponentType, ReactNode } from 'react'; -import Id from '../Context/Id'; +import Id from '../IdContext/Id'; import Service, { useService } from '../Service'; const withService: ( diff --git a/src/__tests__/Environment.test.ts b/src/__tests__/Environment.test.ts index 7ce4710..8e8ec45 100644 --- a/src/__tests__/Environment.test.ts +++ b/src/__tests__/Environment.test.ts @@ -1,4 +1,4 @@ -import Environment, { wrap, unwrap } from '../Context/Environment'; +import Environment, { wrap, unwrap } from '../IdContext/Environment'; describe('Environment', () => { let env: Environment; diff --git a/src/__tests__/Context.test.tsx b/src/__tests__/IdContext.test.tsx similarity index 66% rename from src/__tests__/Context.test.tsx rename to src/__tests__/IdContext.test.tsx index 05c14b2..9e2846e 100644 --- a/src/__tests__/Context.test.tsx +++ b/src/__tests__/IdContext.test.tsx @@ -1,27 +1,27 @@ import React, { ReactNode } from 'react'; import { ReactTestRenderer, act, create } from 'react-test-renderer'; import mockRender from '../__fixtures__/mockRender'; -import withContext from '../__fixtures__/withContext'; -import { createContext } from '../Context'; +import withIdContext from '../__fixtures__/withIdContext'; +import { createIdContext } from '../IdContext'; -describe('Context', () => { +describe('IdContext', () => { beforeEach(() => { mockRender.mockClear(); }); - let useContextSpy: jest.SpyInstance; + let useIdContextSpy: jest.SpyInstance; beforeAll(() => { - const actual = jest.requireActual('../Context'); - const actualUseContext = actual.useContext; + const actual = jest.requireActual('../IdContext'); + const actualUseIdContext = actual.useIdContext; - useContextSpy = jest.spyOn(actual, 'useContext').mockImplementation( - (context, id) => actualUseContext(context, id), + useIdContextSpy = jest.spyOn(actual, 'useIdContext').mockImplementation( + (context, id) => actualUseIdContext(context, id), ); }); beforeEach(() => { - useContextSpy.mockClear(); + useIdContextSpy.mockClear(); }); let root: ReactTestRenderer; @@ -32,7 +32,7 @@ describe('Context', () => { it('should pass default value to Consumer render callback', () => { const defaultValue = 1; - const UnderTest = createContext(defaultValue); + const UnderTest = createIdContext(defaultValue); act(() => { root = create( @@ -50,17 +50,17 @@ describe('Context', () => { `); }); - it('should pass default value to useContext hook', () => { + it('should pass default value to useIdContext hook', () => { const defaultValue = 1; - const UnderTest = createContext(defaultValue); - const Consumer = withContext(UnderTest); + const UnderTest = createIdContext(defaultValue); + const Consumer = withIdContext(UnderTest); act(() => { root = create(); }); - expect(useContextSpy).toBeCalledWith(UnderTest, null); - expect(useContextSpy).toReturnWith(defaultValue); + expect(useIdContextSpy).toBeCalledWith(UnderTest, null); + expect(useIdContextSpy).toReturnWith(defaultValue); expect(root.toJSON()).toMatchInlineSnapshot(`
${defaultValue} @@ -70,7 +70,7 @@ describe('Context', () => { it('should pass value from Provider to Consumer render callback', () => { const value = 2; - const UnderTest = createContext(1); + const UnderTest = createIdContext(1); act(() => { root = create( @@ -90,10 +90,10 @@ describe('Context', () => { `); }); - it('should pass value from Provider to useContext hook', () => { + it('should pass value from Provider to useIdContext hook', () => { const value = 2; - const UnderTest = createContext(1); - const Consumer = withContext(UnderTest); + const UnderTest = createIdContext(1); + const Consumer = withIdContext(UnderTest); act(() => { root = create( @@ -103,8 +103,8 @@ describe('Context', () => { ); }); - expect(useContextSpy).toBeCalledWith(UnderTest, null); - expect(useContextSpy).toReturnWith(value); + expect(useIdContextSpy).toBeCalledWith(UnderTest, null); + expect(useIdContextSpy).toReturnWith(value); expect(root.toJSON()).toMatchInlineSnapshot(`
${value} @@ -117,7 +117,7 @@ describe('Context', () => { const innerValue = 2; const outerId = 'a'; const innerId = 'b'; - const UnderTest = createContext(1); + const UnderTest = createIdContext(1); act(() => { root = create( @@ -139,13 +139,13 @@ describe('Context', () => { `); }); - it('should pass value from outer Provider to useContext hook', () => { + it('should pass value from outer Provider to useIdContext hook', () => { const outerValue = 3; const innerValue = 2; const outerId = 'a'; const innerId = 'b'; - const UnderTest = createContext(1); - const Consumer = withContext(UnderTest); + const UnderTest = createIdContext(1); + const Consumer = withIdContext(UnderTest); act(() => { root = create( @@ -157,8 +157,8 @@ describe('Context', () => { ); }); - expect(useContextSpy).toBeCalledWith(UnderTest, outerId); - expect(useContextSpy).toReturnWith(outerValue); + expect(useIdContextSpy).toBeCalledWith(UnderTest, outerId); + expect(useIdContextSpy).toReturnWith(outerValue); expect(root.toJSON()).toMatchInlineSnapshot(`
${outerValue} diff --git a/src/__tests__/Service.test.tsx b/src/__tests__/Service.test.tsx index 165d5fb..8a194b4 100644 --- a/src/__tests__/Service.test.tsx +++ b/src/__tests__/Service.test.tsx @@ -1,3 +1,4 @@ +import { promisify } from 'util'; import React, { ReactNode, Suspense } from 'react'; import { ReactTestRenderer, act, create } from 'react-test-renderer'; import ErrorBoundary from '../__fixtures__/ErrorBoundary'; @@ -5,12 +6,6 @@ import mockRender from '../__fixtures__/mockRender'; import withService from '../__fixtures__/withService'; import { createService } from '../Service'; -const allPromiseChainsAdoptingSettledPromises = () => ( - new Promise((resolve) => { - setImmediate(resolve); - }) -); - describe('Service', () => { beforeEach(() => { mockRender.mockClear(); @@ -37,24 +32,19 @@ describe('Service', () => { root.unmount(); }); + const whenAllSettledPromisesAreFlushed = promisify(setImmediate); + describe('expected errors', () => { - let noProviderError: Error; - let createContextSpy: jest.SpyInstance; + beforeEach(() => { + mockRender.mockClear(); + }); beforeEach(() => { - const actual = jest.requireActual('../Context'); - const actualCreateContext = actual.createContext; - - noProviderError = new Error(); - createContextSpy = jest.spyOn(actual, 'createContext').mockImplementationOnce( - () => actualCreateContext(() => { - throw noProviderError; - }), - ); + useServiceSpy.mockClear(); }); afterEach(() => { - createContextSpy.mockRestore(); + root.unmount(); }); let mockConsoleErrorToSilenceErrorBoundaries: jest.SpyInstance; @@ -76,9 +66,6 @@ describe('Service', () => { const mockOnError = jest.fn(); const UnderTest = createService(mockHandler); - expect(createContextSpy).toBeCalledTimes(1); - expect(createContextSpy.mock.calls[0][0]).toThrow(); - act(() => { root = create( error} onError={mockOnError}> @@ -91,7 +78,7 @@ describe('Service', () => { expect(mockRender).not.toBeCalled(); expect(mockHandler).not.toBeCalled(); - expect(mockOnError).toBeCalledWith(noProviderError); + expect(mockOnError).toBeCalledWith(expect.any(Error)); expect(root.toJSON()).toMatchInlineSnapshot(`
           error
@@ -105,9 +92,6 @@ describe('Service', () => {
       const UnderTest = createService(mockHandler);
       const Consumer = withService(UnderTest);
 
-      expect(createContextSpy).toBeCalledTimes(1);
-      expect(createContextSpy.mock.calls[0][0]).toThrow();
-
       act(() => {
         root = create(
           error
} onError={mockOnError}> @@ -118,9 +102,9 @@ describe('Service', () => { expect(useServiceSpy).toBeCalledWith(UnderTest, null); expect(useServiceSpy.mock.results[0].type).toBe('throw'); - expect(useServiceSpy.mock.results[0].value).toBe(noProviderError); + expect(useServiceSpy.mock.results[0].value).toBeInstanceOf(Error); expect(mockHandler).not.toBeCalled(); - expect(mockOnError).toBeCalledWith(noProviderError); + expect(mockOnError).toBeCalledWith(expect.any(Error)); expect(root.toJSON()).toMatchInlineSnapshot(`
           error
@@ -137,7 +121,7 @@ describe('Service', () => {
 
       act(() => {
         root = create(
-          
+          
             error
} onError={mockOnError}> suspended

}> @@ -163,7 +147,7 @@ describe('Service', () => { mockOnError.mockClear(); await act(async () => { - await allPromiseChainsAdoptingSettledPromises(); + await whenAllSettledPromisesAreFlushed(); }); expect(mockRender).not.toBeCalled(); @@ -186,7 +170,7 @@ describe('Service', () => { act(() => { root = create( - + error} onError={mockOnError}> suspended

}> @@ -212,7 +196,7 @@ describe('Service', () => { mockOnError.mockClear(); await act(async () => { - await allPromiseChainsAdoptingSettledPromises(); + await whenAllSettledPromisesAreFlushed(); }); expect(useServiceSpy).toBeCalledWith(UnderTest, null); @@ -236,7 +220,7 @@ describe('Service', () => { act(() => { root = create( - + suspended

}> {mockRender} @@ -258,10 +242,10 @@ describe('Service', () => { mockHandler.mockClear(); await act(async () => { - await allPromiseChainsAdoptingSettledPromises(); + await whenAllSettledPromisesAreFlushed(); }); - expect(mockRender).toBeCalledWith(response); + expect(mockRender).toBeCalledWith(response, expect.any(Function)); expect(mockHandler).not.toBeCalled(); expect(root.toJSON()).toMatchInlineSnapshot(` @@ -279,7 +263,7 @@ describe('Service', () => { act(() => { root = create( - + suspended

}>
@@ -301,7 +285,7 @@ describe('Service', () => { mockHandler.mockClear(); await act(async () => { - await allPromiseChainsAdoptingSettledPromises(); + await whenAllSettledPromisesAreFlushed(); }); expect(useServiceSpy).toBeCalledWith(UnderTest, null); @@ -321,15 +305,19 @@ describe('Service', () => { const innerId = 'b'; const outerResponse = 'three'; const innerResponse = 'two'; - const mockHandler = jest.fn() - .mockResolvedValueOnce(innerResponse) - .mockResolvedValueOnce(outerResponse); + const mockHandler = jest.fn( + async (request: number) => ( + request === outerRequest + ? outerResponse + : innerResponse + ), + ); const UnderTest = createService(mockHandler); act(() => { root = create( - - + + suspended

}> {mockRender} @@ -341,8 +329,8 @@ describe('Service', () => { }); expect(mockRender).not.toBeCalled(); - expect(mockHandler).nthCalledWith(1, innerRequest, innerId); - expect(mockHandler).nthCalledWith(2, outerRequest, outerId); + expect(mockHandler).toBeCalledWith(outerRequest, outerId); + expect(mockHandler).toBeCalledWith(innerRequest, innerId); expect(root.toJSON()).toMatchInlineSnapshot(`

suspended @@ -353,10 +341,10 @@ describe('Service', () => { mockHandler.mockClear(); await act(async () => { - await allPromiseChainsAdoptingSettledPromises(); + await whenAllSettledPromisesAreFlushed(); }); - expect(mockRender).toBeCalledWith(outerResponse); + expect(mockRender).toBeCalledWith(outerResponse, expect.any(Function)); expect(mockHandler).not.toBeCalled(); expect(root.toJSON()).toMatchInlineSnapshot(` @@ -372,16 +360,20 @@ describe('Service', () => { const innerId = 'b'; const outerResponse = 'three'; const innerResponse = 'two'; - const mockHandler = jest.fn() - .mockResolvedValueOnce(innerResponse) - .mockResolvedValueOnce(outerResponse); + const mockHandler = jest.fn( + async (request: number) => ( + request === outerRequest + ? outerResponse + : innerResponse + ), + ); const UnderTest = createService(mockHandler); const Consumer = withService(UnderTest); act(() => { root = create( - - + + suspended

}>
@@ -393,8 +385,8 @@ describe('Service', () => { expect(useServiceSpy).toBeCalledWith(UnderTest, outerId); expect(useServiceSpy.mock.results[0].type).toBe('throw'); expect(useServiceSpy.mock.results[0].value).resolves.toBe(outerResponse); - expect(mockHandler).nthCalledWith(1, innerRequest, innerId); - expect(mockHandler).nthCalledWith(2, outerRequest, outerId); + expect(mockHandler).toBeCalledWith(outerRequest, outerId); + expect(mockHandler).toBeCalledWith(innerRequest, innerId); expect(root.toJSON()).toMatchInlineSnapshot(`

suspended @@ -405,7 +397,7 @@ describe('Service', () => { mockHandler.mockClear(); await act(async () => { - await allPromiseChainsAdoptingSettledPromises(); + await whenAllSettledPromisesAreFlushed(); }); expect(useServiceSpy).toBeCalledWith(UnderTest, outerId); @@ -426,7 +418,7 @@ describe('Service', () => { act(() => { root = create( - suspended

}> + suspended

}> {mockRender} @@ -446,10 +438,10 @@ describe('Service', () => { mockHandler.mockClear(); await act(async () => { - await allPromiseChainsAdoptingSettledPromises(); + await whenAllSettledPromisesAreFlushed(); }); - expect(mockRender).toBeCalledWith(response); + expect(mockRender).toBeCalledWith(response, expect.any(Function)); expect(mockHandler).not.toBeCalled(); expect(root.toJSON()).toMatchInlineSnapshot(` @@ -467,7 +459,7 @@ describe('Service', () => { act(() => { root = create( - suspended

}> + suspended

}>
, ); @@ -487,7 +479,7 @@ describe('Service', () => { mockHandler.mockClear(); await act(async () => { - await allPromiseChainsAdoptingSettledPromises(); + await whenAllSettledPromisesAreFlushed(); }); expect(useServiceSpy).toBeCalledWith(UnderTest, null); @@ -502,12 +494,12 @@ describe('Service', () => { it('should not suspend if Provider is not consumed', async () => { const request = 6; - const mockHandler = jest.fn().mockReturnValueOnce(new Promise(() => undefined)); - const UnderTest = createService(mockHandler); + const mockHandler = jest.fn().mockReturnValueOnce(Promise.race([])); + const UnderTest = createService(mockHandler); act(() => { root = create( - +
other
diff --git a/src/index.ts b/src/index.ts index 82d6949..c73ee66 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,8 @@ -export { default as Context, createContext, useContext } from './Context'; -export { default as Service, createService, useService } from './Service'; -export { Reset, useReducer, useState } from './State'; +export { default as Id } from './IdContext/Id'; +export { default as IdContextConsumer, IdContextConsumerProps } from './IdContext/Consumer'; +export { default as IdContextProvider, IdContextProviderProps } from './IdContext/Provider'; +export { default as IdContext, createIdContext, useIdContext } from './IdContext'; +export { default as ServiceConsumer, ServiceConsumerProps } from './Service/Consumer'; +export { default as ServiceProvider, ServiceProviderProps } from './Service/Provider'; +export { default as Service, createService, useService, useServiceState } from './Service'; +export { default as Reset } from './State/Reset';