diff --git a/rollup.config.js b/rollup.config.js index 4013edac5..d4d6330c3 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,7 +7,7 @@ import pkg from './package.json' const env = process.env.NODE_ENV -const extensions = ['.js', '.ts', '.json'] +const extensions = ['.js', '.ts', '.tsx', '.json'] const config = { input: 'src/index.js', diff --git a/src/components/Context.js b/src/components/Context.js deleted file mode 100644 index a706d517f..000000000 --- a/src/components/Context.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' - -export const ReactReduxContext = /*#__PURE__*/ React.createContext(null) - -if (process.env.NODE_ENV !== 'production') { - ReactReduxContext.displayName = 'ReactRedux' -} - -export default ReactReduxContext diff --git a/src/components/Context.ts b/src/components/Context.ts new file mode 100644 index 000000000..684438771 --- /dev/null +++ b/src/components/Context.ts @@ -0,0 +1,17 @@ +import React from 'react' +import { Action, AnyAction, Store } from 'redux' +import type { FixTypeLater } from '../types' +import type Subscription from '../utils/Subscription' + +export interface ReactReduxContextValue { + store: Store + subscription: Subscription +} + +export const ReactReduxContext = /*#__PURE__*/ React.createContext(null) + +if (process.env.NODE_ENV !== 'production') { + ReactReduxContext.displayName = 'ReactRedux' +} + +export default ReactReduxContext diff --git a/src/components/Provider.js b/src/components/Provider.js deleted file mode 100644 index fdfc48b75..000000000 --- a/src/components/Provider.js +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useMemo } from 'react' -import PropTypes from 'prop-types' -import { ReactReduxContext } from './Context' -import Subscription from '../utils/Subscription' -import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' - -function Provider({ store, context, children }) { - const contextValue = useMemo(() => { - const subscription = new Subscription(store) - subscription.onStateChange = subscription.notifyNestedSubs - return { - store, - subscription, - } - }, [store]) - - const previousState = useMemo(() => store.getState(), [store]) - - useIsomorphicLayoutEffect(() => { - const { subscription } = contextValue - subscription.trySubscribe() - - if (previousState !== store.getState()) { - subscription.notifyNestedSubs() - } - return () => { - subscription.tryUnsubscribe() - subscription.onStateChange = null - } - }, [contextValue, previousState]) - - const Context = context || ReactReduxContext - - return {children} -} - -if (process.env.NODE_ENV !== 'production') { - Provider.propTypes = { - store: PropTypes.shape({ - subscribe: PropTypes.func.isRequired, - dispatch: PropTypes.func.isRequired, - getState: PropTypes.func.isRequired, - }), - context: PropTypes.object, - children: PropTypes.any, - } -} - -export default Provider diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx new file mode 100644 index 000000000..bbd84d0a1 --- /dev/null +++ b/src/components/Provider.tsx @@ -0,0 +1,52 @@ +import React, { Context, ReactNode, useMemo } from 'react' +import { ReactReduxContext, ReactReduxContextValue } from './Context' +import Subscription from '../utils/Subscription' +import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' +import type { FixTypeLater } from '../types' +import { Action, AnyAction, Store } from 'redux' + +interface ProviderProps { + /** + * The single Redux store in your application. + */ + store: Store + /** + * Optional context to be used internally in react-redux. Use React.createContext() to create a context to be used. + * If this is used, generate own connect HOC by using connectAdvanced, supplying the same context provided to the + * Provider. Initial value doesn't matter, as it is overwritten with the internal state of Provider. + */ + context?: Context + children: ReactNode +} + +function Provider({ store, context, children }: ProviderProps) { + const contextValue = useMemo(() => { + const subscription = new Subscription(store) + subscription.onStateChange = subscription.notifyNestedSubs + return { + store, + subscription, + } + }, [store]) + + const previousState = useMemo(() => store.getState(), [store]) + + useIsomorphicLayoutEffect(() => { + const { subscription } = contextValue + subscription.trySubscribe() + + if (previousState !== store.getState()) { + subscription.notifyNestedSubs() + } + return () => { + subscription.tryUnsubscribe() + subscription.onStateChange = undefined + } + }, [contextValue, previousState]) + + const Context = context || ReactReduxContext + + return {children} +} + +export default Provider diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..d8b25c80f --- /dev/null +++ b/src/types.ts @@ -0,0 +1 @@ +export type FixTypeLater = any \ No newline at end of file diff --git a/src/utils/Subscription.js b/src/utils/Subscription.ts similarity index 71% rename from src/utils/Subscription.js rename to src/utils/Subscription.ts index 7030ec84a..d708e3b32 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.ts @@ -4,12 +4,16 @@ import { getBatch } from './batch' // well as nesting subscriptions of descendant components, so that we can ensure the // ancestor components re-render before descendants -const nullListeners = { notify() {} } +type Listener = { + callback: () => void + next: Listener | null + prev: Listener | null +} function createListenerCollection() { const batch = getBatch() - let first = null - let last = null + let first: Listener | null = null + let last: Listener | null = null return { clear() { @@ -37,10 +41,10 @@ function createListenerCollection() { return listeners }, - subscribe(callback) { + subscribe(callback: () => void) { let isSubscribed = true - let listener = (last = { + let listener: Listener = (last = { callback, next: null, prev: last, @@ -71,29 +75,35 @@ function createListenerCollection() { } } +type ListenerCollection = ReturnType + export default class Subscription { - constructor(store, parentSub) { + private store: any + private parentSub?: Subscription + private unsubscribe?: () => void + private listeners?: ListenerCollection + public onStateChange?: () => void + + constructor(store: any, parentSub?: Subscription) { this.store = store this.parentSub = parentSub - this.unsubscribe = null - this.listeners = nullListeners + this.unsubscribe = undefined + this.listeners = undefined this.handleChangeWrapper = this.handleChangeWrapper.bind(this) } - addNestedSub(listener) { + addNestedSub(listener: () => void) { this.trySubscribe() - return this.listeners.subscribe(listener) + return this.listeners?.subscribe(listener) } notifyNestedSubs() { - this.listeners.notify() + this.listeners?.notify() } handleChangeWrapper() { - if (this.onStateChange) { - this.onStateChange() - } + this.onStateChange?.() } isSubscribed() { @@ -113,9 +123,9 @@ export default class Subscription { tryUnsubscribe() { if (this.unsubscribe) { this.unsubscribe() - this.unsubscribe = null - this.listeners.clear() - this.listeners = nullListeners + this.unsubscribe = undefined + this.listeners?.clear() + this.listeners = undefined } } } diff --git a/tsconfig.json b/tsconfig.json index 97a0dae15..8bddcdad3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ // "lib": [], /* Specify library files to be included in the compilation. */ "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ "declaration": true, /* Generates corresponding '.d.ts' file. */ "emitDeclarationOnly": true, // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */