Skip to content

Commit

Permalink
Added StateContext to library
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickroberts committed Jan 15, 2021
1 parent c3956e4 commit 1d8f869
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 31 deletions.
74 changes: 50 additions & 24 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "suspense-service",
"version": "0.2.4",
"version": "0.2.5",
"description": "Suspense integration library for React",
"repository": "github:patrickroberts/suspense-service",
"main": "dst/cjs/suspense-service.js",
Expand Down Expand Up @@ -28,15 +28,15 @@
"@babel/preset-react": "^7.12.10",
"@rollup/plugin-commonjs": "^15.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@types/jest": "^26.0.19",
"@types/jest": "^26.0.20",
"@types/react": "^16.14.2",
"@types/react-dom": "^16.9.10",
"@types/react-test-renderer": "^16.9.4",
"@typescript-eslint/eslint-plugin": "4.11.1",
"@typescript-eslint/parser": "4.11.1",
"@wessberg/rollup-plugin-ts": "^1.3.8",
"concurrently": "^5.3.0",
"eslint": "^7.16.0",
"eslint": "^7.17.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.1.3",
Expand All @@ -49,11 +49,11 @@
"react-dom": "^16.14.0",
"react-test-renderer": "^16.14.0",
"rimraf": "^3.0.2",
"rollup": "^2.35.1",
"rollup": "^2.36.1",
"rollup-plugin-terser": "^7.0.2",
"ts-jest": "^26.4.4",
"typedoc": "^0.20.0",
"typedoc-plugin-markdown": "^3.2.1",
"typedoc": "^0.20.14",
"typedoc-plugin-markdown": "^3.4.0",
"typedoc-plugin-sourcefile-url": "^1.0.6",
"typescript": "^4.1.3"
},
Expand All @@ -70,6 +70,7 @@
"library",
"react",
"service",
"stateful",
"suspense",
"typescript"
],
Expand Down
2 changes: 1 addition & 1 deletion src/IdContext/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function createIdContext<T>(defaultValue: T): IdContext<T> {
}

/**
* Consumes a value from a {@link IdContextProvider}
* Consumes a value from an {@link IdContextProvider}
* @param context the {@link IdContext} to use
* @param id the {@link IdContextProviderProps.id | IdContextProvider id} to use
*/
Expand Down
16 changes: 16 additions & 0 deletions src/StateContext/Consumer/Props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Dispatch, ReactNode, SetStateAction } from 'react';
import Id from '../../IdContext/Id';

export default interface StateConsumerProps<T> {
/**
* The {@link StateProvider} to use
* @default null
*/
id?: Id;
children: (value: T, setState: Dispatch<SetStateAction<T>>) => ReactNode;
}

/** @ignore */
export const defaultProps = {
id: null,
};
32 changes: 32 additions & 0 deletions src/StateContext/Consumer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { ComponentType, memo, useCallback, useMemo } from 'react';
import IdContext from '../../IdContext';
import State from '../State';
import StateContextConsumerProps, { defaultProps } from './Props';

type StateContextConsumer<T> = ComponentType<StateContextConsumerProps<T>>;

export default StateContextConsumer;
export { StateContextConsumerProps };

/** @ignore */
export function createStateContextConsumer<T>(
{ Consumer }: IdContext<State<T>>,
): StateContextConsumer<T> {
const StateConsumer: StateContextConsumer<T> = ({ id, children }) => {
const render = useCallback(
([state, setState]: State<T>) => children(state, setState),
[children],
);

return useMemo(() => (
<Consumer id={id}>{render}</Consumer>
), [id, render]);
};

StateConsumer.defaultProps = defaultProps;

return memo(StateConsumer, (prev, next) => (
Object.is(prev.id, next.id)
&& Object.is(prev.children, next.children)
));
}
29 changes: 29 additions & 0 deletions src/StateContext/Provider/Props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ReactNode } from 'react';
import Id from '../../IdContext/Id';
import Reset from '../../State/Reset';

export default interface StateContextProviderProps<T> {
/**
* The initial value to provide
*/
value: T;
/**
* The key that identifies the {@link StateContextProvider} to be consumed
* @default null
*/
id?: Id;
/**
* @default null
*/
children?: ReactNode;
/**
* The reset function when {@link StateProviderProps.value | value} updates
*/
reset?: Reset<T>;
}

/** @ignore */
export const defaultProps = {
id: null,
children: null,
};
32 changes: 32 additions & 0 deletions src/StateContext/Provider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { ComponentType, memo, useMemo } from 'react';
import IdContext from '../../IdContext';
import useResetState from '../../State/useResetState';
import State from '../State';
import StateContextProviderProps, { defaultProps } from './Props';

type StateContextProvider<T> = ComponentType<StateContextProviderProps<T>>;

export default StateContextProvider;
export { StateContextProviderProps };

/** @ignore */
export function createStateContextProvider<T>(
StateContext: IdContext<State<T>>,
): StateContextProvider<T> {
const { Provider } = StateContext;
const StateProvider: StateContextProvider<T> = ({ value, id, children, reset }) => {
const state = useResetState(value, reset);

return useMemo(() => (
<Provider value={state} id={id}>{children}</Provider>
), [state, id, children]);
};

StateProvider.defaultProps = defaultProps;

return memo(StateProvider, (prev, next) => (
Object.is(prev.value, next.value)
&& Object.is(prev.id, next.id)
&& Object.is(prev.children, next.children)
));
}
8 changes: 8 additions & 0 deletions src/StateContext/State.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Dispatch, SetStateAction } from 'react';

/**
* A stateful value and a function to update it
*/
type State<T> = [T, Dispatch<SetStateAction<T>>];

export default State;
47 changes: 47 additions & 0 deletions src/StateContext/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Id from '../IdContext/Id';
import State from './State';
import IdContext, { createIdContext, useIdContext } from '../IdContext';
import StateContextConsumer, { createStateContextConsumer } from './Consumer';
import StateContextProvider, { createStateContextProvider } from './Provider';

/**
* A privately scoped unique symbol for accessing {@link StateContext} internal {@link State}
* @internal
*/
const kState = Symbol('kState');

/**
* A State Context with support for multiple keyed values
*/
export default interface StateContext<T> {
Consumer: StateContextConsumer<T>;
Provider: StateContextProvider<T>;
/** @internal */
[kState]: IdContext<State<T>>;
}

/**
* Creates a State Context for providing a stateful value and a function to update it.
* @param defaultValue the value consumed if no {@link StateContextProvider} is in scope and the
* {@link StateContextConsumerProps.id | consumer `id`} is `null`
*/
export function createStateContext<T>(defaultValue: T): StateContext<T> {
const StateContext = createIdContext<State<T>>(
[defaultValue, () => undefined],
);

return {
Consumer: createStateContextConsumer(StateContext),
Provider: createStateContextProvider(StateContext),
[kState]: StateContext,
};
}

/**
* Consumes a stateful value from a {@link StateContextProvider}, and a function to update it
* @param context the {@link StateContext} to use
* @param id the {@link StateContextProviderProps.id | StateContextProvider id} to use
*/
export function useStateContext<T>(context: StateContext<T>, id: Id = null): State<T> {
return useIdContext(context[kState], id);
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ 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, Handler, createService, useService, useServiceState } from './Service';
export { default as State } from './StateContext/State';
export { default as StateContextConsumer, StateContextConsumerProps } from './StateContext/Consumer';
export { default as StateContextProvider, StateContextProviderProps } from './StateContext/Provider';
export { default as StateContext, createStateContext, useStateContext } from './StateContext';
export { default as Reset } from './State/Reset';

0 comments on commit 1d8f869

Please sign in to comment.