Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
const { defaults: tsjPreset } = require('ts-jest/presets')

const defaults = {
coverageDirectory: './coverage/',
collectCoverage: true,
testURL: 'http://localhost',
}
const testFolderPath = (folderName) => `<rootDir>/test/${folderName}/**/*.js`

const NORMAL_TEST_FOLDERS = ['components', 'hooks', 'integration', 'utils']

const standardConfig = {
...defaults,
displayName: 'ReactDOM',
testMatch: NORMAL_TEST_FOLDERS.map(testFolderPath),
}

const tsTestFolderPath = (folderName) =>
`<rootDir>/test/${folderName}/**/*.{ts,tsx}`

@@ -26,13 +21,14 @@ const tsStandardConfig = {
const rnConfig = {
...defaults,
displayName: 'React Native',
testMatch: [testFolderPath('react-native')],
testMatch: [tsTestFolderPath('react-native')],
preset: 'react-native',
transform: {
'^.+\\.js$': '<rootDir>/node_modules/react-native/jest/preprocessor.js',
...tsjPreset.transform,
},
}

module.exports = {
projects: [tsStandardConfig, standardConfig, rnConfig],
projects: [tsStandardConfig, rnConfig],
}
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -77,13 +77,15 @@
"@rollup/plugin-replace": "^2.3.3",
"@testing-library/jest-dom": "^5.11.5",
"@testing-library/jest-native": "^3.4.3",
"@testing-library/react": "^12.0.0",
"@testing-library/react-hooks": "^3.4.2",
"@testing-library/react": "https://pkg.csb.dev/testing-library/react-testing-library/commit/0e2cf7da/@testing-library/react/_pkg.tgz",
"@testing-library/react-hooks": "^7.0.1",
"@testing-library/react-native": "^7.1.0",
"@types/create-react-class": "^15.6.3",
"@types/object-assign": "^4.0.30",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
"@types/react-is": "^17.0.1",
"@types/react-native": "^0.64.12",
"@types/react-redux": "^7.1.18",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
@@ -101,10 +103,10 @@
"glob": "^7.1.6",
"jest": "^26.6.1",
"prettier": "^2.1.2",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react": "18.0.0-alpha-b9934d6db-20210805",
"react-dom": "18.0.0-alpha-b9934d6db-20210805",
"react-native": "^0.64.1",
"react-test-renderer": "^16.14.0",
"react-test-renderer": "18.0.0-alpha-b9934d6db-20210805",
"redux": "^4.0.5",
"rimraf": "^3.0.2",
"rollup": "^2.32.1",
9 changes: 0 additions & 9 deletions src/alternate-renderers.ts

This file was deleted.

5 changes: 2 additions & 3 deletions src/components/Context.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React from 'react'
import React, { MutableSource } from 'react'
import { Action, AnyAction, Store } from 'redux'
import type { FixTypeLater } from '../types'
import type { Subscription } from '../utils/Subscription'

export interface ReactReduxContextValue<
SS = FixTypeLater,
A extends Action = AnyAction
> {
storeSource: MutableSource<Store<SS, A>>
store: Store<SS, A>
subscription: Subscription
}

export const ReactReduxContext =
44 changes: 17 additions & 27 deletions src/components/Provider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, { Context, ReactNode, useMemo } from 'react'
import React, {
Context,
ReactNode,
unstable_createMutableSource as createMutableSource,
useMemo,
} from 'react'
import { ReactReduxContext, ReactReduxContextValue } from './Context'
import { createSubscription } from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
import type { FixTypeLater } from '../types'
import { Action, AnyAction, Store } from 'redux'

@@ -19,31 +22,18 @@ export interface ProviderProps<A extends Action = AnyAction> {
children: ReactNode
}

function Provider({ store, context, children }: ProviderProps) {
const contextValue = useMemo(() => {
const subscription = createSubscription(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])
export function createReduxContext(store: Store) {
return {
storeSource: createMutableSource(store, () => store.getState()),
store,
}
}

function Provider({ store, context, children }: ProviderProps) {
const contextValue: ReactReduxContextValue = useMemo(
() => createReduxContext(store),
[store]
)
const Context = context || ReactReduxContext

return <Context.Provider value={contextValue}>{children}</Context.Provider>
354 changes: 56 additions & 298 deletions src/components/connect.tsx

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions src/exports.ts
Original file line number Diff line number Diff line change
@@ -22,7 +22,6 @@ import { useSelector, createSelectorHook } from './hooks/useSelector'
import { useStore, createStoreHook } from './hooks/useStore'

import shallowEqual from './utils/shallowEqual'
import type { Subscription } from '../src/utils/Subscription'

export * from './types'
export type {
@@ -41,7 +40,6 @@ export type {
MapDispatchToPropsNonObject,
MergeProps,
ReactReduxContextValue,
Subscription,
}
export {
Provider,
122 changes: 18 additions & 104 deletions src/hooks/useSelector.ts
Original file line number Diff line number Diff line change
@@ -1,106 +1,11 @@
import { useReducer, useRef, useMemo, useContext, useDebugValue } from 'react'
import { useCallback, useContext, useDebugValue, useRef } from 'react'
import { useReduxContext as useDefaultReduxContext } from './useReduxContext'
import { createSubscription, Subscription } from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
import { ReactReduxContext } from '../components/Context'
import { AnyAction, Store } from 'redux'
import { DefaultRootState, EqualityFn } from '../types'
import { useStoreSource } from '../utils/useStoreSource'

const refEquality: EqualityFn<any> = (a, b) => a === b

type TSelector<S, R> = (state: S) => R

function useSelectorWithStoreAndSubscription<TStoreState, TSelectedState>(
selector: TSelector<TStoreState, TSelectedState>,
equalityFn: EqualityFn<TSelectedState>,
store: Store<TStoreState, AnyAction>,
contextSub: Subscription
): TSelectedState {
const [, forceRender] = useReducer((s) => s + 1, 0)

const subscription = useMemo(
() => createSubscription(store, contextSub),
[store, contextSub]
)

const latestSubscriptionCallbackError = useRef<Error>()
const latestSelector = useRef<TSelector<TStoreState, TSelectedState>>()
const latestStoreState = useRef<TStoreState>()
const latestSelectedState = useRef<TSelectedState>()

const storeState = store.getState()
let selectedState: TSelectedState | undefined

try {
if (
selector !== latestSelector.current ||
storeState !== latestStoreState.current ||
latestSubscriptionCallbackError.current
) {
const newSelectedState = selector(storeState)
// ensure latest selected state is reused so that a custom equality function can result in identical references
if (
latestSelectedState.current === undefined ||
!equalityFn(newSelectedState, latestSelectedState.current)
) {
selectedState = newSelectedState
} else {
selectedState = latestSelectedState.current
}
} else {
selectedState = latestSelectedState.current
}
} catch (err) {
if (latestSubscriptionCallbackError.current) {
;(
err as Error
).message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
}

throw err
}

useIsomorphicLayoutEffect(() => {
latestSelector.current = selector
latestStoreState.current = storeState
latestSelectedState.current = selectedState
latestSubscriptionCallbackError.current = undefined
})

useIsomorphicLayoutEffect(() => {
function checkForUpdates() {
try {
const newStoreState = store.getState()
const newSelectedState = latestSelector.current!(newStoreState)

if (equalityFn(newSelectedState, latestSelectedState.current)) {
return
}

latestSelectedState.current = newSelectedState
latestStoreState.current = newStoreState
} catch (err) {
// we ignore all errors here, since when the component
// is re-rendered, the selectors are called again, and
// will throw again, if neither props nor store state
// changed
latestSubscriptionCallbackError.current = err as Error
}

forceRender()
}

subscription.onStateChange = checkForUpdates
subscription.trySubscribe()

checkForUpdates()

return () => subscription.tryUnsubscribe()
}, [store, subscription])

return selectedState!
}

/**
* Hook factory, which creates a `useSelector` hook bound to a given context.
*
@@ -135,15 +40,24 @@ export function createSelectorHook(
)
}
}
const { store, subscription: contextSub } = useReduxContext()!

const selectedState = useSelectorWithStoreAndSubscription(
selector,
equalityFn,
store,
contextSub
const { storeSource } = useReduxContext()!

const lastValue = useRef<Selected | undefined>()
const getSnapshot = useCallback(
(store): Selected => {
const value = selector(store.getState())
if (
lastValue.current === undefined ||
!equalityFn(value, lastValue.current)
) {
lastValue.current = value
}
return lastValue.current
},
[selector, equalityFn]
)

const selectedState = useStoreSource(storeSource, getSnapshot)
useDebugValue(selectedState)

return selectedState
9 changes: 0 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1 @@
export * from './exports'

import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
import { setBatch } from './utils/batch'

// Enable batched updates in our subscriptions for use
// with standard React renderers (ReactDOM, React Native)
setBatch(batch)

export { batch }
151 changes: 0 additions & 151 deletions src/utils/Subscription.ts

This file was deleted.

13 changes: 0 additions & 13 deletions src/utils/batch.ts

This file was deleted.

5 changes: 0 additions & 5 deletions src/utils/reactBatchedUpdates.native.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/utils/reactBatchedUpdates.ts

This file was deleted.

5 changes: 0 additions & 5 deletions src/utils/useIsomorphicLayoutEffect.native.ts

This file was deleted.

17 changes: 0 additions & 17 deletions src/utils/useIsomorphicLayoutEffect.ts

This file was deleted.

16 changes: 16 additions & 0 deletions src/utils/useStoreSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Store } from 'redux'
import {
MutableSource,
unstable_useMutableSource as useMutableSource,
} from 'react'

const subscribe = (store: Store, callback: () => void) => {
return store.subscribe(callback)
}

export const useStoreSource = <Value>(
source: MutableSource<Store>,
getSnapshot: (store: Store) => Value
): Value => {
return useMutableSource(source, getSnapshot, subscribe)
}
19 changes: 11 additions & 8 deletions test/components/Provider.spec.tsx
Original file line number Diff line number Diff line change
@@ -342,16 +342,19 @@ describe('React', () => {
}
}

const div = document.createElement('div')
ReactDOM.render(
<Provider store={store}>
<div />
</Provider>,
div
)
const root = ReactDOM.createRoot(document.createElement('div'))
rtl.act(() => {
root.render(
<Provider store={store}>
<div />
</Provider>
)
})

expect(spy).toHaveBeenCalledTimes(0)
ReactDOM.unmountComponentAtNode(div)
rtl.act(() => {
root.unmount()
})
expect(spy).toHaveBeenCalledTimes(1)
})

1,890 changes: 1,213 additions & 677 deletions test/components/connect.spec.js → test/components/connect.spec.tsx

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion test/hooks/useReduxContext.spec.tsx
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ describe('React', () => {

const { result } = renderHook(() => useReduxContext())

expect(result.error.message).toMatch(
expect(result.error?.message).toMatch(
/could not find react-redux context value/
)

78 changes: 14 additions & 64 deletions test/hooks/useSelector.spec.tsx
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

import React, { useCallback, useReducer, useLayoutEffect } from 'react'
import { createStore } from 'redux'
import { renderHook, act } from '@testing-library/react-hooks'
import { renderHook } from '@testing-library/react-hooks'
import * as rtl from '@testing-library/react'
import {
Provider as ProviderMock,
@@ -11,14 +11,12 @@ import {
connect,
createSelectorHook,
} from '../../src/index'
import { useReduxContext } from '../../src/hooks/useReduxContext'
import type { FunctionComponent, DispatchWithoutAction, ReactNode } from 'react'
import type { Store, AnyAction } from 'redux'
import type {
ProviderProps,
TypedUseSelectorHook,
ReactReduxContextValue,
Subscription,
} from '../../src/'

describe('React', () => {
@@ -74,7 +72,7 @@ describe('React', () => {
expect(result.current).toEqual(0)
expect(selector).toHaveBeenCalledTimes(2)

act(() => {
rtl.act(() => {
normalStore.dispatch({ type: '' })
})

@@ -102,67 +100,13 @@ describe('React', () => {

expect(renderedItems).toEqual([1])

store.dispatch({ type: '' })
rtl.act(() => {
store.dispatch({ type: '' })
})

expect(renderedItems).toEqual([1, 2])
})

it('subscribes to the store synchronously', () => {
let rootSubscription: Subscription

const Parent = () => {
const { subscription } = useReduxContext() as ReactReduxContextValue
rootSubscription = subscription
const count = useNormalSelector((s) => s.count)
return count === 1 ? <Child /> : null
}

const Child = () => {
const count = useNormalSelector((s) => s.count)
return <div>{count}</div>
}

rtl.render(
<ProviderMock store={normalStore}>
<Parent />
</ProviderMock>
)
// @ts-ignore ts(2454)
expect(rootSubscription.getListeners().get().length).toBe(1)

normalStore.dispatch({ type: '' })
// @ts-ignore ts(2454)
expect(rootSubscription.getListeners().get().length).toBe(2)
})

it('unsubscribes when the component is unmounted', () => {
let rootSubscription: Subscription

const Parent = () => {
const { subscription } = useReduxContext() as ReactReduxContextValue
rootSubscription = subscription
const count = useNormalSelector((s) => s.count)
return count === 0 ? <Child /> : null
}

const Child = () => {
const count = useNormalSelector((s) => s.count)
return <div>{count}</div>
}

rtl.render(
<ProviderMock store={normalStore}>
<Parent />
</ProviderMock>
)
// @ts-ignore ts(2454)
expect(rootSubscription.getListeners().get().length).toBe(2)

normalStore.dispatch({ type: '' })
// @ts-ignore ts(2454)
expect(rootSubscription.getListeners().get().length).toBe(1)
})

it('notices store updates between render and store subscription effect', () => {
const Comp = () => {
const count = useNormalSelector((s) => s.count)
@@ -279,7 +223,9 @@ describe('React', () => {

expect(renderedItems.length).toBe(1)

store.dispatch({ type: '' })
rtl.act(() => {
store.dispatch({ type: '' })
})

expect(renderedItems.length).toBe(1)
})
@@ -458,7 +404,9 @@ describe('React', () => {
</ProviderMock>
)

normalStore.dispatch({ type: '' })
rtl.act(() => {
normalStore.dispatch({ type: '' })
})

expect(sawInconsistentState).toBe(false)

@@ -484,7 +432,9 @@ describe('React', () => {

expect(renderedItems.length).toBe(1)

normalStore.dispatch({ type: '' })
rtl.act(() => {
normalStore.dispatch({ type: '' })
})

expect(renderedItems.length).toBe(2)
expect(renderedItems[0]).toBe(renderedItems[1])
501 changes: 0 additions & 501 deletions test/react-native/batch-integration.js

This file was deleted.

57 changes: 0 additions & 57 deletions test/utils/Subscription.spec.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import isPlainObject from '../../src/utils/isPlainObject'
import vm from 'vm'

class Test {}
describe('isPlainObject', () => {
it('returns true only if plain object', () => {
function Test() {
this.prop = 1
}

const sandbox = { fromAnotherRealm: false }
vm.runInNewContext('fromAnotherRealm = {}', sandbox)

@@ -15,6 +11,7 @@ describe('isPlainObject', () => {
expect(isPlainObject(new Date())).toBe(false)
expect(isPlainObject([1, 2, 3])).toBe(false)
expect(isPlainObject(null)).toBe(false)
//@ts-expect-error
expect(isPlainObject()).toBe(false)
expect(isPlainObject({ x: 1, y: 2 })).toBe(true)
expect(isPlainObject(Object.create(null))).toBe(true)
File renamed without changes.
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -12,7 +12,8 @@
"emitDeclarationOnly": true,
"outDir": "./es",
"forceConsistentCasingInFileNames": true,
"experimentalDecorators":true
"experimentalDecorators":true,
"types": ["react/next", "react-dom/next", "jest", "node", "@testing-library/jest-dom"]
},
"include": ["src/**/*", "test/**/*", "types"],
"exclude": ["node_modules", "dist"]
13 changes: 0 additions & 13 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
/* eslint-disable no-unused-vars */

declare module 'react-native' {
export function unstable_batchedUpdates<A, B>(
callback: (a: A, b: B) => any,
a: A,
b: B
): void
export function unstable_batchedUpdates<A>(
callback: (a: A) => any,
a: A
): void
export function unstable_batchedUpdates(callback: () => any): void
}

declare module 'react-is' {
import * as React from 'react'
export function isContextConsumer(value: any): value is React.ReactElement
1 change: 1 addition & 0 deletions website/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# dependencies
/node_modules
/package.lock.json

# production
/build
4 changes: 2 additions & 2 deletions website/package.json
Original file line number Diff line number Diff line change
@@ -9,8 +9,8 @@
"serve": "docusaurus serve"
},
"dependencies": {
"@docusaurus/core": "^2.0.0-beta.2",
"@docusaurus/preset-classic": "^2.0.0-beta.2",
"@docusaurus/core": "2.0.0-beta.4",
"@docusaurus/preset-classic": "2.0.0-beta.4",
"classnames": "^2.2.6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
10 changes: 10 additions & 0 deletions website/static/css/custom.css
Original file line number Diff line number Diff line change
@@ -77,6 +77,16 @@
--ifm-hero-background-color: #593d88;
--ifm-hero-text-color: #ffffff;
}

.admonition a,
blockquote a {
color: var(--ifm-color-primary-darkest);
}
.admonition a:hover,
blockquote a:hover {
color: var(--blockquote-text-color);
}

blockquote {
color: var(--blockquote-text-color);
background-color: var(--ifm-blockquote-color);
2,503 changes: 1,213 additions & 1,290 deletions yarn.lock

Large diffs are not rendered by default.