diff --git a/.yalc/react-redux/LICENSE.md b/.yalc/react-redux/LICENSE.md new file mode 100644 index 0000000..55bc8df --- /dev/null +++ b/.yalc/react-redux/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-present Dan Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.yalc/react-redux/README.md b/.yalc/react-redux/README.md new file mode 100644 index 0000000..2834b24 --- /dev/null +++ b/.yalc/react-redux/README.md @@ -0,0 +1,64 @@ +# React Redux + +Official React bindings for [Redux](https://github.com/reduxjs/redux). +Performant and flexible. + +![GitHub Workflow Status](https://img.shields.io/github/workflow/status/reduxjs/react-redux/CI?style=flat-square) [![npm version](https://img.shields.io/npm/v/react-redux.svg?style=flat-square)](https://www.npmjs.com/package/react-redux) +[![npm downloads](https://img.shields.io/npm/dm/react-redux.svg?style=flat-square)](https://www.npmjs.com/package/react-redux) +[![redux channel on discord](https://img.shields.io/badge/discord-redux@reactiflux-61DAFB.svg?style=flat-square)](http://www.reactiflux.com) + +## Installation + +### Using Create React App + +The recommended way to start new apps with React Redux is by using the [official Redux+JS template](https://github.com/reduxjs/cra-template-redux) for [Create React App](https://github.com/facebook/create-react-app), which takes advantage of [Redux Toolkit](https://redux-toolkit.js.org/). + +```sh +npx create-react-app my-app --template redux +``` + +### An Existing React App + +React Redux 7.1 requires **React 16.8.3 or later.** + +To use React Redux with your React app, install it as a dependency: + +```bash +# If you use npm: +npm install react-redux + +# Or if you use Yarn: +yarn add react-redux +``` + +You'll also need to [install Redux](https://redux.js.org/introduction/installation) and [set up a Redux store](https://redux.js.org/recipes/configuring-your-store/) in your app. + +This assumes that you’re using [npm](http://npmjs.com/) package manager +with a module bundler like [Webpack](https://webpack.js.org/) or +[Browserify](http://browserify.org/) to consume [CommonJS +modules](https://webpack.js.org/api/module-methods/#commonjs). + +If you don’t yet use [npm](http://npmjs.com/) or a modern module bundler, and would rather prefer a single-file [UMD](https://github.com/umdjs/umd) build that makes `ReactRedux` available as a global object, you can grab a pre-built version from [cdnjs](https://cdnjs.com/libraries/react-redux). We _don’t_ recommend this approach for any serious application, as most of the libraries complementary to Redux are only available on [npm](http://npmjs.com/). + +## React Native + +As of React Native 0.18, React Redux 5.x should work with React Native. If you have any issues with React Redux 5.x on React Native, run `npm ls react` and make sure you don’t have a duplicate React installation in your `node_modules`. We recommend that you use `npm@3.x` which is better at avoiding these kinds of issues. + +## Documentation + +The React Redux docs are now published at **https://react-redux.js.org** . + +We're currently expanding and rewriting our docs content - check back soon for more updates! + +## How Does It Work? + +We do a deep dive on how React Redux works in [this readthesource episode](https://www.youtube.com/watch?v=VJ38wSFbM3A). + +Also, the post [The History and Implementation of React-Redux](https://blog.isquaredsoftware.com/2018/11/react-redux-history-implementation/) +explains what it does, how it works, and how the API and implementation have evolved over time. + +Enjoy! + +## License + +[MIT](LICENSE.md) diff --git a/.yalc/react-redux/package.json b/.yalc/react-redux/package.json new file mode 100644 index 0000000..c5ecc70 --- /dev/null +++ b/.yalc/react-redux/package.json @@ -0,0 +1,69 @@ +{ + "name": "react-redux", + "version": "8.0.0-alpha.0", + "description": "Official React bindings for Redux", + "keywords": [ + "react", + "reactjs", + "redux" + ], + "license": "MIT", + "author": "Dan Abramov (https://github.com/gaearon)", + "homepage": "https://github.com/reduxjs/react-redux", + "repository": "github:reduxjs/react-redux", + "bugs": "https://github.com/reduxjs/react-redux/issues", + "main": "./lib/index.js", + "types": "./es/index.d.ts", + "unpkg": "dist/react-redux.js", + "module": "es/index.js", + "files": [ + "dist", + "lib", + "src", + "es" + ], + "scripts": { + "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --extensions \".js,.ts,.tsx\" --out-dir lib", + "build:es": "babel src --extensions \".js,.ts,.tsx\" --out-dir es", + "build:umd": "cross-env NODE_ENV=development rollup -c -o dist/react-redux.js", + "build:umd:min": "cross-env NODE_ENV=production rollup -c -o dist/react-redux.min.js", + "build:types": "tsc", + "build": "yarn build:types && yarn build:commonjs && yarn build:es && yarn build:umd && yarn build:umd:min", + "clean": "rimraf lib dist es coverage", + "api-types": "api-extractor run --local", + "format": "prettier --write \"{src,test}/**/*.{js,ts}\" \"docs/**/*.md\"", + "lint": "eslint src --ext ts,js test/utils test/components test/hooks", + "pretest": "yarn lint", + "test": "jest", + "type-tests": "yarn tsc -p test/typetests", + "coverage": "codecov" + }, + "workspaces": [ + "website" + ], + "peerDependencies": { + "react": "^16.8.3 || ^17" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + }, + "dependencies": { + "@babel/runtime": "^7.12.1", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.13.1", + "use-sync-external-store": "0.0.0-experimental-5b57bc6e3-20210921" + }, + "browserify": { + "transform": [ + "loose-envify" + ] + }, + "yalcSig": "ff0db988ba4013dfd771fdeeafc7f593" +} diff --git a/.yalc/react-redux/src/alternate-renderers.ts b/.yalc/react-redux/src/alternate-renderers.ts new file mode 100644 index 0000000..456c7a2 --- /dev/null +++ b/.yalc/react-redux/src/alternate-renderers.ts @@ -0,0 +1,9 @@ +export * from './exports' + +import { getBatch } from './utils/batch' + +// For other renderers besides ReactDOM and React Native, +// use the default noop batch function +const batch = getBatch() + +export { batch } diff --git a/.yalc/react-redux/src/components/Context.ts b/.yalc/react-redux/src/components/Context.ts new file mode 100644 index 0000000..1dbf956 --- /dev/null +++ b/.yalc/react-redux/src/components/Context.ts @@ -0,0 +1,23 @@ +import React 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 +> { + store: Store + subscription: Subscription +} + +export const ReactReduxContext = + /*#__PURE__*/ React.createContext(null) + +export type ReactReduxContextInstance = typeof ReactReduxContext + +if (process.env.NODE_ENV !== 'production') { + ReactReduxContext.displayName = 'ReactRedux' +} + +export default ReactReduxContext diff --git a/.yalc/react-redux/src/components/Provider.tsx b/.yalc/react-redux/src/components/Provider.tsx new file mode 100644 index 0000000..4e77a2e --- /dev/null +++ b/.yalc/react-redux/src/components/Provider.tsx @@ -0,0 +1,53 @@ +import React, { Context, ReactNode, 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' + +export 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, you'll need to customize `connect` by 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 = createSubscription(store) + return { + store, + subscription, + } + }, [store]) + + const previousState = useMemo(() => store.getState(), [store]) + + useIsomorphicLayoutEffect(() => { + const { subscription } = contextValue + + subscription.onStateChange = subscription.notifyNestedSubs + 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/.yalc/react-redux/src/components/connect.tsx b/.yalc/react-redux/src/components/connect.tsx new file mode 100644 index 0000000..914e930 --- /dev/null +++ b/.yalc/react-redux/src/components/connect.tsx @@ -0,0 +1,804 @@ +/* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */ +import hoistStatics from 'hoist-non-react-statics' +import React, { useContext, useMemo, useRef, useReducer } from 'react' +import { isValidElementType, isContextConsumer } from 'react-is' +import { useSyncExternalStore } from 'use-sync-external-store' + +import type { Store, Dispatch, Action, AnyAction } from 'redux' + +import type { + AdvancedComponentDecorator, + ConnectedComponent, + DefaultRootState, + InferableComponentEnhancer, + InferableComponentEnhancerWithProps, + ResolveThunks, + DispatchProp, +} from '../types' + +import defaultSelectorFactory, { + MapStateToPropsParam, + MapDispatchToPropsParam, + MergeProps, + MapDispatchToPropsNonObject, + SelectorFactoryOptions, +} from '../connect/selectorFactory' +import defaultMapDispatchToPropsFactories from '../connect/mapDispatchToProps' +import defaultMapStateToPropsFactories from '../connect/mapStateToProps' +import defaultMergePropsFactories from '../connect/mergeProps' + +import { createSubscription, Subscription } from '../utils/Subscription' +import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' +import shallowEqual from '../utils/shallowEqual' + +import { + ReactReduxContext, + ReactReduxContextValue, + ReactReduxContextInstance, +} from './Context' + +// Define some constant arrays just to avoid re-creating these +const EMPTY_ARRAY: [unknown, number] = [null, 0] +const NO_SUBSCRIPTION_ARRAY = [null, null] + +// Attempts to stringify whatever not-really-a-component value we were given +// for logging in an error message +const stringifyComponent = (Comp: unknown) => { + try { + return JSON.stringify(Comp) + } catch (err) { + return String(Comp) + } +} + +type EffectFunc = (...args: any[]) => void | ReturnType + +// This is "just" a `useLayoutEffect`, but with two modifications: +// - we need to fall back to `useEffect` in SSR to avoid annoying warnings +// - we extract this to a separate function to avoid closing over values +// and causing memory leaks +function useIsomorphicLayoutEffectWithArgs( + effectFunc: EffectFunc, + effectArgs: any[], + dependencies?: React.DependencyList +) { + useIsomorphicLayoutEffect(() => effectFunc(...effectArgs), dependencies) +} + +// Effect callback, extracted: assign the latest props values to refs for later usage +function captureWrapperProps( + lastWrapperProps: React.MutableRefObject, + lastChildProps: React.MutableRefObject, + renderIsScheduled: React.MutableRefObject, + wrapperProps: unknown, + // actualChildProps: unknown, + childPropsFromStoreUpdate: React.MutableRefObject, + notifyNestedSubs: () => void +) { + // We want to capture the wrapper props and child props we used for later comparisons + lastWrapperProps.current = wrapperProps + renderIsScheduled.current = false + + // If the render was from a store update, clear out that reference and cascade the subscriber update + if (childPropsFromStoreUpdate.current) { + childPropsFromStoreUpdate.current = null + notifyNestedSubs() + } +} + +// Effect callback, extracted: subscribe to the Redux store or nearest connected ancestor, +// check for updates after dispatched actions, and trigger re-renders. +function subscribeUpdates( + shouldHandleStateChanges: boolean, + store: Store, + subscription: Subscription, + childPropsSelector: (state: unknown, props: unknown) => unknown, + lastWrapperProps: React.MutableRefObject, + lastChildProps: React.MutableRefObject, + renderIsScheduled: React.MutableRefObject, + isMounted: React.MutableRefObject, + childPropsFromStoreUpdate: React.MutableRefObject, + notifyNestedSubs: () => void, + // forceComponentUpdateDispatch: React.Dispatch, + additionalSubscribeListener: () => void +) { + // If we're not subscribed to the store, nothing to do here + if (!shouldHandleStateChanges) return () => {} + + // Capture values for checking if and when this component unmounts + let didUnsubscribe = false + let lastThrownError: Error | null = null + + // We'll run this callback every time a store subscription update propagates to this component + const checkForUpdates = () => { + if (didUnsubscribe || !isMounted.current) { + // Don't run stale listeners. + // Redux doesn't guarantee unsubscriptions happen until next dispatch. + return + } + + const latestStoreState = store.getState() + + let newChildProps, error + try { + // Actually run the selector with the most recent store state and wrapper props + // to determine what the child props should be + newChildProps = childPropsSelector( + latestStoreState, + lastWrapperProps.current + ) + } catch (e) { + error = e + lastThrownError = e as Error | null + } + + if (!error) { + lastThrownError = null + } + + // If the child props haven't changed, nothing to do here - cascade the subscription update + if (newChildProps === lastChildProps.current) { + if (!renderIsScheduled.current) { + notifyNestedSubs() + } + } else { + // Save references to the new child props. Note that we track the "child props from store update" + // as a ref instead of a useState/useReducer because we need a way to determine if that value has + // been processed. If this went into useState/useReducer, we couldn't clear out the value without + // forcing another re-render, which we don't want. + lastChildProps.current = newChildProps + childPropsFromStoreUpdate.current = newChildProps + renderIsScheduled.current = true + + // Trigger the React `useSyncExternalStore` subscriber + additionalSubscribeListener() + } + } + + // Actually subscribe to the nearest connected ancestor (or store) + subscription.onStateChange = checkForUpdates + subscription.trySubscribe() + + // Pull data from the store after first render in case the store has + // changed since we began. + checkForUpdates() + + const unsubscribeWrapper = () => { + didUnsubscribe = true + subscription.tryUnsubscribe() + subscription.onStateChange = null + + if (lastThrownError) { + // It's possible that we caught an error due to a bad mapState function, but the + // parent re-rendered without this component and we're about to unmount. + // This shouldn't happen as long as we do top-down subscriptions correctly, but + // if we ever do those wrong, this throw will surface the error in our tests. + // In that case, throw the error from here so it doesn't get lost. + throw lastThrownError + } + } + + return unsubscribeWrapper +} + +// Reducer initial state creation for our update reducer +const initStateUpdates = () => EMPTY_ARRAY + +export interface ConnectProps { + reactReduxForwardedRef?: React.ForwardedRef + context?: ReactReduxContextInstance + store?: Store +} + +function match( + arg: unknown, + factories: ((value: unknown) => T)[], + name: string +): T { + for (let i = factories.length - 1; i >= 0; i--) { + const result = factories[i](arg) + if (result) return result + } + + return ((dispatch: Dispatch, options: { wrappedComponentName: string }) => { + throw new Error( + `Invalid value of type ${typeof arg} for ${name} argument when connecting component ${ + options.wrappedComponentName + }.` + ) + }) as any +} + +function strictEqual(a: unknown, b: unknown) { + return a === b +} + +/** + * Infers the type of props that a connector will inject into a component. + */ +export type ConnectedProps = + TConnector extends InferableComponentEnhancerWithProps< + infer TInjectedProps, + any + > + ? unknown extends TInjectedProps + ? TConnector extends InferableComponentEnhancer + ? TInjectedProps + : never + : TInjectedProps + : never + +export interface ConnectOptions< + State = DefaultRootState, + TStateProps = {}, + TOwnProps = {}, + TMergedProps = {} +> { + forwardRef?: boolean + context?: typeof ReactReduxContext + areStatesEqual?: (nextState: State, prevState: State) => boolean + + areOwnPropsEqual?: ( + nextOwnProps: TOwnProps, + prevOwnProps: TOwnProps + ) => boolean + + areStatePropsEqual?: ( + nextStateProps: TStateProps, + prevStateProps: TStateProps + ) => boolean + areMergedPropsEqual?: ( + nextMergedProps: TMergedProps, + prevMergedProps: TMergedProps + ) => boolean +} + +/* @public */ +function connect(): InferableComponentEnhancer + +/* @public */ +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam +): InferableComponentEnhancerWithProps + +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject +): InferableComponentEnhancerWithProps + +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam +): InferableComponentEnhancerWithProps, TOwnProps> + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject +): InferableComponentEnhancerWithProps + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam +): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps +> + +/* @public */ +function connect< + no_state = {}, + no_dispatch = {}, + TOwnProps = {}, + TMergedProps = {} +>( + mapStateToProps: null | undefined, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps + +/* @public */ +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps + +/* @public */ +function connect< + no_state = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {} +>( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps + +/* @public */ +// @ts-ignore +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps + +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: ConnectOptions<{}, TStateProps, TOwnProps> +): InferableComponentEnhancerWithProps + +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: ConnectOptions<{}, TStateProps, TOwnProps> +): InferableComponentEnhancerWithProps, TOwnProps> + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps +> + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps, + options?: ConnectOptions +): InferableComponentEnhancerWithProps + +/** + * Connects a React component to a Redux store. + * + * - Without arguments, just wraps the component, without changing the behavior / props + * + * - If 2 params are passed (3rd param, mergeProps, is skipped), default behavior + * is to override ownProps (as stated in the docs), so what remains is everything that's + * not a state or dispatch prop + * + * - When 3rd param is passed, we don't know if ownProps propagate and whether they + * should be valid component props, because it depends on mergeProps implementation. + * As such, it is the user's responsibility to extend ownProps interface from state or + * dispatch props or both when applicable + * + * @param mapStateToProps A function that extracts values from state + * @param mapDispatchToProps Setup for dispatching actions + * @param mergeProps Optional callback to merge state and dispatch props together + * @param options Options for configuring the connection + * + */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultRootState +>( + mapStateToProps?: MapStateToPropsParam, + mapDispatchToProps?: MapDispatchToPropsParam, + mergeProps?: MergeProps, + { + // The `pure` option has been removed, so TS doesn't like us destructuring this to check its existence. + // @ts-ignore + pure, + areStatesEqual = strictEqual, + areOwnPropsEqual = shallowEqual, + areStatePropsEqual = shallowEqual, + areMergedPropsEqual = shallowEqual, + + // use React's forwardRef to expose a ref of the wrapped component + forwardRef = false, + + // the context consumer to use + context = ReactReduxContext, + }: ConnectOptions = {} +): unknown { + if (process.env.NODE_ENV !== 'production') { + if (pure !== undefined) { + throw new Error( + 'The `pure` option has been removed. `connect` is now always a "pure/memoized" component' + ) + } + } + + const Context = context + + type WrappedComponentProps = TOwnProps & ConnectProps + + const initMapStateToProps = match( + mapStateToProps, + // @ts-ignore + defaultMapStateToPropsFactories, + 'mapStateToProps' + )! + const initMapDispatchToProps = match( + mapDispatchToProps, + // @ts-ignore + defaultMapDispatchToPropsFactories, + 'mapDispatchToProps' + )! + const initMergeProps = match( + mergeProps, + // @ts-ignore + defaultMergePropsFactories, + 'mergeProps' + )! + + const shouldHandleStateChanges = Boolean(mapStateToProps) + + const wrapWithConnect: AdvancedComponentDecorator< + TOwnProps, + WrappedComponentProps + > = (WrappedComponent) => { + if ( + process.env.NODE_ENV !== 'production' && + !isValidElementType(WrappedComponent) + ) { + throw new Error( + `You must pass a component to the function returned by connect. Instead received ${stringifyComponent( + WrappedComponent + )}` + ) + } + + const wrappedComponentName = + WrappedComponent.displayName || WrappedComponent.name || 'Component' + + const displayName = `Connect(${wrappedComponentName})` + + const selectorFactoryOptions: SelectorFactoryOptions = { + pure, + shouldHandleStateChanges, + displayName, + wrappedComponentName, + WrappedComponent, + initMapStateToProps, + initMapDispatchToProps, + // @ts-ignore + initMergeProps, + areStatesEqual, + areStatePropsEqual, + areOwnPropsEqual, + areMergedPropsEqual, + } + + // If we aren't running in "pure" mode, we don't want to memoize values. + // To avoid conditionally calling hooks, we fall back to a tiny wrapper + // that just executes the given callback immediately. + const usePureOnlyMemo = pure ? useMemo : (callback: () => any) => callback() + + function ConnectFunction(props: ConnectProps & TOwnProps) { + const [propsContext, reactReduxForwardedRef, wrapperProps] = + useMemo(() => { + // Distinguish between actual "data" props that were passed to the wrapper component, + // and values needed to control behavior (forwarded refs, alternate context instances). + // To maintain the wrapperProps object reference, memoize this destructuring. + const { reactReduxForwardedRef, ...wrapperProps } = props + return [props.context, reactReduxForwardedRef, wrapperProps] + }, [props]) + + const ContextToUse: ReactReduxContextInstance = useMemo(() => { + // Users may optionally pass in a custom context instance to use instead of our ReactReduxContext. + // Memoize the check that determines which context instance we should use. + return propsContext && + propsContext.Consumer && + // @ts-ignore + isContextConsumer() + ? propsContext + : Context + }, [propsContext, Context]) + + // Retrieve the store and ancestor subscription via context, if available + const contextValue = useContext(ContextToUse) + + // The store _must_ exist as either a prop or in context. + // We'll check to see if it _looks_ like a Redux store first. + // This allows us to pass through a `store` prop that is just a plain value. + const didStoreComeFromProps = + Boolean(props.store) && + Boolean(props.store!.getState) && + Boolean(props.store!.dispatch) + const didStoreComeFromContext = + Boolean(contextValue) && Boolean(contextValue!.store) + + if ( + process.env.NODE_ENV !== 'production' && + !didStoreComeFromProps && + !didStoreComeFromContext + ) { + throw new Error( + `Could not find "store" in the context of ` + + `"${displayName}". Either wrap the root component in a , ` + + `or pass a custom React context provider to and the corresponding ` + + `React context consumer to ${displayName} in connect options.` + ) + } + + // Based on the previous check, one of these must be true + const store: Store = didStoreComeFromProps + ? props.store! + : contextValue!.store + + const childPropsSelector = useMemo(() => { + // The child props selector needs the store reference as an input. + // Re-create this selector whenever the store changes. + return defaultSelectorFactory(store.dispatch, selectorFactoryOptions) + }, [store]) + + const [subscription, notifyNestedSubs] = useMemo(() => { + if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY + + // This Subscription's source should match where store came from: props vs. context. A component + // connected to the store via props shouldn't use subscription from context, or vice versa. + const subscription = createSubscription( + store, + didStoreComeFromProps ? undefined : contextValue!.subscription + ) + + // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in + // the middle of the notification loop, where `subscription` will then be null. This can + // probably be avoided if Subscription's listeners logic is changed to not call listeners + // that have been unsubscribed in the middle of the notification loop. + const notifyNestedSubs = + subscription.notifyNestedSubs.bind(subscription) + + return [subscription, notifyNestedSubs] + }, [store, didStoreComeFromProps, contextValue]) + + // Determine what {store, subscription} value should be put into nested context, if necessary, + // and memoize that value to avoid unnecessary context updates. + const overriddenContextValue = useMemo(() => { + if (didStoreComeFromProps) { + // This component is directly subscribed to a store from props. + // We don't want descendants reading from this store - pass down whatever + // the existing context value is from the nearest connected ancestor. + return contextValue! + } + + // Otherwise, put this component's subscription instance into context, so that + // connected descendants won't update until after this component is done + return { + ...contextValue, + subscription, + } as ReactReduxContextValue + }, [didStoreComeFromProps, contextValue, subscription]) + + // Set up refs to coordinate values between the subscription effect and the render logic + const lastChildProps = useRef() + const lastWrapperProps = useRef(wrapperProps) + const childPropsFromStoreUpdate = useRef() + const renderIsScheduled = useRef(false) + const isProcessingDispatch = useRef(false) + const isMounted = useRef(false) + + const latestSubscriptionCallbackError = useRef() + + useIsomorphicLayoutEffect(() => { + isMounted.current = true + return () => { + isMounted.current = false + } + }, []) + + const actualChildPropsSelector = usePureOnlyMemo(() => { + const selector = () => { + // Tricky logic here: + // - This render may have been triggered by a Redux store update that produced new child props + // - However, we may have gotten new wrapper props after that + // If we have new child props, and the same wrapper props, we know we should use the new child props as-is. + // But, if we have new wrapper props, those might change the child props, so we have to recalculate things. + // So, we'll use the child props from store update only if the wrapper props are the same as last time. + if ( + childPropsFromStoreUpdate.current && + wrapperProps === lastWrapperProps.current + ) { + return childPropsFromStoreUpdate.current + } + + // TODO We're reading the store directly in render() here. Bad idea? + // This will likely cause Bad Things (TM) to happen in Concurrent Mode. + // Note that we do this because on renders _not_ caused by store updates, we need the latest store state + // to determine what the child props should be. + return childPropsSelector(store.getState(), wrapperProps) + } + return selector + }, [store, wrapperProps]) + + // We need this to execute synchronously every time we re-render. However, React warns + // about useLayoutEffect in SSR, so we try to detect environment and fall back to + // just useEffect instead to avoid the warning, since neither will run anyway. + + const subscribeForReact = useMemo(() => { + const subscribe = (reactListener: () => void) => { + if (!subscription) { + return () => {} + } + + return subscribeUpdates( + shouldHandleStateChanges, + store, + subscription, + // @ts-ignore + childPropsSelector, + lastWrapperProps, + lastChildProps, + renderIsScheduled, + isMounted, + childPropsFromStoreUpdate, + notifyNestedSubs, + reactListener + ) + } + + return subscribe + }, [subscription]) + + useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [ + lastWrapperProps, + lastChildProps, + renderIsScheduled, + wrapperProps, + childPropsFromStoreUpdate, + notifyNestedSubs, + ]) + + let actualChildProps: unknown + + try { + actualChildProps = useSyncExternalStore( + subscribeForReact, + actualChildPropsSelector, + // TODO Need a real getServerSnapshot here + actualChildPropsSelector + ) + } 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(() => { + latestSubscriptionCallbackError.current = undefined + childPropsFromStoreUpdate.current = undefined + lastChildProps.current = actualChildProps + }) + + // Now that all that's done, we can finally try to actually render the child component. + // We memoize the elements for the rendered child component as an optimization. + const renderedWrappedComponent = useMemo(() => { + return ( + // @ts-ignore + + ) + }, [reactReduxForwardedRef, WrappedComponent, actualChildProps]) + + // If React sees the exact same element reference as last time, it bails out of re-rendering + // that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate. + const renderedChild = useMemo(() => { + if (shouldHandleStateChanges) { + // If this component is subscribed to store updates, we need to pass its own + // subscription instance down to our descendants. That means rendering the same + // Context instance, and putting a different value into the context. + return ( + + {renderedWrappedComponent} + + ) + } + + return renderedWrappedComponent + }, [ContextToUse, renderedWrappedComponent, overriddenContextValue]) + + return renderedChild + } + + // If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed. + const _Connect = React.memo(ConnectFunction) + + type ConnectedWrapperComponent = typeof _Connect & { + WrappedComponent: typeof WrappedComponent + } + + // Add a hacky cast to get the right output type + const Connect = _Connect as unknown as ConnectedComponent< + typeof WrappedComponent, + WrappedComponentProps + > + Connect.WrappedComponent = WrappedComponent + Connect.displayName = ConnectFunction.displayName = displayName + + if (forwardRef) { + const _forwarded = React.forwardRef(function forwardConnectRef( + props, + ref + ) { + // @ts-ignore + return + }) + + const forwarded = _forwarded as ConnectedWrapperComponent + forwarded.displayName = displayName + forwarded.WrappedComponent = WrappedComponent + return hoistStatics(forwarded, WrappedComponent) + } + + return hoistStatics(Connect, WrappedComponent) + } + + return wrapWithConnect +} + +export default connect diff --git a/.yalc/react-redux/src/connect/mapDispatchToProps.ts b/.yalc/react-redux/src/connect/mapDispatchToProps.ts new file mode 100644 index 0000000..aee5878 --- /dev/null +++ b/.yalc/react-redux/src/connect/mapDispatchToProps.ts @@ -0,0 +1,36 @@ +import { ActionCreatorsMapObject, Dispatch } from 'redux' +import { FixTypeLater } from '../types' +import bindActionCreators from '../utils/bindActionCreators' +import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps' + +export function whenMapDispatchToPropsIsFunction( + mapDispatchToProps: ActionCreatorsMapObject | FixTypeLater +) { + return typeof mapDispatchToProps === 'function' + ? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps') + : undefined +} + +export function whenMapDispatchToPropsIsMissing(mapDispatchToProps: undefined) { + return !mapDispatchToProps + ? wrapMapToPropsConstant((dispatch: Dispatch) => ({ + dispatch, + })) + : undefined +} + +export function whenMapDispatchToPropsIsObject( + mapDispatchToProps: ActionCreatorsMapObject +) { + return mapDispatchToProps && typeof mapDispatchToProps === 'object' + ? wrapMapToPropsConstant((dispatch: Dispatch) => + bindActionCreators(mapDispatchToProps, dispatch) + ) + : undefined +} + +export default [ + whenMapDispatchToPropsIsFunction, + whenMapDispatchToPropsIsMissing, + whenMapDispatchToPropsIsObject, +] diff --git a/.yalc/react-redux/src/connect/mapStateToProps.ts b/.yalc/react-redux/src/connect/mapStateToProps.ts new file mode 100644 index 0000000..2dcd7e7 --- /dev/null +++ b/.yalc/react-redux/src/connect/mapStateToProps.ts @@ -0,0 +1,17 @@ +import { + MapToProps, + wrapMapToPropsConstant, + wrapMapToPropsFunc, +} from './wrapMapToProps' + +export function whenMapStateToPropsIsFunction(mapStateToProps?: MapToProps) { + return typeof mapStateToProps === 'function' + ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps') + : undefined +} + +export function whenMapStateToPropsIsMissing(mapStateToProps?: MapToProps) { + return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined +} + +export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing] diff --git a/.yalc/react-redux/src/connect/mergeProps.ts b/.yalc/react-redux/src/connect/mergeProps.ts new file mode 100644 index 0000000..d94fdc3 --- /dev/null +++ b/.yalc/react-redux/src/connect/mergeProps.ts @@ -0,0 +1,88 @@ +import { Dispatch } from 'redux' +import verifyPlainObject from '../utils/verifyPlainObject' + +type MergeProps = ( + stateProps: TStateProps, + dispatchProps: TDispatchProps, + ownProps: TOwnProps +) => TMergedProps + +export function defaultMergeProps( + stateProps: TStateProps, + dispatchProps: TDispatchProps, + ownProps: TOwnProps +) { + return { ...ownProps, ...stateProps, ...dispatchProps } +} + +interface InitMergeOptions { + displayName: string + areMergedPropsEqual: (a: any, b: any) => boolean +} + +export function wrapMergePropsFunc< + TStateProps, + TDispatchProps, + TOwnProps, + TMergedProps +>( + mergeProps: MergeProps +): ( + dispatch: Dispatch, + options: InitMergeOptions +) => MergeProps { + return function initMergePropsProxy( + dispatch, + { displayName, areMergedPropsEqual } + ) { + let hasRunOnce = false + let mergedProps: TMergedProps + + return function mergePropsProxy( + stateProps: TStateProps, + dispatchProps: TDispatchProps, + ownProps: TOwnProps + ) { + const nextMergedProps = mergeProps(stateProps, dispatchProps, ownProps) + + if (hasRunOnce) { + if (!areMergedPropsEqual(nextMergedProps, mergedProps)) + mergedProps = nextMergedProps + } else { + hasRunOnce = true + mergedProps = nextMergedProps + + if (process.env.NODE_ENV !== 'production') + verifyPlainObject(mergedProps, displayName, 'mergeProps') + } + + return mergedProps + } + } +} + +export function whenMergePropsIsFunction< + TStateProps, + TDispatchProps, + TOwnProps, + TMergedProps +>( + mergeProps: MergeProps +) { + return typeof mergeProps === 'function' + ? wrapMergePropsFunc(mergeProps) + : undefined +} + +export function whenMergePropsIsOmitted< + TStateProps, + TDispatchProps, + TOwnProps, + TMergedProps +>( + mergeProps?: MergeProps +) { + return !mergeProps ? () => defaultMergeProps : undefined +} + +export default [whenMergePropsIsFunction, whenMergePropsIsOmitted] as const diff --git a/.yalc/react-redux/src/connect/selectorFactory.ts b/.yalc/react-redux/src/connect/selectorFactory.ts new file mode 100644 index 0000000..1dfea16 --- /dev/null +++ b/.yalc/react-redux/src/connect/selectorFactory.ts @@ -0,0 +1,244 @@ +import type { Dispatch, Action } from 'redux' +import verifySubselectors from './verifySubselectors' +import type { DefaultRootState, EqualityFn } from '../types' + +export type SelectorFactory = ( + dispatch: Dispatch, + factoryOptions: TFactoryOptions +) => Selector + +export type Selector = TOwnProps extends + | null + | undefined + ? (state: S) => TProps + : (state: S, ownProps: TOwnProps) => TProps + +export type MapStateToProps< + TStateProps, + TOwnProps, + State = DefaultRootState +> = (state: State, ownProps: TOwnProps) => TStateProps + +export type MapStateToPropsFactory< + TStateProps, + TOwnProps, + State = DefaultRootState +> = ( + initialState: State, + ownProps: TOwnProps +) => MapStateToProps + +export type MapStateToPropsParam< + TStateProps, + TOwnProps, + State = DefaultRootState +> = + | MapStateToPropsFactory + | MapStateToProps + | null + | undefined + +export type MapDispatchToPropsFunction = ( + dispatch: Dispatch, + ownProps: TOwnProps +) => TDispatchProps + +export type MapDispatchToProps = + | MapDispatchToPropsFunction + | TDispatchProps + +export type MapDispatchToPropsFactory = ( + dispatch: Dispatch, + ownProps: TOwnProps +) => MapDispatchToPropsFunction + +export type MapDispatchToPropsParam = + | MapDispatchToPropsFactory + | MapDispatchToProps + +export type MapDispatchToPropsNonObject = + | MapDispatchToPropsFactory + | MapDispatchToPropsFunction + +export type MergeProps = ( + stateProps: TStateProps, + dispatchProps: TDispatchProps, + ownProps: TOwnProps +) => TMergedProps + +interface PureSelectorFactoryComparisonOptions< + TOwnProps, + State = DefaultRootState +> { + areStatesEqual: EqualityFn + areOwnPropsEqual: EqualityFn + areStatePropsEqual: EqualityFn + displayName: string +} + +export function pureFinalPropsSelectorFactory< + TStateProps, + TOwnProps, + TDispatchProps, + TMergedProps, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam & { + dependsOnOwnProps: boolean + }, + mapDispatchToProps: MapDispatchToPropsParam & { + dependsOnOwnProps: boolean + }, + mergeProps: MergeProps, + dispatch: Dispatch, + { + areStatesEqual, + areOwnPropsEqual, + areStatePropsEqual, + }: PureSelectorFactoryComparisonOptions +) { + let hasRunAtLeastOnce = false + let state: State + let ownProps: TOwnProps + let stateProps: TStateProps + let dispatchProps: TDispatchProps + let mergedProps: TMergedProps + + function handleFirstCall(firstState: State, firstOwnProps: TOwnProps) { + state = firstState + ownProps = firstOwnProps + // @ts-ignore + stateProps = mapStateToProps!(state, ownProps) + // @ts-ignore + dispatchProps = mapDispatchToProps!(dispatch, ownProps) + mergedProps = mergeProps(stateProps, dispatchProps, ownProps) + hasRunAtLeastOnce = true + return mergedProps + } + + function handleNewPropsAndNewState() { + // @ts-ignore + stateProps = mapStateToProps!(state, ownProps) + + if (mapDispatchToProps!.dependsOnOwnProps) + // @ts-ignore + dispatchProps = mapDispatchToProps(dispatch, ownProps) + + mergedProps = mergeProps(stateProps, dispatchProps, ownProps) + return mergedProps + } + + function handleNewProps() { + if (mapStateToProps!.dependsOnOwnProps) + // @ts-ignore + stateProps = mapStateToProps!(state, ownProps) + + if (mapDispatchToProps.dependsOnOwnProps) + // @ts-ignore + dispatchProps = mapDispatchToProps(dispatch, ownProps) + + mergedProps = mergeProps(stateProps, dispatchProps, ownProps) + return mergedProps + } + + function handleNewState() { + const nextStateProps = mapStateToProps(state, ownProps) + const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps) + // @ts-ignore + stateProps = nextStateProps + + if (statePropsChanged) + mergedProps = mergeProps(stateProps, dispatchProps, ownProps) + + return mergedProps + } + + function handleSubsequentCalls(nextState: State, nextOwnProps: TOwnProps) { + const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps) + const stateChanged = !areStatesEqual(nextState, state) + state = nextState + ownProps = nextOwnProps + + if (propsChanged && stateChanged) return handleNewPropsAndNewState() + if (propsChanged) return handleNewProps() + if (stateChanged) return handleNewState() + return mergedProps + } + + return function pureFinalPropsSelector( + nextState: State, + nextOwnProps: TOwnProps + ) { + return hasRunAtLeastOnce + ? handleSubsequentCalls(nextState, nextOwnProps) + : handleFirstCall(nextState, nextOwnProps) + } +} + +export interface SelectorFactoryOptions< + TStateProps, + TOwnProps, + TDispatchProps, + TMergedProps, + State = DefaultRootState +> extends PureSelectorFactoryComparisonOptions { + initMapStateToProps: ( + dispatch: Dispatch, + options: PureSelectorFactoryComparisonOptions + ) => MapStateToPropsParam + initMapDispatchToProps: ( + dispatch: Dispatch, + options: PureSelectorFactoryComparisonOptions + ) => MapDispatchToPropsParam + initMergeProps: ( + dispatch: Dispatch, + options: PureSelectorFactoryComparisonOptions + ) => MergeProps +} + +// TODO: Add more comments + +// The selector returned by selectorFactory will memoize its results, +// allowing connect's shouldComponentUpdate to return false if final +// props have not changed. + +export default function finalPropsSelectorFactory< + TStateProps, + TOwnProps, + TDispatchProps, + TMergedProps, + State = DefaultRootState +>( + dispatch: Dispatch, + { + initMapStateToProps, + initMapDispatchToProps, + initMergeProps, + ...options + }: SelectorFactoryOptions< + TStateProps, + TOwnProps, + TDispatchProps, + TMergedProps, + State + > +) { + const mapStateToProps = initMapStateToProps(dispatch, options) + const mapDispatchToProps = initMapDispatchToProps(dispatch, options) + const mergeProps = initMergeProps(dispatch, options) + + if (process.env.NODE_ENV !== 'production') { + verifySubselectors(mapStateToProps, mapDispatchToProps, mergeProps) + } + + const selectorFactory = pureFinalPropsSelectorFactory + + return selectorFactory( + // @ts-ignore + mapStateToProps!, + mapDispatchToProps, + mergeProps, + dispatch, + options + ) +} diff --git a/.yalc/react-redux/src/connect/verifySubselectors.ts b/.yalc/react-redux/src/connect/verifySubselectors.ts new file mode 100644 index 0000000..487cdb1 --- /dev/null +++ b/.yalc/react-redux/src/connect/verifySubselectors.ts @@ -0,0 +1,26 @@ +import warning from '../utils/warning' + +function verify(selector: unknown, methodName: string): void { + if (!selector) { + throw new Error(`Unexpected value for ${methodName} in connect.`) + } else if ( + methodName === 'mapStateToProps' || + methodName === 'mapDispatchToProps' + ) { + if (!Object.prototype.hasOwnProperty.call(selector, 'dependsOnOwnProps')) { + warning( + `The selector for ${methodName} of connect did not specify a value for dependsOnOwnProps.` + ) + } + } +} + +export default function verifySubselectors( + mapStateToProps: unknown, + mapDispatchToProps: unknown, + mergeProps: unknown +): void { + verify(mapStateToProps, 'mapStateToProps') + verify(mapDispatchToProps, 'mapDispatchToProps') + verify(mergeProps, 'mergeProps') +} diff --git a/.yalc/react-redux/src/connect/wrapMapToProps.ts b/.yalc/react-redux/src/connect/wrapMapToProps.ts new file mode 100644 index 0000000..22f4b77 --- /dev/null +++ b/.yalc/react-redux/src/connect/wrapMapToProps.ts @@ -0,0 +1,110 @@ +import { ActionCreatorsMapObject, Dispatch, ActionCreator } from 'redux' + +import { FixTypeLater } from '../types' +import verifyPlainObject from '../utils/verifyPlainObject' + +type AnyState = { [key: string]: any } +type StateOrDispatch = S | Dispatch + +type AnyProps = { [key: string]: any } + +export type MapToProps

= { + // eslint-disable-next-line no-unused-vars + (stateOrDispatch: StateOrDispatch, ownProps?: P): FixTypeLater + dependsOnOwnProps?: boolean +} + +export function wrapMapToPropsConstant( + // * Note: + // It seems that the dispatch argument + // could be a dispatch function in some cases (ex: whenMapDispatchToPropsIsMissing) + // and a state object in some others (ex: whenMapStateToPropsIsMissing) + // eslint-disable-next-line no-unused-vars + getConstant: (dispatch: Dispatch) => + | { + dispatch?: Dispatch + dependsOnOwnProps?: boolean + } + | ActionCreatorsMapObject + | ActionCreator +) { + return function initConstantSelector(dispatch: Dispatch) { + const constant = getConstant(dispatch) + + function constantSelector() { + return constant + } + constantSelector.dependsOnOwnProps = false + return constantSelector + } +} + +// dependsOnOwnProps is used by createMapToPropsProxy to determine whether to pass props as args +// to the mapToProps function being wrapped. It is also used by makePurePropsSelector to determine +// whether mapToProps needs to be invoked when props have changed. +// +// A length of one signals that mapToProps does not depend on props from the parent component. +// A length of zero is assumed to mean mapToProps is getting args via arguments or ...args and +// therefore not reporting its length accurately.. +// TODO Can this get pulled out so that we can subscribe directly to the store if we don't need ownProps? +export function getDependsOnOwnProps(mapToProps: MapToProps) { + return mapToProps.dependsOnOwnProps + ? Boolean(mapToProps.dependsOnOwnProps) + : mapToProps.length !== 1 +} + +// Used by whenMapStateToPropsIsFunction and whenMapDispatchToPropsIsFunction, +// this function wraps mapToProps in a proxy function which does several things: +// +// * Detects whether the mapToProps function being called depends on props, which +// is used by selectorFactory to decide if it should reinvoke on props changes. +// +// * On first call, handles mapToProps if returns another function, and treats that +// new function as the true mapToProps for subsequent calls. +// +// * On first call, verifies the first result is a plain object, in order to warn +// the developer that their mapToProps function is not returning a valid result. +// +export function wrapMapToPropsFunc

( + mapToProps: MapToProps, + methodName: string +) { + return function initProxySelector( + dispatch: Dispatch, + { displayName }: { displayName: string } + ) { + const proxy = function mapToPropsProxy( + stateOrDispatch: StateOrDispatch, + ownProps?: P + ): MapToProps { + return proxy.dependsOnOwnProps + ? proxy.mapToProps(stateOrDispatch, ownProps) + : proxy.mapToProps(stateOrDispatch, undefined) + } + + // allow detectFactoryAndVerify to get ownProps + proxy.dependsOnOwnProps = true + + proxy.mapToProps = function detectFactoryAndVerify( + stateOrDispatch: StateOrDispatch, + ownProps?: P + ): MapToProps { + proxy.mapToProps = mapToProps + proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps) + let props = proxy(stateOrDispatch, ownProps) + + if (typeof props === 'function') { + proxy.mapToProps = props + proxy.dependsOnOwnProps = getDependsOnOwnProps(props) + props = proxy(stateOrDispatch, ownProps) + } + + if (process.env.NODE_ENV !== 'production') + verifyPlainObject(props, displayName, methodName) + + return props + } + + return proxy + } +} diff --git a/.yalc/react-redux/src/exports.ts b/.yalc/react-redux/src/exports.ts new file mode 100644 index 0000000..94fd09b --- /dev/null +++ b/.yalc/react-redux/src/exports.ts @@ -0,0 +1,57 @@ +import Provider from './components/Provider' +import type { ProviderProps } from './components/Provider' +import connect, { ConnectProps, ConnectedProps } from './components/connect' +import type { + SelectorFactory, + Selector, + MapStateToProps, + MapStateToPropsFactory, + MapStateToPropsParam, + MapDispatchToPropsFunction, + MapDispatchToProps, + MapDispatchToPropsFactory, + MapDispatchToPropsParam, + MapDispatchToPropsNonObject, + MergeProps, +} from './connect/selectorFactory' +import { ReactReduxContext } from './components/Context' +import type { ReactReduxContextValue } from './components/Context' + +import { useDispatch, createDispatchHook } from './hooks/useDispatch' +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 { + ProviderProps, + SelectorFactory, + Selector, + MapStateToProps, + MapStateToPropsFactory, + MapStateToPropsParam, + ConnectProps, + ConnectedProps, + MapDispatchToPropsFunction, + MapDispatchToProps, + MapDispatchToPropsFactory, + MapDispatchToPropsParam, + MapDispatchToPropsNonObject, + MergeProps, + ReactReduxContextValue, + Subscription, +} +export { + Provider, + ReactReduxContext, + connect, + useDispatch, + createDispatchHook, + useSelector, + createSelectorHook, + useStore, + createStoreHook, + shallowEqual, +} diff --git a/.yalc/react-redux/src/hooks/useDispatch.ts b/.yalc/react-redux/src/hooks/useDispatch.ts new file mode 100644 index 0000000..08c921b --- /dev/null +++ b/.yalc/react-redux/src/hooks/useDispatch.ts @@ -0,0 +1,41 @@ +import { ReactReduxContext } from '../components/Context' +import { useStore as useDefaultStore, createStoreHook } from './useStore' + +/** + * Hook factory, which creates a `useDispatch` hook bound to a given context. + * + * @param {React.Context} [context=ReactReduxContext] Context passed to your ``. + * @returns {Function} A `useDispatch` hook bound to the specified context. + */ +export function createDispatchHook(context = ReactReduxContext) { + const useStore = + context === ReactReduxContext ? useDefaultStore : createStoreHook(context) + + return function useDispatch() { + const store = useStore() + return store.dispatch + } +} + +/** + * A hook to access the redux `dispatch` function. + * + * @returns {any|function} redux store's `dispatch` function + * + * @example + * + * import React, { useCallback } from 'react' + * import { useDispatch } from 'react-redux' + * + * export const CounterComponent = ({ value }) => { + * const dispatch = useDispatch() + * const increaseCounter = useCallback(() => dispatch({ type: 'increase-counter' }), []) + * return ( + *

+ * {value} + * + *
+ * ) + * } + */ +export const useDispatch = /*#__PURE__*/ createDispatchHook() diff --git a/.yalc/react-redux/src/hooks/useReduxContext.ts b/.yalc/react-redux/src/hooks/useReduxContext.ts new file mode 100644 index 0000000..bce0aa3 --- /dev/null +++ b/.yalc/react-redux/src/hooks/useReduxContext.ts @@ -0,0 +1,31 @@ +import { useContext } from 'react' +import { ReactReduxContext } from '../components/Context' +import type { ReactReduxContextValue } from '../components/Context' + +/** + * A hook to access the value of the `ReactReduxContext`. This is a low-level + * hook that you should usually not need to call directly. + * + * @returns {any} the value of the `ReactReduxContext` + * + * @example + * + * import React from 'react' + * import { useReduxContext } from 'react-redux' + * + * export const CounterComponent = ({ value }) => { + * const { store } = useReduxContext() + * return
{store.getState()}
+ * } + */ +export function useReduxContext(): ReactReduxContextValue | null { + const contextValue = useContext(ReactReduxContext) + + if (process.env.NODE_ENV !== 'production' && !contextValue) { + throw new Error( + 'could not find react-redux context value; please ensure the component is wrapped in a ' + ) + } + + return contextValue +} diff --git a/.yalc/react-redux/src/hooks/useSelector.ts b/.yalc/react-redux/src/hooks/useSelector.ts new file mode 100644 index 0000000..65c0a22 --- /dev/null +++ b/.yalc/react-redux/src/hooks/useSelector.ts @@ -0,0 +1,121 @@ +import { useRef, useMemo, useContext, useDebugValue } from 'react' + +import { useSyncExternalStoreExtra } from 'use-sync-external-store/extra' + +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' + +const refEquality: EqualityFn = (a, b) => a === b + +type TSelector = (state: S) => R + +function useSelectorWithStoreAndSubscription( + selector: TSelector, + equalityFn: EqualityFn, + store: Store, + contextSub: Subscription +): TSelectedState { + const subscribe = useMemo(() => { + const subscription = createSubscription(store, contextSub) + const subscribe = (reactListener: () => void) => { + // React provides its own subscription handler - trigger that on dispatch + subscription.onStateChange = reactListener + subscription.trySubscribe() + + return () => { + subscription.tryUnsubscribe() + + subscription.onStateChange = null + } + } + + return subscribe + }, [store, contextSub]) + + return useSyncExternalStoreExtra( + subscribe, + store.getState, + // TODO Need a server-side snapshot here + store.getState, + selector, + equalityFn + ) +} + +/** + * Hook factory, which creates a `useSelector` hook bound to a given context. + * + * @param {React.Context} [context=ReactReduxContext] Context passed to your ``. + * @returns {Function} A `useSelector` hook bound to the specified context. + */ +export function createSelectorHook( + context = ReactReduxContext +): ( + selector: (state: TState) => Selected, + equalityFn?: EqualityFn +) => Selected { + const useReduxContext = + context === ReactReduxContext + ? useDefaultReduxContext + : () => useContext(context) + + return function useSelector( + selector: (state: TState) => Selected, + equalityFn: EqualityFn = refEquality + ): Selected { + if (process.env.NODE_ENV !== 'production') { + if (!selector) { + throw new Error(`You must pass a selector to useSelector`) + } + if (typeof selector !== 'function') { + throw new Error(`You must pass a function as a selector to useSelector`) + } + if (typeof equalityFn !== 'function') { + throw new Error( + `You must pass a function as an equality function to useSelector` + ) + } + } + const { store, subscription: contextSub } = useReduxContext()! + + const selectedState = useSelectorWithStoreAndSubscription( + selector, + equalityFn, + store, + contextSub + ) + + useDebugValue(selectedState) + + return selectedState + } +} + +/** + * A hook to access the redux store's state. This hook takes a selector function + * as an argument. The selector is called with the store state. + * + * This hook takes an optional equality comparison function as the second parameter + * that allows you to customize the way the selected state is compared to determine + * whether the component needs to be re-rendered. + * + * @param {Function} selector the selector function + * @param {Function=} equalityFn the function that will be used to determine equality + * + * @returns {any} the selected state + * + * @example + * + * import React from 'react' + * import { useSelector } from 'react-redux' + * + * export const CounterComponent = () => { + * const counter = useSelector(state => state.counter) + * return
{counter}
+ * } + */ +export const useSelector = /*#__PURE__*/ createSelectorHook() diff --git a/.yalc/react-redux/src/hooks/useStore.ts b/.yalc/react-redux/src/hooks/useStore.ts new file mode 100644 index 0000000..85fd238 --- /dev/null +++ b/.yalc/react-redux/src/hooks/useStore.ts @@ -0,0 +1,37 @@ +import { useContext } from 'react' +import { ReactReduxContext } from '../components/Context' +import { useReduxContext as useDefaultReduxContext } from './useReduxContext' + +/** + * Hook factory, which creates a `useStore` hook bound to a given context. + * + * @param {React.Context} [context=ReactReduxContext] Context passed to your ``. + * @returns {Function} A `useStore` hook bound to the specified context. + */ +export function createStoreHook(context = ReactReduxContext) { + const useReduxContext = + context === ReactReduxContext + ? useDefaultReduxContext + : () => useContext(context) + return function useStore() { + const { store } = useReduxContext()! + return store + } +} + +/** + * A hook to access the redux store. + * + * @returns {any} the redux store + * + * @example + * + * import React from 'react' + * import { useStore } from 'react-redux' + * + * export const ExampleComponent = () => { + * const store = useStore() + * return
{store.getState()}
+ * } + */ +export const useStore = /*#__PURE__*/ createStoreHook() diff --git a/.yalc/react-redux/src/index.ts b/.yalc/react-redux/src/index.ts new file mode 100644 index 0000000..f37b19b --- /dev/null +++ b/.yalc/react-redux/src/index.ts @@ -0,0 +1,10 @@ +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 } diff --git a/.yalc/react-redux/src/types.ts b/.yalc/react-redux/src/types.ts new file mode 100644 index 0000000..a291b83 --- /dev/null +++ b/.yalc/react-redux/src/types.ts @@ -0,0 +1,277 @@ +/* eslint-disable no-unused-vars */ +// TODO Ignoring all unused variables for now + +import { + ClassAttributes, + Component, + ComponentClass, + ComponentType, + StatelessComponent, + Context, + NamedExoticComponent, +} from 'react' + +import { Action, ActionCreator, AnyAction, Dispatch, Store } from 'redux' + +// import hoistNonReactStatics = require('hoist-non-react-statics'); +import type { NonReactStatics } from 'hoist-non-react-statics' + +export type FixTypeLater = any + +export type EqualityFn = (a: T | undefined, b: T | undefined) => boolean + +/** + * This interface can be augmented by users to add default types for the root state when + * using `react-redux`. + * Use module augmentation to append your own type definition in a your_custom_type.d.ts file. + * https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation + */ +// tslint:disable-next-line:no-empty-interface +export interface DefaultRootState {} + +export type AnyIfEmpty = keyof T extends never ? any : T +export type RootStateOrAny = AnyIfEmpty + +// Omit taken from https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html +export type Omit = Pick> + +export type DistributiveOmit = T extends unknown + ? Omit + : never + +export interface DispatchProp
{ + dispatch: Dispatch +} + +export type AdvancedComponentDecorator = ( + component: ComponentType +) => ComponentType + +/** + * A property P will be present if: + * - it is present in DecorationTargetProps + * + * Its value will be dependent on the following conditions + * - if property P is present in InjectedProps and its definition extends the definition + * in DecorationTargetProps, then its definition will be that of DecorationTargetProps[P] + * - if property P is not present in InjectedProps then its definition will be that of + * DecorationTargetProps[P] + * - if property P is present in InjectedProps but does not extend the + * DecorationTargetProps[P] definition, its definition will be that of InjectedProps[P] + */ +export type Matching = { + [P in keyof DecorationTargetProps]: P extends keyof InjectedProps + ? InjectedProps[P] extends DecorationTargetProps[P] + ? DecorationTargetProps[P] + : InjectedProps[P] + : DecorationTargetProps[P] +} + +/** + * a property P will be present if : + * - it is present in both DecorationTargetProps and InjectedProps + * - InjectedProps[P] can satisfy DecorationTargetProps[P] + * ie: decorated component can accept more types than decorator is injecting + * + * For decoration, inject props or ownProps are all optionally + * required by the decorated (right hand side) component. + * But any property required by the decorated component must be satisfied by the injected property. + */ +export type Shared = { + [P in Extract< + keyof InjectedProps, + keyof DecorationTargetProps + >]?: InjectedProps[P] extends DecorationTargetProps[P] + ? DecorationTargetProps[P] + : never +} + +// Infers prop type from component C +export type GetProps = C extends ComponentType + ? C extends ComponentClass

+ ? ClassAttributes> & P + : P + : never + +// Applies LibraryManagedAttributes (proper handling of defaultProps +// and propTypes), as well as defines WrappedComponent. +export type ConnectedComponent< + C extends ComponentType, + P +> = ComponentType

& + NonReactStatics & { + WrappedComponent: C + } + +// Injects props and removes them from the prop requirements. +// Will not pass through the injected props if they are passed in during +// render. Also adds new prop requirements from TNeedsProps. +// Uses distributive omit to preserve discriminated unions part of original prop type +export type InferableComponentEnhancerWithProps = < + C extends ComponentType>> +>( + component: C +) => ConnectedComponent< + C, + DistributiveOmit, keyof Shared>> & + TNeedsProps +> + +// Injects props and removes them from the prop requirements. +// Will not pass through the injected props if they are passed in during +// render. +export type InferableComponentEnhancer = + InferableComponentEnhancerWithProps + +export type InferThunkActionCreatorType< + TActionCreator extends (...args: any[]) => any +> = TActionCreator extends ( + ...args: infer TParams +) => (...args: any[]) => infer TReturn + ? (...args: TParams) => TReturn + : TActionCreator + +export type HandleThunkActionCreator = TActionCreator extends ( + ...args: any[] +) => any + ? InferThunkActionCreatorType + : TActionCreator + +// redux-thunk middleware returns thunk's return value from dispatch call +// https://github.com/reduxjs/redux-thunk#composition +export type ResolveThunks = TDispatchProps extends { + [key: string]: any +} + ? { + [C in keyof TDispatchProps]: HandleThunkActionCreator + } + : TDispatchProps + +// the conditional type is to support TypeScript 3.0, which does not support mapping over tuples and arrays; +// once the typings are updated to at least TypeScript 3.1, a simple mapped type can replace this mess +export type ResolveArrayThunks> = + TDispatchProps extends [ + infer A1, + infer A2, + infer A3, + infer A4, + infer A5, + infer A6, + infer A7, + infer A8, + infer A9 + ] + ? [ + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator + ] + : TDispatchProps extends [ + infer A1, + infer A2, + infer A3, + infer A4, + infer A5, + infer A6, + infer A7, + infer A8 + ] + ? [ + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator + ] + : TDispatchProps extends [ + infer A1, + infer A2, + infer A3, + infer A4, + infer A5, + infer A6, + infer A7 + ] + ? [ + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator + ] + : TDispatchProps extends [ + infer A1, + infer A2, + infer A3, + infer A4, + infer A5, + infer A6 + ] + ? [ + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator + ] + : TDispatchProps extends [infer A1, infer A2, infer A3, infer A4, infer A5] + ? [ + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator + ] + : TDispatchProps extends [infer A1, infer A2, infer A3, infer A4] + ? [ + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator + ] + : TDispatchProps extends [infer A1, infer A2, infer A3] + ? [ + HandleThunkActionCreator, + HandleThunkActionCreator, + HandleThunkActionCreator + ] + : TDispatchProps extends [infer A1, infer A2] + ? [HandleThunkActionCreator, HandleThunkActionCreator] + : TDispatchProps extends [infer A1] + ? [HandleThunkActionCreator] + : TDispatchProps extends Array + ? Array> + : TDispatchProps extends ReadonlyArray + ? ReadonlyArray> + : never + +/** + * This interface allows you to easily create a hook that is properly typed for your + * store's root state. + * + * @example + * + * interface RootState { + * property: string; + * } + * + * const useTypedSelector: TypedUseSelectorHook = useSelector; + */ +export interface TypedUseSelectorHook { + ( + selector: (state: TState) => TSelected, + equalityFn?: EqualityFn + ): TSelected +} diff --git a/.yalc/react-redux/src/utils/Subscription.ts b/.yalc/react-redux/src/utils/Subscription.ts new file mode 100644 index 0000000..0ea00c4 --- /dev/null +++ b/.yalc/react-redux/src/utils/Subscription.ts @@ -0,0 +1,151 @@ +import { getBatch } from './batch' + +// encapsulates the subscription logic for connecting a component to the redux store, as +// well as nesting subscriptions of descendant components, so that we can ensure the +// ancestor components re-render before descendants + +type VoidFunc = () => void + +type Listener = { + callback: VoidFunc + next: Listener | null + prev: Listener | null +} + +function createListenerCollection() { + const batch = getBatch() + let first: Listener | null = null + let last: Listener | null = null + + return { + clear() { + first = null + last = null + }, + + notify() { + batch(() => { + let listener = first + while (listener) { + listener.callback() + listener = listener.next + } + }) + }, + + get() { + let listeners = [] + let listener = first + while (listener) { + listeners.push(listener) + listener = listener.next + } + return listeners + }, + + subscribe(callback: () => void) { + let isSubscribed = true + + let listener: Listener = (last = { + callback, + next: null, + prev: last, + }) + + if (listener.prev) { + listener.prev.next = listener + } else { + first = listener + } + + return function unsubscribe() { + if (!isSubscribed || first === null) return + isSubscribed = false + + if (listener.next) { + listener.next.prev = listener.prev + } else { + last = listener.prev + } + if (listener.prev) { + listener.prev.next = listener.next + } else { + first = listener.next + } + } + }, + } +} + +type ListenerCollection = ReturnType + +export interface Subscription { + addNestedSub: (listener: VoidFunc) => VoidFunc + notifyNestedSubs: VoidFunc + handleChangeWrapper: VoidFunc + isSubscribed: () => boolean + onStateChange?: VoidFunc | null + trySubscribe: VoidFunc + tryUnsubscribe: VoidFunc + getListeners: () => ListenerCollection +} + +const nullListeners = { + notify() {}, + get: () => [], +} as unknown as ListenerCollection + +export function createSubscription(store: any, parentSub?: Subscription) { + let unsubscribe: VoidFunc | undefined + let listeners: ListenerCollection = nullListeners + + function addNestedSub(listener: () => void) { + trySubscribe() + return listeners.subscribe(listener) + } + + function notifyNestedSubs() { + listeners.notify() + } + + function handleChangeWrapper() { + if (subscription.onStateChange) { + subscription.onStateChange() + } + } + + function isSubscribed() { + return Boolean(unsubscribe) + } + + function trySubscribe() { + if (!unsubscribe) { + unsubscribe = parentSub + ? parentSub.addNestedSub(handleChangeWrapper) + : store.subscribe(handleChangeWrapper) + + listeners = createListenerCollection() + } + } + + function tryUnsubscribe() { + if (unsubscribe) { + unsubscribe() + unsubscribe = undefined + listeners.clear() + listeners = nullListeners + } + } + + const subscription: Subscription = { + addNestedSub, + notifyNestedSubs, + handleChangeWrapper, + isSubscribed, + trySubscribe, + tryUnsubscribe, + getListeners: () => listeners, + } + + return subscription +} diff --git a/.yalc/react-redux/src/utils/batch.ts b/.yalc/react-redux/src/utils/batch.ts new file mode 100644 index 0000000..2d116ea --- /dev/null +++ b/.yalc/react-redux/src/utils/batch.ts @@ -0,0 +1,13 @@ +// Default to a dummy "batch" implementation that just runs the callback +function defaultNoopBatch(callback: () => void) { + callback() +} + +let batch = defaultNoopBatch + +// Allow injecting another batching function later +export const setBatch = (newBatch: typeof defaultNoopBatch) => + (batch = newBatch) + +// Supply a getter just to skip dealing with ESM bindings +export const getBatch = () => batch diff --git a/.yalc/react-redux/src/utils/bindActionCreators.ts b/.yalc/react-redux/src/utils/bindActionCreators.ts new file mode 100644 index 0000000..fa50581 --- /dev/null +++ b/.yalc/react-redux/src/utils/bindActionCreators.ts @@ -0,0 +1,16 @@ +import { ActionCreatorsMapObject, Dispatch } from 'redux' + +export default function bindActionCreators( + actionCreators: ActionCreatorsMapObject, + dispatch: Dispatch +): ActionCreatorsMapObject { + const boundActionCreators: ActionCreatorsMapObject = {} + + for (const key in actionCreators) { + const actionCreator = actionCreators[key] + if (typeof actionCreator === 'function') { + boundActionCreators[key] = (...args) => dispatch(actionCreator(...args)) + } + } + return boundActionCreators +} diff --git a/.yalc/react-redux/src/utils/isPlainObject.ts b/.yalc/react-redux/src/utils/isPlainObject.ts new file mode 100644 index 0000000..2157ea0 --- /dev/null +++ b/.yalc/react-redux/src/utils/isPlainObject.ts @@ -0,0 +1,17 @@ +/** + * @param {any} obj The object to inspect. + * @returns {boolean} True if the argument appears to be a plain object. + */ +export default function isPlainObject(obj: unknown) { + if (typeof obj !== 'object' || obj === null) return false + + let proto = Object.getPrototypeOf(obj) + if (proto === null) return true + + let baseProto = proto + while (Object.getPrototypeOf(baseProto) !== null) { + baseProto = Object.getPrototypeOf(baseProto) + } + + return proto === baseProto +} diff --git a/.yalc/react-redux/src/utils/reactBatchedUpdates.native.ts b/.yalc/react-redux/src/utils/reactBatchedUpdates.native.ts new file mode 100644 index 0000000..a92cd67 --- /dev/null +++ b/.yalc/react-redux/src/utils/reactBatchedUpdates.native.ts @@ -0,0 +1,5 @@ +/* eslint-disable import/namespace */ +/* eslint-disable import/named */ +import { unstable_batchedUpdates } from 'react-native' + +export { unstable_batchedUpdates } diff --git a/.yalc/react-redux/src/utils/reactBatchedUpdates.ts b/.yalc/react-redux/src/utils/reactBatchedUpdates.ts new file mode 100644 index 0000000..0fca6d8 --- /dev/null +++ b/.yalc/react-redux/src/utils/reactBatchedUpdates.ts @@ -0,0 +1 @@ +export { unstable_batchedUpdates } from 'react-dom' diff --git a/.yalc/react-redux/src/utils/shallowEqual.ts b/.yalc/react-redux/src/utils/shallowEqual.ts new file mode 100644 index 0000000..e50c6be --- /dev/null +++ b/.yalc/react-redux/src/utils/shallowEqual.ts @@ -0,0 +1,36 @@ +function is(x: unknown, y: unknown) { + if (x === y) { + return x !== 0 || y !== 0 || 1 / x === 1 / y + } else { + return x !== x && y !== y + } +} + +export default function shallowEqual(objA: any, objB: any) { + if (is(objA, objB)) return true + + if ( + typeof objA !== 'object' || + objA === null || + typeof objB !== 'object' || + objB === null + ) { + return false + } + + const keysA = Object.keys(objA) + const keysB = Object.keys(objB) + + if (keysA.length !== keysB.length) return false + + for (let i = 0; i < keysA.length; i++) { + if ( + !Object.prototype.hasOwnProperty.call(objB, keysA[i]) || + !is(objA[keysA[i]], objB[keysA[i]]) + ) { + return false + } + } + + return true +} diff --git a/.yalc/react-redux/src/utils/useIsomorphicLayoutEffect.native.ts b/.yalc/react-redux/src/utils/useIsomorphicLayoutEffect.native.ts new file mode 100644 index 0000000..e80393a --- /dev/null +++ b/.yalc/react-redux/src/utils/useIsomorphicLayoutEffect.native.ts @@ -0,0 +1,5 @@ +import { useLayoutEffect } from 'react' + +// Under React Native, we know that we always want to use useLayoutEffect + +export const useIsomorphicLayoutEffect = useLayoutEffect diff --git a/.yalc/react-redux/src/utils/useIsomorphicLayoutEffect.ts b/.yalc/react-redux/src/utils/useIsomorphicLayoutEffect.ts new file mode 100644 index 0000000..0e87d6e --- /dev/null +++ b/.yalc/react-redux/src/utils/useIsomorphicLayoutEffect.ts @@ -0,0 +1,17 @@ +import { useEffect, useLayoutEffect } from 'react' + +// React currently throws a warning when using useLayoutEffect on the server. +// To get around it, we can conditionally useEffect on the server (no-op) and +// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store +// subscription callback always has the selector from the latest render commit +// available, otherwise a store update may happen between render and the effect, +// which may cause missed updates; we also must ensure the store subscription +// is created synchronously, otherwise a store update may occur before the +// subscription is created and an inconsistent state may be observed + +export const useIsomorphicLayoutEffect = + typeof window !== 'undefined' && + typeof window.document !== 'undefined' && + typeof window.document.createElement !== 'undefined' + ? useLayoutEffect + : useEffect diff --git a/.yalc/react-redux/src/utils/verifyPlainObject.ts b/.yalc/react-redux/src/utils/verifyPlainObject.ts new file mode 100644 index 0000000..212d511 --- /dev/null +++ b/.yalc/react-redux/src/utils/verifyPlainObject.ts @@ -0,0 +1,14 @@ +import isPlainObject from './isPlainObject' +import warning from './warning' + +export default function verifyPlainObject( + value: unknown, + displayName: string, + methodName: string +) { + if (!isPlainObject(value)) { + warning( + `${methodName}() in ${displayName} must return a plain object. Instead received ${value}.` + ) + } +} diff --git a/.yalc/react-redux/src/utils/warning.ts b/.yalc/react-redux/src/utils/warning.ts new file mode 100644 index 0000000..92d3d63 --- /dev/null +++ b/.yalc/react-redux/src/utils/warning.ts @@ -0,0 +1,21 @@ +/** + * Prints a warning in the console if it exists. + * + * @param {String} message The warning message. + * @returns {void} + */ +export default function warning(message: string) { + /* eslint-disable no-console */ + if (typeof console !== 'undefined' && typeof console.error === 'function') { + console.error(message) + } + /* eslint-enable no-console */ + try { + // This error was thrown as a convenience so that if you enable + // "break on all exceptions" in your console, + // it would pause the execution at this line. + throw new Error(message) + /* eslint-disable no-empty */ + } catch (e) {} + /* eslint-enable no-empty */ +} diff --git a/.yalc/react-redux/yalc.sig b/.yalc/react-redux/yalc.sig new file mode 100644 index 0000000..f2b4a4d --- /dev/null +++ b/.yalc/react-redux/yalc.sig @@ -0,0 +1 @@ +ff0db988ba4013dfd771fdeeafc7f593 \ No newline at end of file diff --git a/package.json b/package.json index 071a575..4d8a110 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,9 @@ "description": "benchmark suite and runner for react-redux", "main": "index.js", "scripts": { - "initialize": "node ./setupRuns.js", "start": "node ./runBenchmarks.js", - "test": "jest", "format": "prettier --write \"sources/**/src/*.{js,jsx}\" *.js", - "esbuild-test": "node scripts/cli.js" + "build": "node scripts/cli.js" }, "repository": { "type": "git", @@ -38,17 +36,19 @@ "glob": "^7.1.3", "performance-mark-metadata": "^1.0.3", "puppeteer": "^1.7.0", - "react": "0.0.0-experimental-50263d327-20210914", - "react-dom": "0.0.0-experimental-50263d327-20210914", - "react-redux": "^7.2.5", + "react": "0.0.0-experimental-5a0607278-20210922", + "react-dom": "0.0.0-experimental-5a0607278-20210922", + "react-redux": "file:.yalc/react-redux", "react-redux-5.1.2": "npm:react-redux@5.1.2", "react-redux-6.0.1": "npm:react-redux@6.0.1", "react-redux-7.2.5": "npm:react-redux@7.2.5", + "react-redux-8.0.0-alpha.0": "file:.yalc/react-redux", "recursive-copy": "^2.0.9", "redux": "^4.1.1", "reselect": "^4.0.0", "seedrandom": "^3.0.5", - "tracealyzer": "^0.9.3" + "tracealyzer": "^0.9.3", + "use-sync-external-store": "0.0.0-experimental-5a0607278-20210922" }, "devDependencies": { "@types/fs-extra": "^9.0.12", diff --git a/runBenchmarks.js b/runBenchmarks.js index 67696ae..eb0ebd2 100644 --- a/runBenchmarks.js +++ b/runBenchmarks.js @@ -3,6 +3,7 @@ const path = require('path') const puppeteer = require('puppeteer') +const fs = require('fs') const Table = require('cli-table2') const _ = require('lodash') const glob = require('glob') @@ -171,8 +172,8 @@ function calculateBenchmarkStats( async function runBenchmarks({ scenarios, versions, length, trace }) { console.log('Scenarios: ', scenarios) - - const server = await serverUtils.runServer(9999, path.resolve('dist')) + const distFolder = path.resolve('dist') + const server = await serverUtils.runServer(9999, distFolder) for (let scenario of scenarios) { const versionPerfEntries = {} @@ -185,6 +186,15 @@ async function runBenchmarks({ scenarios, versions, length, trace }) { //headless: false }) + const folderPath = path.join(distFolder, version, scenario) + + if (!fs.existsSync(folderPath)) { + console.log( + `Scenario ${scenario} does not exist for version ${version}, skipping` + ) + continue + } + const URL = `http://localhost:9999/${version}/${scenario}` try { console.log(` Checking max FPS... (${length} seconds)`) diff --git a/scripts/build.ts b/scripts/build.ts index 13fdec7..975bce2 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -7,9 +7,18 @@ import ts from 'typescript' import rimraf from 'rimraf' import alias from 'esbuild-plugin-alias' import glob from 'glob' +import yargs from 'yargs' +import semver from 'semver' const pkg = require(path.join(process.cwd(), 'package.json')) +const args = yargs(process.argv.slice(2)).options('concurrent', { + alias: 'c', + describe: "Use React 18's `createRoot` rendering", + type: 'boolean', + default: false, +}) + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) const readFolderNames = (searchDir) => { @@ -28,11 +37,25 @@ const outputDir = path.join(__dirname, '../dist') interface BuildOptions { scenarioName: string reactReduxVersion: string + concurrent: boolean } async function bundle(options: BuildOptions) { - const { scenarioName = 'counter', reactReduxVersion = '7.2.5' } = - options ?? {} + const { + scenarioName = 'counter', + reactReduxVersion = '7.2.5', + concurrent = false, + } = options ?? {} + + if (scenarioName.includes('hooks')) { + if (semver.lt(reactReduxVersion, '7.1.0')) { + console.log( + `Skipping build for scenario ${scenarioName}, version ${reactReduxVersion}` + ) + // Hooks didn't exist until React-Redux 7.1, skip this build + return + } + } const outputFolder = path.join('dist', reactReduxVersion, scenarioName) fs.ensureDirSync(outputFolder) @@ -40,6 +63,21 @@ async function bundle(options: BuildOptions) { const entryPoint = path.join('src/scenarios', scenarioName, 'index.tsx') + let reactReduxPackageVersion = `react-redux-${reactReduxVersion}` + + const depVersion = pkg.dependencies[reactReduxPackageVersion] + + let resolvedReactReduxPath: string + + if (depVersion?.startsWith('file:')) { + resolvedReactReduxPath = path.join( + path.resolve(depVersion.replace('file:', '')), + 'es/index.js' + ) + } else { + resolvedReactReduxPath = require.resolve(reactReduxPackageVersion) + } + const result = await build({ entryPoints: [entryPoint], outfile: outputFilePath, @@ -58,12 +96,12 @@ async function bundle(options: BuildOptions) { 'process.env.NODE_ENV': JSON.stringify('production'), 'process.env.NAME': JSON.stringify(scenarioName), 'process.env.RR_VERSION': JSON.stringify(reactReduxVersion), - 'process.env.CONCURRENT_RENDERING': JSON.stringify(false), + 'process.env.CONCURRENT_RENDERING': JSON.stringify(concurrent), }, plugins: [ alias({ 'react-dom': require.resolve('react-dom/profiling'), - 'react-redux': require.resolve(`react-redux-${reactReduxVersion}`), + 'react-redux': resolvedReactReduxPath, }), ], }) @@ -78,9 +116,10 @@ async function bundle(options: BuildOptions) { interface MainArgs { scenarios: string[] versions: string[] + concurrent: boolean } -async function main({ scenarios, versions }: MainArgs) { +async function main({ scenarios, versions, concurrent }: MainArgs) { rimraf.sync(outputDir) // Dist folder will be removed by rimraf beforehand so TSC can generate typedefs fs.ensureDirSync(outputDir) @@ -95,6 +134,7 @@ async function main({ scenarios, versions }: MainArgs) { bundle({ scenarioName, reactReduxVersion: version, + concurrent, }) ) await Promise.all(bundlePromises) @@ -104,7 +144,11 @@ async function main({ scenarios, versions }: MainArgs) { console.log('Available versions: ', availableVersions) console.log('Available scenarios: ', allScenarios) +// @ts-ignore +const { concurrent } = args.argv + main({ scenarios: allScenarios, versions: availableVersions, + concurrent, }) diff --git a/src/common/index.tsx b/src/common/index.tsx index fadeaa3..d037105 100644 --- a/src/common/index.tsx +++ b/src/common/index.tsx @@ -46,6 +46,7 @@ export const renderApp = (App: React.ComponentType, store: Store) => { const domNode = document.getElementById('root')! if (process.env.CONCURRENT_RENDERING) { + console.log('Using React 18 `createRoot`...') const domNode = document.getElementById('root')! const root = ReactDOM.createRoot(domNode) root.render(rootElements) diff --git a/src/scenarios/deeptree-nested-hooks/App.jsx b/src/scenarios/deeptree-nested-hooks/App.jsx new file mode 100644 index 0000000..fdcc500 --- /dev/null +++ b/src/scenarios/deeptree-nested-hooks/App.jsx @@ -0,0 +1,79 @@ +import React from "react"; +import { connect } from "react-redux"; + +import Slice from "./Slice"; +import * as c from "./constants"; +import { incrementMany, incrementRandomCounter } from "./counters"; +import { appendRandomCharacter, appendRandomCharToMany } from "./strings"; + +let slices; + +const mapState = state => { + if (!slices) { + slices = Array.from({ length: c.NUMBER_OF_SLICES }).map( + (dummy, idx) => idx + ); + //slices.sort(); + } + + return { slices }; +}; + +function doUpdateMany(mod) { + return incrementMany({ mod }); +} + +const mapDispatch = { + incrementRandomCounter, + incrementFifth: () => doUpdateMany(5), + incrementThird: () => doUpdateMany(3), + appendRandomCharacter, + appendMany: () => appendRandomCharToMany(4) +}; + +class App extends React.Component { + render() { + return ( +

+
+ + + + + +
+
+ {this.props.slices.map((slice, idx) => { + return ( +
+ +
+ ); + })} +
+
+ ); + } +} +App.displayName = "App"; + +export default connect( + mapState, + mapDispatch +)(App); diff --git a/src/scenarios/deeptree-nested-hooks/Slice.jsx b/src/scenarios/deeptree-nested-hooks/Slice.jsx new file mode 100644 index 0000000..e5b997e --- /dev/null +++ b/src/scenarios/deeptree-nested-hooks/Slice.jsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react' +import { shallowEqual, useSelector, useDispatch } from 'react-redux' + +import { initialize, createStringId } from './strings' +import { TEXT_INPUT_MOD } from './constants' + +const Counter = ({ idx }) => { + const value = useSelector((state) => state.counters[idx]) + return
Value: {value}
+} + +Counter.displayName = 'Counter' + +const TextDisplay = ({ idx, inputId, children }) => { + const dispatch = useDispatch() + const stringId = createStringId(idx, inputId) + const text = useSelector((state) => { + //`${idx}-${remainingDepth}`; + const text = state.strings[stringId] || 'unknown' + return text + }) + + useEffect(() => { + dispatch(initialize({ stringId })) + }, []) + + return ( +
+ Text {stringId}:
+