UNPKG

use-async-effekt-hooks

Version:

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

704 lines (703 loc) 36.4 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 { renderHook, act } from "./test-utils"; import { useAsyncEffekt } from "../useAsyncEffekt"; // Helper to create a delay var delay = function (ms) { return new Promise(function (resolve) { return setTimeout(resolve, ms); }); }; describe("useAsyncEffekt", function () { beforeEach(function () { jest.clearAllMocks(); jest.clearAllTimers(); jest.useFakeTimers(); }); afterEach(function () { jest.runOnlyPendingTimers(); jest.useRealTimers(); }); it("should execute async effect on mount", function () { return __awaiter(void 0, void 0, void 0, function () { var mockEffect; return __generator(this, function (_a) { switch (_a.label) { case 0: mockEffect = jest.fn().mockResolvedValue(undefined); renderHook(function () { return useAsyncEffekt(mockEffect, []); }); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); expect(mockEffect).toHaveBeenCalledWith({ isMounted: expect.any(Function), waitForPrevious: expect.any(Function), }); return [2 /*return*/]; } }); }); }); it("should provide isMounted function that returns true when mounted", function () { return __awaiter(void 0, void 0, void 0, function () { var isMountedFn, mockEffect; return __generator(this, function (_a) { switch (_a.label) { case 0: mockEffect = jest.fn().mockImplementation(function (_a) { var isMounted = _a.isMounted; isMountedFn = isMounted; return Promise.resolve(); }); renderHook(function () { return useAsyncEffekt(mockEffect, []); }); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(isMountedFn).toBeDefined(); expect(isMountedFn()).toBe(true); return [2 /*return*/]; } }); }); }); it("should provide isMounted function that returns false when unmounted", function () { return __awaiter(void 0, void 0, void 0, function () { var isMountedFn, mockEffect, unmount; return __generator(this, function (_a) { switch (_a.label) { case 0: mockEffect = jest.fn().mockImplementation(function (_a) { var isMounted = _a.isMounted; isMountedFn = isMounted; return Promise.resolve(); }); unmount = renderHook(function () { return useAsyncEffekt(mockEffect, []); }).unmount; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); unmount(); expect(isMountedFn).toBeDefined(); expect(isMountedFn()).toBe(false); return [2 /*return*/]; } }); }); }); it("should re-run effect when dependencies change", function () { return __awaiter(void 0, void 0, void 0, function () { var mockEffect, dep, rerender; return __generator(this, function (_a) { switch (_a.label) { case 0: mockEffect = jest.fn().mockResolvedValue(undefined); dep = 1; rerender = renderHook(function () { return useAsyncEffekt(mockEffect, [dep]); }).rerender; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); dep = 2; rerender(); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 2: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(2); return [2 /*return*/]; } }); }); }); it("should not re-run effect when dependencies stay the same", function () { return __awaiter(void 0, void 0, void 0, function () { var mockEffect, dep, rerender; return __generator(this, function (_a) { switch (_a.label) { case 0: mockEffect = jest.fn().mockResolvedValue(undefined); dep = 1; rerender = renderHook(function () { return useAsyncEffekt(mockEffect, [dep]); }).rerender; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); rerender(); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 2: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); return [2 /*return*/]; } }); }); }); it("should handle synchronous cleanup function", function () { return __awaiter(void 0, void 0, void 0, function () { var mockCleanup, mockEffect, unmount; return __generator(this, function (_a) { switch (_a.label) { case 0: mockCleanup = jest.fn(); mockEffect = jest.fn().mockResolvedValue(mockCleanup); unmount = renderHook(function () { return useAsyncEffekt(mockEffect, []); }).unmount; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); expect(mockCleanup).not.toHaveBeenCalled(); unmount(); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 2: _a.sent(); expect(mockCleanup).toHaveBeenCalledTimes(1); return [2 /*return*/]; } }); }); }); it("should handle asynchronous cleanup function", function () { return __awaiter(void 0, void 0, void 0, function () { var mockCleanup, mockEffect, unmount; return __generator(this, function (_a) { switch (_a.label) { case 0: mockCleanup = jest.fn().mockResolvedValue(undefined); mockEffect = jest.fn().mockResolvedValue(mockCleanup); unmount = renderHook(function () { return useAsyncEffekt(mockEffect, []); }).unmount; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); expect(mockCleanup).not.toHaveBeenCalled(); unmount(); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 2: _a.sent(); expect(mockCleanup).toHaveBeenCalledTimes(1); return [2 /*return*/]; } }); }); }); it("should wait for previous effect when waitForPrevious is called", function () { return __awaiter(void 0, void 0, void 0, function () { var resolveFirstEffect, fistEffectFinishedAndCleaned, firstEffect, secondEffect, dep, rerender; return __generator(this, function (_a) { switch (_a.label) { case 0: fistEffectFinishedAndCleaned = false; firstEffect = jest.fn().mockImplementation(function () { return new Promise(function (resolve) { resolveFirstEffect = resolve; }); }); secondEffect = jest .fn() .mockImplementation(function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { var waitForPrevious = _b.waitForPrevious; return __generator(this, function (_c) { switch (_c.label) { case 0: return [4 /*yield*/, waitForPrevious()]; case 1: _c.sent(); fistEffectFinishedAndCleaned = true; return [2 /*return*/]; } }); }); }); dep = 1; rerender = renderHook(function () { return useAsyncEffekt(dep === 1 ? firstEffect : secondEffect, [dep]); }).rerender; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(firstEffect).toHaveBeenCalledTimes(1); expect(fistEffectFinishedAndCleaned).toBe(false); // Change dependency to trigger second effect dep = 2; rerender(); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 2: _a.sent(); // Second effect should be waiting expect(secondEffect).toHaveBeenCalledTimes(1); expect(fistEffectFinishedAndCleaned).toBe(false); // Resolve first effect resolveFirstEffect(); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 3: _a.sent(); expect(fistEffectFinishedAndCleaned).toBe(true); return [2 /*return*/]; } }); }); }); it("should handle errors in async effects gracefully", function () { return __awaiter(void 0, void 0, void 0, function () { var consoleErrorSpy, error, mockEffect; return __generator(this, function (_a) { switch (_a.label) { case 0: consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); error = new Error("Test error"); mockEffect = jest.fn().mockRejectedValue(error); renderHook(function () { return useAsyncEffekt(mockEffect, []); }); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); expect(consoleErrorSpy).toHaveBeenCalledWith("useAsyncEffekt error:", error); consoleErrorSpy.mockRestore(); return [2 /*return*/]; } }); }); }); it("should handle errors in cleanup functions gracefully", function () { return __awaiter(void 0, void 0, void 0, function () { var consoleErrorSpy, error, mockCleanup, mockEffect, unmount; return __generator(this, function (_a) { switch (_a.label) { case 0: consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); error = new Error("Cleanup error"); mockCleanup = jest.fn().mockRejectedValue(error); mockEffect = jest.fn().mockResolvedValue(mockCleanup); unmount = renderHook(function () { return useAsyncEffekt(mockEffect, []); }).unmount; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); unmount(); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 2: _a.sent(); expect(consoleErrorSpy).toHaveBeenCalledWith("useAsyncEffekt cleanup error:", error); consoleErrorSpy.mockRestore(); return [2 /*return*/]; } }); }); }); it("should cancel effect if component unmounts before effect completes", function () { return __awaiter(void 0, void 0, void 0, function () { var effectCompleted, mockEffect, unmount; return __generator(this, function (_a) { switch (_a.label) { case 0: effectCompleted = false; mockEffect = jest.fn().mockImplementation(function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) { var isMounted = _b.isMounted; return __generator(this, function (_c) { switch (_c.label) { case 0: return [4 /*yield*/, delay(100)]; case 1: _c.sent(); if (isMounted()) { effectCompleted = true; } return [2 /*return*/]; } }); }); }); unmount = renderHook(function () { return useAsyncEffekt(mockEffect, []); }).unmount; // Start the effect return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.advanceTimersByTime(50); return [2 /*return*/]; }); }); })]; case 1: // Start the effect _a.sent(); // Unmount before effect completes unmount(); // Complete the timer return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.advanceTimersByTime(100); return [2 /*return*/]; }); }); })]; case 2: // Complete the timer _a.sent(); expect(effectCompleted).toBe(false); return [2 /*return*/]; } }); }); }); it("should work without dependencies array", function () { return __awaiter(void 0, void 0, void 0, function () { var mockEffect; return __generator(this, function (_a) { switch (_a.label) { case 0: mockEffect = jest.fn().mockResolvedValue(undefined); renderHook(function () { return useAsyncEffekt(mockEffect); }); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); return [2 /*return*/]; } }); }); }); it("should handle rapid dependency changes correctly", function () { return __awaiter(void 0, void 0, void 0, function () { var mockEffect, dep, rerender, i; return __generator(this, function (_a) { switch (_a.label) { case 0: mockEffect = jest.fn().mockResolvedValue(undefined); dep = 1; rerender = renderHook(function () { return useAsyncEffekt(mockEffect, [dep]); }).rerender; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); // Rapidly change dependencies for (i = 2; i <= 5; i++) { dep = i; rerender(); } return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 2: _a.sent(); // Should have been called for each dependency change expect(mockEffect).toHaveBeenCalledTimes(5); return [2 /*return*/]; } }); }); }); it("should handle effect that throws synchronously", function () { return __awaiter(void 0, void 0, void 0, function () { var consoleErrorSpy, syncError, mockEffect; return __generator(this, function (_a) { switch (_a.label) { case 0: consoleErrorSpy = jest .spyOn(console, "error") .mockImplementation(function () { }); syncError = new Error("Synchronous error"); mockEffect = jest.fn().mockImplementation(function () { throw syncError; }); renderHook(function () { return useAsyncEffekt(mockEffect, []); }); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); expect(consoleErrorSpy).toHaveBeenCalledWith("useAsyncEffekt error:", syncError); consoleErrorSpy.mockRestore(); return [2 /*return*/]; } }); }); }); it("should handle cleanup function that throws", function () { return __awaiter(void 0, void 0, void 0, function () { var consoleErrorSpy, cleanupError, mockCleanup, mockEffect, unmount; return __generator(this, function (_a) { switch (_a.label) { case 0: consoleErrorSpy = jest .spyOn(console, "error") .mockImplementation(function () { }); cleanupError = new Error("Cleanup error"); mockCleanup = jest.fn().mockImplementation(function () { throw cleanupError; }); mockEffect = jest.fn().mockResolvedValue(mockCleanup); unmount = renderHook(function () { return useAsyncEffekt(mockEffect, []); }).unmount; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); unmount(); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 2: _a.sent(); expect(mockCleanup).toHaveBeenCalledTimes(1); expect(consoleErrorSpy).toHaveBeenCalledWith("useAsyncEffekt cleanup error:", cleanupError); consoleErrorSpy.mockRestore(); return [2 /*return*/]; } }); }); }); it("should handle async cleanup function that rejects", function () { return __awaiter(void 0, void 0, void 0, function () { var consoleErrorSpy, cleanupError, mockCleanup, mockEffect, unmount; return __generator(this, function (_a) { switch (_a.label) { case 0: consoleErrorSpy = jest .spyOn(console, "error") .mockImplementation(function () { }); cleanupError = new Error("Async cleanup error"); mockCleanup = jest.fn().mockRejectedValue(cleanupError); mockEffect = jest.fn().mockResolvedValue(mockCleanup); unmount = renderHook(function () { return useAsyncEffekt(mockEffect, []); }).unmount; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); unmount(); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 2: _a.sent(); expect(mockCleanup).toHaveBeenCalledTimes(1); expect(consoleErrorSpy).toHaveBeenCalledWith("useAsyncEffekt cleanup error:", cleanupError); consoleErrorSpy.mockRestore(); return [2 /*return*/]; } }); }); }); it("should handle multiple rapid dependency changes", function () { return __awaiter(void 0, void 0, void 0, function () { var mockEffect, dep, rerender, i; return __generator(this, function (_a) { switch (_a.label) { case 0: mockEffect = jest.fn().mockResolvedValue(undefined); dep = 1; rerender = renderHook(function () { return useAsyncEffekt(mockEffect, [dep]); }).rerender; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); // Rapid changes for (i = 2; i <= 5; i++) { dep = i; rerender(); } return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 2: _a.sent(); // Should handle all changes properly expect(mockEffect).toHaveBeenCalledTimes(5); return [2 /*return*/]; } }); }); }); it("should handle effect that returns null", function () { return __awaiter(void 0, void 0, void 0, function () { var mockEffect, unmount; return __generator(this, function (_a) { switch (_a.label) { case 0: mockEffect = jest.fn().mockResolvedValue(null); unmount = renderHook(function () { return useAsyncEffekt(mockEffect, []); }).unmount; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); // Should not throw when unmounting expect(function () { return unmount(); }).not.toThrow(); return [2 /*return*/]; } }); }); }); it("should handle undefined dependencies", function () { return __awaiter(void 0, void 0, void 0, function () { var mockEffect; return __generator(this, function (_a) { switch (_a.label) { case 0: mockEffect = jest.fn().mockResolvedValue(undefined); renderHook(function () { return useAsyncEffekt(mockEffect); }); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); expect(mockEffect).toHaveBeenCalledTimes(1); return [2 /*return*/]; } }); }); }); it("should handle empty dependencies array vs undefined", function () { return __awaiter(void 0, void 0, void 0, function () { var mockEffect1, mockEffect2, rerender1, rerender2; return __generator(this, function (_a) { switch (_a.label) { case 0: mockEffect1 = jest.fn().mockResolvedValue(undefined); mockEffect2 = jest.fn().mockResolvedValue(undefined); rerender1 = renderHook(function () { return useAsyncEffekt(mockEffect1, []); }).rerender; rerender2 = renderHook(function () { return useAsyncEffekt(mockEffect2); }).rerender; return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 1: _a.sent(); // Both should run once initially expect(mockEffect1).toHaveBeenCalledTimes(1); expect(mockEffect2).toHaveBeenCalledTimes(1); // Rerender with empty deps should not re-run rerender1(); // Rerender with undefined deps should re-run every time rerender2(); return [4 /*yield*/, act(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { jest.runAllTimers(); return [2 /*return*/]; }); }); })]; case 2: _a.sent(); expect(mockEffect1).toHaveBeenCalledTimes(1); // Still 1 - empty deps expect(mockEffect2).toHaveBeenCalledTimes(2); // Now 2 - undefined deps run on every render return [2 /*return*/]; } }); }); }); });