UNPKG

use-async-effekt-hooks

Version:

React hooks for async effects and memoization with proper dependency tracking and linting support

770 lines (769 loc) 38 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; import React from "react"; import { renderHook, act, waitFor } from "./test-utils"; import { useAsyncMemo } from "../useAsyncMemo"; describe("useAsyncMemo", function () { beforeEach(function () { jest.clearAllMocks(); jest.clearAllTimers(); jest.useRealTimers(); }); afterEach(function () { jest.useRealTimers(); }); it("should return undefined initially and then the computed value", function () { return __awaiter(void 0, void 0, void 0, function () { var computeFn, result; return __generator(this, function (_a) { switch (_a.label) { case 0: computeFn = jest.fn().mockResolvedValue("test-value"); result = renderHook(function () { return useAsyncMemo(computeFn, []); }).result; expect(result.current).toBeUndefined(); expect(computeFn).toHaveBeenCalledTimes(1); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("test-value"); })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }); it("should recompute when dependencies change", function () { return __awaiter(void 0, void 0, void 0, function () { var computeFn, _a, result, rerender; return __generator(this, function (_b) { switch (_b.label) { case 0: computeFn = jest .fn() .mockResolvedValueOnce("value-1") .mockResolvedValueOnce("value-2"); _a = renderHook(function (_a) { var dep = _a.dep; return useAsyncMemo(computeFn, [dep]); }, { initialProps: { dep: "dep1" } }), result = _a.result, rerender = _a.rerender; expect(result.current).toBeUndefined(); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("value-1"); })]; case 1: _b.sent(); act(function () { rerender({ dep: "dep2" }); }); // useAsyncMemo keeps the previous value during recomputation expect(result.current).toBe("value-1"); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("value-2"); })]; case 2: _b.sent(); expect(computeFn).toHaveBeenCalledTimes(2); return [2 /*return*/]; } }); }); }); it("should not recompute when dependencies are the same", function () { return __awaiter(void 0, void 0, void 0, function () { var computeFn, _a, result, rerender; return __generator(this, function (_b) { switch (_b.label) { case 0: computeFn = jest.fn().mockResolvedValue("test-value"); _a = renderHook(function (_a) { var dep = _a.dep; return useAsyncMemo(computeFn, [dep]); }, { initialProps: { dep: "same-dep" } }), result = _a.result, rerender = _a.rerender; return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("test-value"); })]; case 1: _b.sent(); act(function () { rerender({ dep: "same-dep" }); }); expect(computeFn).toHaveBeenCalledTimes(1); expect(result.current).toBe("test-value"); return [2 /*return*/]; } }); }); }); it("should handle errors and keep the last successful value", function () { return __awaiter(void 0, void 0, void 0, function () { var mockConsoleError, computeFn, _a, result, rerender; return __generator(this, function (_b) { switch (_b.label) { case 0: mockConsoleError = jest .spyOn(console, "error") .mockImplementation(function () { }); computeFn = jest .fn() .mockResolvedValueOnce("success-value") .mockRejectedValueOnce(new Error("computation error")); _a = renderHook(function (_a) { var dep = _a.dep; return useAsyncMemo(computeFn, [dep]); }, { initialProps: { dep: "dep1" } }), result = _a.result, rerender = _a.rerender; return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("success-value"); })]; case 1: _b.sent(); act(function () { rerender({ dep: "dep2" }); }); // Should keep the last successful value on error return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("success-value"); })]; case 2: // Should keep the last successful value on error _b.sent(); expect(computeFn).toHaveBeenCalledTimes(2); mockConsoleError.mockRestore(); return [2 /*return*/]; } }); }); }); it("should handle initial error by returning undefined", function () { return __awaiter(void 0, void 0, void 0, function () { var mockConsoleError, computeFn, result; return __generator(this, function (_a) { switch (_a.label) { case 0: mockConsoleError = jest .spyOn(console, "error") .mockImplementation(function () { }); computeFn = jest.fn().mockRejectedValue(new Error("initial error")); result = renderHook(function () { return useAsyncMemo(computeFn, []); }).result; expect(result.current).toBeUndefined(); // Wait a bit to ensure the error is handled return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 10); })]; case 1: // Wait a bit to ensure the error is handled _a.sent(); expect(result.current).toBeUndefined(); expect(computeFn).toHaveBeenCalledTimes(1); mockConsoleError.mockRestore(); return [2 /*return*/]; } }); }); }); it("should cancel previous computation when dependencies change", function () { return __awaiter(void 0, void 0, void 0, function () { var resolveFirst, resolveSecond, firstPromise, secondPromise, computeFn, _a, result, rerender; return __generator(this, function (_b) { switch (_b.label) { case 0: firstPromise = new Promise(function (resolve) { resolveFirst = resolve; }); secondPromise = new Promise(function (resolve) { resolveSecond = resolve; }); computeFn = jest .fn() .mockReturnValueOnce(firstPromise) .mockReturnValueOnce(secondPromise); _a = renderHook(function (_a) { var dep = _a.dep; return useAsyncMemo(computeFn, [dep]); }, { initialProps: { dep: "dep1" } }), result = _a.result, rerender = _a.rerender; expect(result.current).toBeUndefined(); // Change dependencies before first computation completes act(function () { rerender({ dep: "dep2" }); }); // Resolve both promises return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: resolveFirst("first-value"); resolveSecond("second-value"); return [4 /*yield*/, Promise.all([firstPromise, secondPromise])]; case 1: _a.sent(); return [2 /*return*/]; } }); }); })]; case 1: // Resolve both promises _b.sent(); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("second-value"); })]; case 2: _b.sent(); expect(computeFn).toHaveBeenCalledTimes(2); return [2 /*return*/]; } }); }); }); it("should handle rapid dependency changes", function () { return __awaiter(void 0, void 0, void 0, function () { var computeFn, _a, result, rerender; return __generator(this, function (_b) { switch (_b.label) { case 0: computeFn = jest .fn() .mockResolvedValueOnce("value-1") .mockResolvedValueOnce("value-2") .mockResolvedValueOnce("value-3"); _a = renderHook(function (_a) { var dep = _a.dep; return useAsyncMemo(computeFn, [dep]); }, { initialProps: { dep: "dep1" } }), result = _a.result, rerender = _a.rerender; // Wait for first value return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("value-1"); })]; case 1: // Wait for first value _b.sent(); // Rapidly change dependencies act(function () { rerender({ dep: "dep2" }); rerender({ dep: "dep3" }); }); // Wait for the final computation to complete // Due to cancellation, we might get value-2 or value-3 return [4 /*yield*/, waitFor(function () { expect(["value-2", "value-3"]).toContain(result.current); })]; case 2: // Wait for the final computation to complete // Due to cancellation, we might get value-2 or value-3 _b.sent(); // Due to rapid changes and cancellation, we might get fewer calls than expected expect(computeFn).toHaveBeenCalledTimes(2); return [2 /*return*/]; } }); }); }); it("should handle null, zero, and empty string values correctly", function () { return __awaiter(void 0, void 0, void 0, function () { var computeFn, _a, result, rerender; return __generator(this, function (_b) { switch (_b.label) { case 0: computeFn = jest .fn() .mockResolvedValueOnce(null) .mockResolvedValueOnce(0) .mockResolvedValueOnce(""); _a = renderHook(function (_a) { var dep = _a.dep; return useAsyncMemo(computeFn, [dep]); }, { initialProps: { dep: "null" } }), result = _a.result, rerender = _a.rerender; return [4 /*yield*/, waitFor(function () { expect(result.current).toBeNull(); })]; case 1: _b.sent(); act(function () { rerender({ dep: "zero" }); }); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe(0); })]; case 2: _b.sent(); act(function () { rerender({ dep: "empty" }); }); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe(""); })]; case 3: _b.sent(); expect(computeFn).toHaveBeenCalledTimes(3); return [2 /*return*/]; } }); }); }); it("should cleanup on unmount", function () { return __awaiter(void 0, void 0, void 0, function () { var resolveComputation, computationPromise, computeFn, _a, result, unmount; return __generator(this, function (_b) { switch (_b.label) { case 0: computationPromise = new Promise(function (resolve) { resolveComputation = resolve; }); computeFn = jest.fn().mockReturnValue(computationPromise); _a = renderHook(function () { return useAsyncMemo(computeFn, []); }), result = _a.result, unmount = _a.unmount; expect(result.current).toBeUndefined(); unmount(); // Resolve after unmount - should not cause any issues return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: resolveComputation("test-value"); return [4 /*yield*/, computationPromise]; case 1: _a.sent(); return [2 /*return*/]; } }); }); })]; case 1: // Resolve after unmount - should not cause any issues _b.sent(); expect(computeFn).toHaveBeenCalledTimes(1); return [2 /*return*/]; } }); }); }); it("should work with complex dependencies", function () { return __awaiter(void 0, void 0, void 0, function () { var computeFn, obj1, obj2, _a, result, rerender; return __generator(this, function (_b) { switch (_b.label) { case 0: computeFn = jest .fn() .mockResolvedValueOnce("result-1") .mockResolvedValueOnce("result-2"); obj1 = { id: 1, name: "test" }; obj2 = { id: 2, name: "test2" }; _a = renderHook(function (_a) { var obj = _a.obj, num = _a.num; return useAsyncMemo(computeFn, [obj.id, obj.name, num]); }, { initialProps: { obj: obj1, num: 42 } }), result = _a.result, rerender = _a.rerender; return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("result-1"); })]; case 1: _b.sent(); act(function () { rerender({ obj: obj2, num: 42 }); }); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("result-2"); })]; case 2: _b.sent(); expect(computeFn).toHaveBeenCalledTimes(2); return [2 /*return*/]; } }); }); }); it("should handle StrictMode double invocation", function () { return __awaiter(void 0, void 0, void 0, function () { var computeFn, result; return __generator(this, function (_a) { switch (_a.label) { case 0: computeFn = jest.fn().mockResolvedValue("test-value"); result = renderHook(function () { return useAsyncMemo(computeFn, []); }, { wrapper: function (_a) { var children = _a.children; return React.createElement(React.StrictMode, null, children); }, }).result; return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("test-value"); })]; case 1: _a.sent(); // In StrictMode, effects may run twice, but the final result should be correct expect(result.current).toBe("test-value"); return [2 /*return*/]; } }); }); }); it("should handle synchronous computation functions", function () { return __awaiter(void 0, void 0, void 0, function () { var computeFn, result; return __generator(this, function (_a) { switch (_a.label) { case 0: computeFn = jest.fn().mockReturnValue("sync-value"); result = renderHook(function () { return useAsyncMemo(computeFn, []); }).result; expect(result.current).toBeUndefined(); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("sync-value"); })]; case 1: _a.sent(); expect(computeFn).toHaveBeenCalledTimes(1); return [2 /*return*/]; } }); }); }); it("should handle delayed async operations with proper timing", function () { return __awaiter(void 0, void 0, void 0, function () { var computeFn, result; return __generator(this, function (_a) { switch (_a.label) { case 0: jest.useFakeTimers(); computeFn = jest .fn() .mockImplementation(function () { return new Promise(function (resolve) { return setTimeout(function () { return resolve("test-value"); }, 1000); }); }); result = renderHook(function () { return useAsyncMemo(computeFn, []); }).result; expect(result.current).toBeUndefined(); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: jest.advanceTimersByTime(1000); return [4 /*yield*/, Promise.resolve()]; case 1: _a.sent(); // Allow promises to resolve return [2 /*return*/]; } }); }); })]; case 1: _a.sent(); expect(result.current).toBe("test-value"); jest.useRealTimers(); return [2 /*return*/]; } }); }); }); it("should handle synchronous factory function", function () { return __awaiter(void 0, void 0, void 0, function () { var syncFactory, result; return __generator(this, function (_a) { switch (_a.label) { case 0: syncFactory = jest.fn().mockReturnValue("sync-value"); result = renderHook(function () { return useAsyncMemo(syncFactory, []); }).result; expect(result.current).toBeUndefined(); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("sync-value"); })]; case 1: _a.sent(); expect(syncFactory).toHaveBeenCalledTimes(1); expect(syncFactory).toHaveBeenCalledWith(expect.any(Function)); return [2 /*return*/]; } }); }); }); it("should handle factory function that checks isMounted", function () { return __awaiter(void 0, void 0, void 0, function () { var isMountedFn, factory, _a, result, unmount; return __generator(this, function (_b) { switch (_b.label) { case 0: factory = jest.fn().mockImplementation(function (isMounted) { isMountedFn = isMounted; return Promise.resolve("test-value"); }); _a = renderHook(function () { return useAsyncMemo(factory, []); }), result = _a.result, unmount = _a.unmount; return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("test-value"); })]; case 1: _b.sent(); expect(isMountedFn).toBeDefined(); expect(isMountedFn()).toBe(true); unmount(); expect(isMountedFn()).toBe(false); return [2 /*return*/]; } }); }); }); it("should not update state if component unmounts before async operation completes", function () { return __awaiter(void 0, void 0, void 0, function () { var resolveFactory, factory, _a, result, unmount; return __generator(this, function (_b) { factory = jest.fn().mockImplementation(function () { return new Promise(function (resolve) { resolveFactory = resolve; }); }); _a = renderHook(function () { return useAsyncMemo(factory, []); }), result = _a.result, unmount = _a.unmount; expect(result.current).toBeUndefined(); expect(factory).toHaveBeenCalledTimes(1); // Unmount before resolving unmount(); // Now resolve the promise act(function () { resolveFactory("late-value"); }); // Should still be undefined since component was unmounted expect(result.current).toBeUndefined(); return [2 /*return*/]; }); }); }); it("should handle factory function returning null", function () { return __awaiter(void 0, void 0, void 0, function () { var factory, result; return __generator(this, function (_a) { switch (_a.label) { case 0: factory = jest.fn().mockResolvedValue(null); result = renderHook(function () { return useAsyncMemo(factory, []); }).result; return [4 /*yield*/, waitFor(function () { expect(result.current).toBeNull(); })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }); it("should handle factory function returning undefined", function () { return __awaiter(void 0, void 0, void 0, function () { var factory, result; return __generator(this, function (_a) { switch (_a.label) { case 0: factory = jest.fn().mockResolvedValue(undefined); result = renderHook(function () { return useAsyncMemo(factory, []); }).result; // Should remain undefined, but factory should have been called expect(result.current).toBeUndefined(); expect(factory).toHaveBeenCalledTimes(1); return [4 /*yield*/, waitFor(function () { expect(result.current).toBeUndefined(); })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }); it("should handle factory function returning 0", function () { return __awaiter(void 0, void 0, void 0, function () { var factory, result; return __generator(this, function (_a) { switch (_a.label) { case 0: factory = jest.fn().mockResolvedValue(0); result = renderHook(function () { return useAsyncMemo(factory, []); }).result; return [4 /*yield*/, waitFor(function () { expect(result.current).toBe(0); })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }); it("should handle factory function returning false", function () { return __awaiter(void 0, void 0, void 0, function () { var factory, result; return __generator(this, function (_a) { switch (_a.label) { case 0: factory = jest.fn().mockResolvedValue(false); result = renderHook(function () { return useAsyncMemo(factory, []); }).result; return [4 /*yield*/, waitFor(function () { expect(result.current).toBe(false); })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }); it("should handle factory function returning empty string", function () { return __awaiter(void 0, void 0, void 0, function () { var factory, result; return __generator(this, function (_a) { switch (_a.label) { case 0: factory = jest.fn().mockResolvedValue(""); result = renderHook(function () { return useAsyncMemo(factory, []); }).result; return [4 /*yield*/, waitFor(function () { expect(result.current).toBe(""); })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }); it("should preserve last successful value through multiple errors", function () { return __awaiter(void 0, void 0, void 0, function () { var mockConsoleError, factory, _a, result, rerender; return __generator(this, function (_b) { switch (_b.label) { case 0: mockConsoleError = jest .spyOn(console, "error") .mockImplementation(function () { }); factory = jest .fn() .mockResolvedValueOnce("success-1") .mockRejectedValueOnce(new Error("error-1")) .mockRejectedValueOnce(new Error("error-2")) .mockResolvedValueOnce("success-2"); _a = renderHook(function (_a) { var dep = _a.dep; return useAsyncMemo(factory, [dep]); }, { initialProps: { dep: 1 } }), result = _a.result, rerender = _a.rerender; // First success return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("success-1"); })]; case 1: // First success _b.sent(); // First error - should keep last successful value act(function () { rerender({ dep: 2 }); }); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("success-1"); })]; case 2: _b.sent(); // Second error - should still keep last successful value act(function () { rerender({ dep: 3 }); }); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("success-1"); })]; case 3: _b.sent(); // Second success - should update to new value act(function () { rerender({ dep: 4 }); }); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("success-2"); })]; case 4: _b.sent(); expect(mockConsoleError).toHaveBeenCalledTimes(2); mockConsoleError.mockRestore(); return [2 /*return*/]; } }); }); }); it("should handle complex object values", function () { return __awaiter(void 0, void 0, void 0, function () { var complexValue, factory, result; return __generator(this, function (_a) { switch (_a.label) { case 0: complexValue = { id: 1, name: "test", nested: { value: "nested-test" }, array: [1, 2, 3], }; factory = jest.fn().mockResolvedValue(complexValue); result = renderHook(function () { return useAsyncMemo(factory, []); }).result; return [4 /*yield*/, waitFor(function () { expect(result.current).toEqual(complexValue); expect(result.current).toBe(complexValue); // Same reference })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }); it("should handle factory that uses isMounted to prevent state updates", function () { return __awaiter(void 0, void 0, void 0, function () { var shouldComplete, factory, _a, result, rerender; return __generator(this, function (_b) { switch (_b.label) { case 0: shouldComplete = false; factory = jest.fn().mockImplementation(function (isMounted) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: // Simulate some async work return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 10); })]; case 1: // Simulate some async work _a.sent(); if (!isMounted()) { return [2 /*return*/, "should-not-be-used"]; } return [2 /*return*/, shouldComplete ? "completed" : "initial"]; } }); }); }); _a = renderHook(function (_a) { var trigger = _a.trigger; return useAsyncMemo(factory, [trigger]); }, { initialProps: { trigger: false } }), result = _a.result, rerender = _a.rerender; return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("initial"); })]; case 1: _b.sent(); shouldComplete = true; act(function () { rerender({ trigger: true }); }); return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("completed"); })]; case 2: _b.sent(); expect(factory).toHaveBeenCalledTimes(2); return [2 /*return*/]; } }); }); }); it("should handle very rapid dependency changes", function () { return __awaiter(void 0, void 0, void 0, function () { var factory, _a, result, rerender, _loop_1, i; return __generator(this, function (_b) { switch (_b.label) { case 0: factory = jest .fn() .mockResolvedValueOnce("value-1") .mockResolvedValueOnce("value-2") .mockResolvedValueOnce("value-3") .mockResolvedValueOnce("value-4") .mockResolvedValueOnce("value-5"); _a = renderHook(function (_a) { var dep = _a.dep; return useAsyncMemo(factory, [dep]); }, { initialProps: { dep: 1 } }), result = _a.result, rerender = _a.rerender; _loop_1 = function (i) { act(function () { rerender({ dep: i }); }); }; // Rapid changes for (i = 2; i <= 5; i++) { _loop_1(i); } // Should eventually settle on the last value return [4 /*yield*/, waitFor(function () { expect(result.current).toBe("value-5"); })]; case 1: // Should eventually settle on the last value _b.sent(); expect(factory).toHaveBeenCalledTimes(5); return [2 /*return*/]; } }); }); }); });