diff --git a/package.json b/package.json index 9866a25..49ac38d 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", "@types/testing-library__jest-dom": "^5.14.5", + "@types/use-sync-external-store": "^0.0.3", "csstype": "^3.1.0", "jest": "^28.1.3", "jest-environment-jsdom": "^28.1.3", @@ -94,7 +95,8 @@ "typescript": "^5.0.4" }, "dependencies": { - "goober": "^2.1.10" + "goober": "^2.1.10", + "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": ">=16", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47dc1fd..c52b9bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ dependencies: goober: specifier: ^2.1.10 version: 2.1.10(csstype@3.1.0) + use-sync-external-store: + specifier: ^1.2.0 + version: 1.2.0(react@18.2.0) devDependencies: '@jest/types': @@ -30,6 +33,9 @@ devDependencies: '@types/testing-library__jest-dom': specifier: ^5.14.5 version: 5.14.5 + '@types/use-sync-external-store': + specifier: ^0.0.3 + version: 0.0.3 csstype: specifier: ^3.1.0 version: 3.1.0 @@ -1116,6 +1122,10 @@ packages: resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} dev: true + /@types/use-sync-external-store@0.0.3: + resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} + dev: true + /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} dev: true @@ -2750,7 +2760,6 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} @@ -2879,7 +2888,6 @@ packages: hasBin: true dependencies: js-tokens: 4.0.0 - dev: true /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} @@ -3223,7 +3231,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: true /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} @@ -3709,6 +3716,14 @@ packages: picocolors: 1.0.0 dev: true + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /v8-to-istanbul@9.0.1: resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} engines: {node: '>=10.12.0'} diff --git a/src/core/store.ts b/src/core/store.ts index 506978a..9ac24b0 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'; import { DefaultToastOptions, Toast, ToastType } from './types'; const TOAST_LIMIT = 20; @@ -157,14 +157,15 @@ export const reducer = (state: State, action: Action): State => { } }; -const listeners: Array<(state: State) => void> = []; +const listeners: Array<() => void> = []; let memoryState: State = { toasts: [], pausedAt: undefined }; export const dispatch = (action: Action) => { + const oldState = memoryState; memoryState = reducer(memoryState, action); listeners.forEach((listener) => { - listener(memoryState); + listener(); }); }; @@ -179,16 +180,16 @@ export const defaultTimeouts: { }; export const useStore = (toastOptions: DefaultToastOptions = {}): State => { - const [state, setState] = useState(memoryState); - useEffect(() => { - listeners.push(setState); + const state = useSyncExternalStore((onStoreChange) => { + listeners.push(onStoreChange); + return () => { - const index = listeners.indexOf(setState); + const index = listeners.indexOf(onStoreChange); if (index > -1) { listeners.splice(index, 1); } - }; - }, [state]); + } + }, () => memoryState); const mergedToasts = state.toasts.map((t) => ({ ...toastOptions, diff --git a/test/toast.test.tsx b/test/toast.test.tsx index 37dfec4..e3592d7 100644 --- a/test/toast.test.tsx +++ b/test/toast.test.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { render, screen, @@ -115,6 +115,30 @@ test('promise toast', async () => { }); }); +test('"toast" can be called from useEffect hook', async () => { + const WAIT_DELAY = 1000; + + const MyComponent = () => { + const [success, setSuccess] = useState(false); + useEffect(() => { + toast.success("Success toast") + setSuccess(true); + }, []); + + return success ?
MyComponent finished
: null; + } + + render( + <> + + + + ); + + await screen.findByText(/MyComponent finished/i); + expect(screen.queryByText(/Success toast/i)).toBeInTheDocument(); +}); + test('promise toast error', async () => { const WAIT_DELAY = 1000;