@layr/react-integration
Version:
React integration for Layr
510 lines • 19 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.useDelay = exports.useForceUpdate = exports.useIsMounted = exports.useAsyncCall = exports.useRecomputableMemo = exports.useAsyncMemo = exports.useAsyncCallback = exports.useObserve = exports.useAction = exports.useData = void 0;
const observable_1 = require("@layr/observable");
const react_1 = require("react");
const core_helpers_1 = require("core-helpers");
const components_1 = require("./components");
/**
* A convenience hook for loading data asynchronously and rendering a React element using the loaded data.
*
* The `getter()` asynchronous function is called when the React component is rendered for the first time and when a change is detected in its `dependencies`.
*
* While the `getter()` function is running, the `useData()` hook returns the result of the nearest `dataPlaceholder()` function, which can be defined in any parent component thanks to the [`Customizer`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) component.
*
* Once the `getter()` function is executed, the `useData()` hook returns the result of the `renderer()` function which is called with the result of the `getter()` function as first parameter.
*
* If an error occurs during the `getter()` function execution, the `useData()` hook returns the result of the nearest `errorRenderer()` function, which can be defined in any parent component thanks to the [`Customizer`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) component.
*
* @param getter An asynchronous function for loading data.
* @param renderer A function which is called with the result of the `getter()` function as first parameter and a `refresh()` function as second parameter. You can call the `refresh()` function to force the re-execution of the `getter()` function. The `renderer()` function should return a React element (or `null`).
* @param [dependencies] An array of values on which the `getter()` function depends (default: `[]`).
* @param [options.dataPlaceholder] A custom `dataPlaceholder()` function.
* @param [options.errorRenderer] A custom `errorRenderer()` function.
*
* @returns A React element (or `null`).
*
* @example
* ```
* import {Component} from '﹫layr/component';
* import React from 'react';
* import {view, useData} from '﹫layr/react-integration';
*
* class Article extends Component {
* // ...
*
* ﹫view() static List() {
* return useData(
* async () => {
* // Return some articles from the backend
* },
*
* (articles) => {
* return articles.map((article) => (
* <div key={article.id}>{article.title}</div>
* ));
* }
* );
* }
* }
* ```
*
* @category High-Level Hooks
* @reacthook
*/
function useData(getter, renderer, deps = [], options = {}) {
const { dataPlaceholder, errorRenderer } = { ...(0, components_1.useCustomization)(), ...options };
const [data, isExecuting, error, refresh] = useAsyncMemo(getter, deps);
if (isExecuting) {
return dataPlaceholder();
}
if (error !== undefined) {
return errorRenderer(error);
}
return renderer(data, refresh);
}
exports.useData = useData;
/**
* A convenience hook for executing some asynchronous actions.
*
* The specified `handler()` asynchronous function is wrapped so that:
*
* - When running, the screen is locked to prevent the user from interacting with any UI element. You can customize the screen locking mechanism in any parent component thanks to the [Customizer's `actionWrapper()`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) prop.
* - In case an error is thrown, an error alert dialog is displayed. You can customize the error alert dialog in any parent component thanks to the [Customizer's `errorNotifier()`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) prop.
*
* @param handler An asynchronous function implementing the action.
* @param [dependencies] An array of values on which the `handler()` function depends (default: `[]`).
* @param [options.actionWrapper] A custom `actionWrapper()` function.
* @param [options.errorNotifier] A custom `errorNotifier()` function.
*
* @returns An asynchronous function wrapping the specified `handler()`.
*
* @example
* ```
* import {Component} from '﹫layr/component';
* import React from 'react';
* import {view, useAction} from '﹫layr/react-integration';
*
* class Article extends Component {
* // ...
*
* ﹫view() EditView() {
* const save = useAction(async () => {
* // Save the edited article to the backend
* });
*
* return (
* <form
* onSubmit={(event) => {
* event.preventDefault();
* save();
* }}
* >
* <div>
* Implement your form fields here.
* </div>
*
* <div>
* <button type="submit">Save</button>
* </div>
* </form>
* );
* }
* }
* ```
*
* @category High-Level Hooks
* @reacthook
*/
function useAction(handler, deps = [], options = {}) {
const { actionWrapper, errorNotifier } = { ...(0, components_1.useCustomization)(), ...options };
const action = (0, react_1.useCallback)(async (...args) => {
try {
return (await actionWrapper(handler, args));
}
catch (error) {
await errorNotifier(error);
throw error;
}
}, deps);
return action;
}
exports.useAction = useAction;
/**
* Makes a view dependent of an [observable](https://layrjs.com/docs/v2/reference/observable#observable-type) so the view is automatically re-rendered when the observable changes.
*
* @param observable An [observable](https://layrjs.com/docs/v2/reference/observable#observable-type) object.
*
* @example
* ```
* import {Component} from '﹫layr/component';
* import {createObservable} from '﹫layr/observable';
* import React from 'react';
* import {view, useObserve} from '﹫layr/react-integration';
*
* const observableArray = createObservable([]);
*
* class MyComponent extends Component {
* ﹫view() static View() {
* useObserve(observableArray);
*
* return (
* <div>
* {`observableArray's length: ${observableArray.length}`}
* </div>
* );
* }
* }
*
* // Changing `observableArray` will re-render `MyComponent.View`
* observableArray.push('abc');
* ```
*
* @category High-Level Hooks
* @reacthook
*/
function useObserve(observable) {
if (!(0, observable_1.isObservable)(observable)) {
throw new Error(`Expected an observable class or instance, but received a value of type '${(0, core_helpers_1.getTypeOf)(observable)}'`);
}
const forceUpdate = useForceUpdate();
(0, react_1.useEffect)(function () {
observable.addObserver(forceUpdate);
return function () {
observable.removeObserver(forceUpdate);
};
}, [observable]);
}
exports.useObserve = useObserve;
/**
* Allows you to define an asynchronous callback and keep track of its execution.
*
* Plays the same role as the React built-in [`useCallback()`](https://reactjs.org/docs/hooks-reference.html#usecallback) hook but works with asynchronous callbacks.
*
* @param asyncCallback An asynchronous callback.
* @param [dependencies] An array of values on which the asynchronous callback depends (default: `[]`).
*
* @returns An array of the shape `[trackedCallback, isExecuting, error, result]` where `trackedCallback` is a function that you can call to execute the asynchronous callback, `isExecuting` is a boolean indicating whether the asynchronous callback is being executed, `error` is the error thrown by the asynchronous callback in case of failed execution, and `result` is the value returned by the asynchronous callback in case of succeeded execution.
*
* @example
* ```
* import {Component} from '﹫layr/component';
* import React from 'react';
* import {view, useAsyncCallback} from '﹫layr/react-integration';
*
* class Article extends Component {
* ﹫view() UpvoteButton() {
* const [handleUpvote, isUpvoting, upvotingError] = useAsyncCallback(async () => {
* await this.upvote();
* });
*
* return (
* <div>
* <button onClick={handleUpvote} disabled={isUpvoting}>Upvote</button>
* {upvotingError && ' An error occurred while upvoting the article.'}
* </div>
* );
* }
* }
* ```
*
* @category Low-Level Hooks
* @reacthook
*/
function useAsyncCallback(asyncCallback, deps = []) {
const [state, setState] = (0, react_1.useState)({});
const isMounted = useIsMounted();
const trackedCallback = (0, react_1.useCallback)(async (...args) => {
setState({ isExecuting: true });
try {
const result = await asyncCallback(...args);
if (isMounted()) {
setState({ result });
}
return result;
}
catch (error) {
if (isMounted()) {
setState({ error });
}
throw error;
}
}, [...deps]);
return [trackedCallback, state.isExecuting === true, state.error, state.result];
}
exports.useAsyncCallback = useAsyncCallback;
/**
* Memoizes the result of an asynchronous function execution and provides a "recompute function" that you can call to recompute the memoized result.
*
* The asynchronous function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the "recompute function" is called.
*
* Plays the same role as the React built-in [`useMemo()`](https://reactjs.org/docs/hooks-reference.html#usememo) hook but works with asynchronous functions and allows to recompute the memoized result.
*
* @param asyncFunc An asynchronous function to compute the memoized result.
* @param [dependencies] An array of values on which the memoized result depends (default: `[]`, which means that the memoized result will be recomputed only when the "recompute function" is called).
*
* @returns An array of the shape `[memoizedResult, isExecuting, error, recompute]` where `memoizedResult` is the result returned by the asynchronous function in case of succeeded execution, `isExecuting` is a boolean indicating whether the asynchronous function is being executed, `error` is the error thrown by the asynchronous function in case of failed execution, and `recompute` is a function that you can call to recompute the memoized result.
*
* @example
* ```
* import {Component} from '﹫layr/component';
* import React from 'react';
* import {view, useAsyncMemo} from '﹫layr/react-integration';
*
* class Article extends Component {
* // ...
*
* ﹫view() static List() {
* const [articles, isLoading, loadingError, retryLoading] = useAsyncMemo(
* async () => {
* // Return some articles from the backend
* }
* );
*
* if (isLoading) {
* return <div>Loading the articles...</div>;
* }
*
* if (loadingError) {
* return (
* <div>
* An error occurred while loading the articles.
* <button onClick={retryLoading}>Retry</button>
* </div>
* );
* }
*
* return articles.map((article) => (
* <div key={article.id}>{article.title}</div>
* ));
* }
* }
* ```
*
* @category Low-Level Hooks
* @reacthook
*/
function useAsyncMemo(asyncFunc, deps = []) {
const [state, setState] = (0, react_1.useState)({ isExecuting: true });
const [recomputeCount, setRecomputeCount] = (0, react_1.useState)(0);
const isMounted = useIsMounted();
(0, react_1.useEffect)(() => {
setState({ isExecuting: true });
asyncFunc().then((result) => {
if (isMounted()) {
setState({ result });
}
return result;
}, (error) => {
if (isMounted()) {
setState({ error });
}
throw error;
});
}, [...deps, recomputeCount]);
const recompute = (0, react_1.useCallback)(() => {
setState({ isExecuting: true });
setRecomputeCount((recomputeCount) => recomputeCount + 1);
}, []);
return [state.result, state.isExecuting === true, state.error, recompute];
}
exports.useAsyncMemo = useAsyncMemo;
/**
* Memoizes the result of a function execution and provides a "recompute function" that you can call to recompute the memoized result.
*
* The function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the "recompute function" is called.
*
* Plays the same role as the React built-in [`useMemo()`](https://reactjs.org/docs/hooks-reference.html#usememo) hook but with the extra ability to recompute the memoized result.
*
* @param func A function to compute the memoized result.
* @param [dependencies] An array of values on which the memoized result depends (default: `[]`, which means that the memoized result will be recomputed only when the "recompute function" is called).
*
* @returns An array of the shape `[memoizedResult, recompute]` where `memoizedResult` is the result of the function execution, and `recompute` is a function that you can call to recompute the memoized result.
*
* @example
* ```
* import {Component, provide} from '﹫layr/component';
* import React, {useCallback} from 'react';
* import {view, useRecomputableMemo} from '﹫layr/react-integration';
*
* class Article extends Component {
* // ...
* }
*
* class Blog extends Component {
* ﹫provide() static Article = Article;
*
* ﹫view() static CreateArticleView() {
* const [article, resetArticle] = useRecomputableMemo(() => new Article());
*
* const createArticle = useCallback(async () => {
* // Save the created article to the backend
* resetArticle();
* }, [article]);
*
* return (
* <div>
* <article.CreateForm onSubmit={createArticle} />
* </div>
* );
* }
* }
* ```
*
* @category Low-Level Hooks
* @reacthook
*/
function useRecomputableMemo(func, deps = []) {
const [recomputeCount, setRecomputeCount] = (0, react_1.useState)(0);
const result = (0, react_1.useMemo)(func, [...deps, recomputeCount]);
const recompute = (0, react_1.useCallback)(() => {
setRecomputeCount((recomputeCount) => recomputeCount + 1);
}, []);
return [result, recompute];
}
exports.useRecomputableMemo = useRecomputableMemo;
/**
* Allows you to call an asynchronous function and keep track of its execution.
*
* The function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the "recall function" is called.
*
* @param asyncFunc The asynchronous function to call.
* @param [dependencies] An array of values on which the asynchronous function depends (default: `[]`, which means that the asynchronous will be recalled only when the "recall function" is called).
*
* @returns An array of the shape `[isExecuting, error, recall]` where `isExecuting` is a boolean indicating whether the asynchronous function is being executed, `error` is the error thrown by the asynchronous function in case of failed execution, and `recall` is a function that you can call to recall the asynchronous function.
*
* @example
* ```
* // JS
*
* import {Component, provide, attribute} from '﹫layr/component';
* import React from 'react';
* import {view, useAsyncCall} from '﹫layr/react-integration';
*
* class Article extends Component {
* // ...
* }
*
* class Blog extends Component {
* ﹫provide() static Article = Article;
*
* ﹫attribute('Article[]?') static loadedArticles;
*
* ﹫view() static View() {
* const [isLoading, loadingError, retryLoading] = useAsyncCall(
* async () => {
* this.loadedArticles = await this.Article.find();
* }
* );
*
* if (isLoading) {
* return <div>Loading the articles...</div>;
* }
*
* if (loadingError) {
* return (
* <div>
* An error occurred while loading the articles.
* <button onClick={retryLoading}>Retry</button>
* </div>
* );
* }
*
* return this.loadedArticles.map((article) => (
* <div key={article.id}>{article.title}</div>
* ));
* }
* }
* ```
*
* @example
* ```
* // TS
*
* import {Component, provide, attribute} from '﹫layr/component';
* import React from 'react';
* import {view, useAsyncCall} from '﹫layr/react-integration';
*
* class Article extends Component {
* // ...
* }
*
* class Blog extends Component {
* ﹫provide() static Article = Article;
*
* ﹫attribute('Article[]?') static loadedArticles?: Article[];
*
* ﹫view() static View() {
* const [isLoading, loadingError, retryLoading] = useAsyncCall(
* async () => {
* this.loadedArticles = await this.Article.find();
* }
* );
*
* if (isLoading) {
* return <div>Loading the articles...</div>;
* }
*
* if (loadingError) {
* return (
* <div>
* An error occurred while loading the articles.
* <button onClick={retryLoading}>Retry</button>
* </div>
* );
* }
*
* return this.loadedArticles!.map((article) => (
* <div key={article.id}>{article.title}</div>
* ));
* }
* }
* ```
*
* @category Low-Level Hooks
* @reacthook
*/
function useAsyncCall(asyncFunc, deps = []) {
const [, isExecuting, error, recall] = useAsyncMemo(asyncFunc, deps);
return [isExecuting, error, recall];
}
exports.useAsyncCall = useAsyncCall;
function useIsMounted() {
const isMountedRef = (0, react_1.useRef)(false);
const isMounted = (0, react_1.useCallback)(() => {
return isMountedRef.current;
}, []);
(0, react_1.useEffect)(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
return isMounted;
}
exports.useIsMounted = useIsMounted;
function useForceUpdate() {
const [, setState] = (0, react_1.useState)({});
const isMounted = useIsMounted();
const forceUpdate = (0, react_1.useCallback)(() => {
if (isMounted()) {
setState({});
}
}, []);
return forceUpdate;
}
exports.useForceUpdate = useForceUpdate;
function useDelay(duration = 100) {
const [isElapsed, setIsElapsed] = (0, react_1.useState)(false);
(0, react_1.useEffect)(() => {
const timeout = setTimeout(() => {
setIsElapsed(true);
}, duration);
return () => {
clearTimeout(timeout);
};
}, []);
return [isElapsed];
}
exports.useDelay = useDelay;
//# sourceMappingURL=hooks.js.map