diff --git a/packages/devtools-extension/package.json b/packages/devtools-extension/package.json
index 5597f1c0..583fe363 100644
--- a/packages/devtools-extension/package.json
+++ b/packages/devtools-extension/package.json
@@ -6,7 +6,7 @@
"cra-template": "1.1.2",
"lodash": "^4.17.21",
"react": "18.1.0",
- "react-async-states": "1.0.0-rc-1.1",
+ "react-async-states": "1.0.0-rc-2",
"react-dom": "18.1.0",
"react-json-view": "^1.21.3",
"react-scripts": "4.0.3"
diff --git a/packages/example/package.json b/packages/example/package.json
index e9d3237b..66992386 100644
--- a/packages/example/package.json
+++ b/packages/example/package.json
@@ -9,7 +9,7 @@
"lodash": "^4.17.21",
"prop-types": "^15.6.2",
"react": "18.1.0",
- "react-async-states": "1.0.0-rc-1.1",
+ "react-async-states": "1.0.0-rc-2",
"react-dom": "18.1.0",
"react-router-dom": "6.4.0-pre.2",
"react-scripts": "4.0.3"
diff --git a/packages/example/src/App2.js b/packages/example/src/App2.js
new file mode 100644
index 00000000..fcb07fce
--- /dev/null
+++ b/packages/example/src/App2.js
@@ -0,0 +1,67 @@
+import React from "react";
+import {
+ RenderStrategy,
+ StateBoundary,
+ useCurrentState,
+ AsyncStateStatus
+} from "react-async-states";
+
+const config = {
+ lazy: false,
+ producer: async function () {
+ const response = await fetch('https://jsonplaceholder.typicode.com/users/12');
+ if (!response.ok) {
+ throw new Error(response.status);
+ }
+ return response.json();
+ }
+}
+
+function Wrapper({children}) {
+ const [t, e] = React.useState(false);
+
+ return (
+ <>
+
+ {t && children}
+ >
+ )
+}
+
+function MyError() {
+ const {state: {data: error}} = useCurrentState();
+
+ return
This error is happening: {error?.toString?.()}
+}
+
+function MyPending() {
+ const {state: {props}} = useCurrentState();
+
+ return PENDING WITH PROPS: {JSON.stringify(props, null, 4)}
+}
+
+export default function App2() {
+ return (
+
+ Result!
+ ,
+ [AsyncStateStatus.success]: ,
+ }}
+ />
+
+ );
+}
+
+function CurrentState() {
+ const currentState = useCurrentState();
+ return
+ Current state details {currentState.state.status}
+
+ {JSON.stringify(currentState, null, 4)}
+
+
+}
diff --git a/packages/example/src/index.js b/packages/example/src/index.js
index 42c21bea..4063510e 100644
--- a/packages/example/src/index.js
+++ b/packages/example/src/index.js
@@ -2,7 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
-import App from "./past/App";
+import App from "./App2";
// import App2 from './past/App2';
diff --git a/packages/react-async-states/package.json b/packages/react-async-states/package.json
index be6c18cf..79df61dd 100644
--- a/packages/react-async-states/package.json
+++ b/packages/react-async-states/package.json
@@ -5,7 +5,7 @@
"types": "dist/react-async-states/src/index",
"author": "incepter",
"sideEffects": false,
- "version": "1.0.0-rc-1.1",
+ "version": "1.0.0-rc-2",
"name": "react-async-states",
"repository": "incepter/react-async-states",
"description": "A hooks-based lightweight React library for state management",
diff --git a/packages/react-async-states/src/__tests__/react-async-state/useAsyncState/subscription/post-subscribe/index.test.tsx b/packages/react-async-states/src/__tests__/react-async-state/useAsyncState/subscription/post-subscribe/index.test.tsx
index c93abb55..8e55e446 100644
--- a/packages/react-async-states/src/__tests__/react-async-state/useAsyncState/subscription/post-subscribe/index.test.tsx
+++ b/packages/react-async-states/src/__tests__/react-async-state/useAsyncState/subscription/post-subscribe/index.test.tsx
@@ -6,12 +6,11 @@ import {UseAsyncState} from "../../../../../types.internal";
import {AsyncStateStatus} from "../../../../../async-state";
import {mockDateNow, TESTS_TS} from "../../../utils/setup";
-jest.useFakeTimers();
mockDateNow();
-
describe('should post subscribe', () => {
it('should invoke post subscribe when present and run producer' +
' and run post unsubscribe', async () => {
+ jest.useFakeTimers();
// given
const onAbort = jest.fn();
const producer = jest.fn().mockImplementation(props => {
diff --git a/packages/react-async-states/src/async-state/types.ts b/packages/react-async-states/src/async-state/types.ts
index 16304dff..988694ba 100644
--- a/packages/react-async-states/src/async-state/types.ts
+++ b/packages/react-async-states/src/async-state/types.ts
@@ -71,8 +71,9 @@ export enum ProducerType {
}
export enum RenderStrategy {
- FetchOnRender = 0,
+ FetchAsYouRender = 0,
FetchThenRender = 1,
+ RenderThenFetch = 2,
}
export type ProducerConfig = {
diff --git a/packages/react-async-states/src/components/AsyncStateComponent.tsx b/packages/react-async-states/src/components/AsyncStateComponent.tsx
deleted file mode 100644
index 9deb0379..00000000
--- a/packages/react-async-states/src/components/AsyncStateComponent.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import * as React from "react";
-import {
- AsyncStateSubscriptionMode,
- UseAsyncState,
- UseAsyncStateConfig
-} from "../types.internal";
-import {useAsyncState} from "../hooks/useAsyncState";
-import {
- AsyncStateSource,
- AsyncStateStatus,
- RenderStrategy
-} from "../async-state";
-import {readAsyncStateFromSource} from "../async-state/read-source";
-
-const defaultDeps = [];
-
-
-interface ComponentSelf {
- didRenderChildren: boolean,
-}
-
-export function AsyncStateComponent({
- config,
- error = null,
- suspend = false,
- children = null,
- fallback = null,
- dependencies = defaultDeps,
- strategy = RenderStrategy.FetchOnRender,
-}: {
- suspend?: boolean,
- dependencies?: any[],
- strategy?: RenderStrategy,
- config: UseAsyncStateConfig,
- error?: React.ReactNode | ((props) => React.ReactNode),
- children?: ((props: UseAsyncState) => React.ReactNode) | null,
- fallback?: React.ReactNode | ((props: { state: E, abort: ((reason?: any) => void) }) => React.ReactNode),
-}): any {
- const props = useAsyncState(config, dependencies);
-
- if (
- strategy === RenderStrategy.FetchThenRender &&
- props.mode !== AsyncStateSubscriptionMode.NOOP &&
- props.mode !== AsyncStateSubscriptionMode.WAITING
- ) {
-
- const asyncState = readAsyncStateFromSource(
- props.source as AsyncStateSource
- );
-
- if (asyncState.currentState.status === AsyncStateStatus.pending) {
- if (suspend) {
- props.read(); // will throw
- }
- // the fallback will only see the pending status,
- // so it will receive only the state
- return render(fallback, {state: props.state, abort: props.abort});
- }
-
- if (asyncState.currentState.status === AsyncStateStatus.error) {
- return render(error, props);
- }
-
- if (asyncState.currentState.status !== AsyncStateStatus.success) {
- return render(fallback, props);
- }
-
- return render(children, props);
- }
- return render(children, props);
-}
-
-function render(create, props) {
- return typeof create === "function" ? React.createElement(create, props) : create;
-}
diff --git a/packages/react-async-states/src/components/StateBoundary.tsx b/packages/react-async-states/src/components/StateBoundary.tsx
new file mode 100644
index 00000000..b4445e02
--- /dev/null
+++ b/packages/react-async-states/src/components/StateBoundary.tsx
@@ -0,0 +1,126 @@
+import * as React from "react";
+import {
+ AsyncStateSource,
+ AsyncStateStatus,
+ RenderStrategy,
+ State
+} from "../async-state";
+import {
+ AsyncStateSubscriptionMode,
+ StateBoundaryProps,
+ UseAsyncState,
+} from "../types.internal";
+import {useAsyncState} from "../hooks/useAsyncState";
+import {readAsyncStateFromSource} from "../async-state/read-source";
+
+const StateBoundaryContext = React.createContext(null);
+
+export function StateBoundary(props: StateBoundaryProps) {
+ return (
+
+ {props.children}
+
+ )
+}
+
+function StateBoundaryImpl(props: StateBoundaryProps) {
+ if (props.strategy === RenderStrategy.FetchThenRender) {
+ return React.createElement(FetchThenRenderBoundary, props);
+ }
+ if (props.strategy === RenderStrategy.FetchAsYouRender) {
+ return React.createElement(FetchAsYouRenderBoundary, props);
+ }
+ return React.createElement(RenderThenFetchBoundary, props);
+}
+
+function inferBoundaryChildren>(
+ result: UseAsyncState,
+ props: StateBoundaryProps
+) {
+ if (!props.render || !result.source) {
+ return props.children;
+ }
+
+ const asyncState = readAsyncStateFromSource(result.source);
+ const {status} = asyncState.currentState;
+
+ return props.render[status] ? props.render[status] : props.children;
+}
+
+export function RenderThenFetchBoundary(props: StateBoundaryProps) {
+ const result = useAsyncState(props.config, props.dependencies);
+
+ const children = inferBoundaryChildren(result, props);
+ return (
+
+ {children}
+
+ );
+}
+
+export function FetchAsYouRenderBoundary(props: StateBoundaryProps) {
+ const result = useAsyncState.auto(props.config, props.dependencies);
+ result.read(); // throws
+ const children = inferBoundaryChildren(result, props);
+ return (
+
+ {children}
+
+ );
+}
+
+type FetchThenRenderSelf = {
+ didLoad: boolean,
+}
+
+export function FetchThenRenderBoundary(props: StateBoundaryProps) {
+ const result = useAsyncState.auto(props.config, props.dependencies);
+ const self = React.useMemo(constructSelf, []);
+
+ if (result.mode === AsyncStateSubscriptionMode.NOOP ||
+ result.mode === AsyncStateSubscriptionMode.WAITING) {
+ throw new Error("FetchThenRenderBoundary is not supported with NOOP and WAITING modes");
+ }
+
+ if (!self.didLoad) {
+ const {source} = result;
+ const asyncState = readAsyncStateFromSource(source as AsyncStateSource);
+
+ const {status} = asyncState.currentState;
+
+ if (status === AsyncStateStatus.error || status === AsyncStateStatus.success) {
+ self.didLoad = true;
+ const children = inferBoundaryChildren(result, props);
+ return (
+
+ {children}
+
+ );
+ }
+
+ return null;
+ } else {
+ const children = inferBoundaryChildren(result, props);
+ return (
+
+ {children}
+
+ );
+ }
+
+ function constructSelf() {
+ return {
+ didLoad: false,
+ };
+ }
+}
+
+export function useCurrentState>(): UseAsyncState {
+ const ctxValue = React.useContext(StateBoundaryContext);
+
+ if (ctxValue === null) {
+ throw new Error('You cannot use useCurrentState outside a StateBoundary');
+ }
+
+ return ctxValue;
+}
diff --git a/packages/react-async-states/src/index.ts b/packages/react-async-states/src/index.ts
index 973e3091..e5a5af67 100644
--- a/packages/react-async-states/src/index.ts
+++ b/packages/react-async-states/src/index.ts
@@ -12,7 +12,13 @@ export {
invalidateCache,
useRunAsyncState,
} from "./hooks/useRun";
-export {AsyncStateComponent} from "./components/AsyncStateComponent";
+export {
+ StateBoundary,
+ useCurrentState,
+ FetchThenRenderBoundary,
+ RenderThenFetchBoundary,
+ FetchAsYouRenderBoundary,
+} from "./components/StateBoundary";
export {useSelector, useAsyncStateSelector} from "./hooks/useSelector";
export * from "./types";
diff --git a/packages/react-async-states/src/types.internal.ts b/packages/react-async-states/src/types.internal.ts
index 3d2ef5ea..428c7e2c 100644
--- a/packages/react-async-states/src/types.internal.ts
+++ b/packages/react-async-states/src/types.internal.ts
@@ -1,3 +1,4 @@
+import * as React from "react";
import {
AbortFn,
AsyncStateInterface,
@@ -9,11 +10,12 @@ import {
Producer,
ProducerConfig,
ProducerProps,
- ProducerRunEffects,
+ ProducerRunEffects, RenderStrategy,
RunExtraProps,
State,
StateUpdater
} from "./async-state";
+import {ReactNode} from "react";
export type Reducer = (
T,
@@ -261,6 +263,18 @@ export type UseAsyncStateConfiguration> = {
lane?: string,
}
+export type StateBoundaryProps = {
+ children: React.ReactNode,
+ config: UseAsyncStateConfig,
+
+ dependencies?: any[],
+ strategy?: RenderStrategy,
+
+ render?: StateBoundaryRenderProp,
+}
+
+export type StateBoundaryRenderProp = Record
+
export type UseAsyncStateEventProps = {
state: State,
};
diff --git a/packages/react-async-states/src/types.ts b/packages/react-async-states/src/types.ts
index 7c3a7e51..a3892571 100644
--- a/packages/react-async-states/src/types.ts
+++ b/packages/react-async-states/src/types.ts
@@ -14,6 +14,7 @@ export type {
} from "./async-state/types";
export {
+ RenderStrategy,
ProducerRunEffects,
} from "./async-state/types";
@@ -41,4 +42,6 @@ export type {
UseAsyncStateEventFn,
UseAsyncStateEventProps,
+ StateBoundaryProps,
+
} from "./types.internal";