adds integrations with bookish-server, the books API and the google books API, adds functionality for library, searching for books, and displaying renters registered in the system
This commit is contained in:
+9
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
export const ReactReduxContext = /*#__PURE__*/ React.createContext(null)
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
ReactReduxContext.displayName = 'ReactRedux'
|
||||
}
|
||||
|
||||
export default ReactReduxContext
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import React, { useMemo, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ReactReduxContext } from './Context'
|
||||
import Subscription from '../utils/Subscription'
|
||||
|
||||
function Provider({ store, context, children }) {
|
||||
const contextValue = useMemo(() => {
|
||||
const subscription = new Subscription(store)
|
||||
subscription.onStateChange = subscription.notifyNestedSubs
|
||||
return {
|
||||
store,
|
||||
subscription
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const previousState = useMemo(() => store.getState(), [store])
|
||||
|
||||
useEffect(() => {
|
||||
const { subscription } = contextValue
|
||||
subscription.trySubscribe()
|
||||
|
||||
if (previousState !== store.getState()) {
|
||||
subscription.notifyNestedSubs()
|
||||
}
|
||||
return () => {
|
||||
subscription.tryUnsubscribe()
|
||||
subscription.onStateChange = null
|
||||
}
|
||||
}, [contextValue, previousState])
|
||||
|
||||
const Context = context || ReactReduxContext
|
||||
|
||||
return <Context.Provider value={contextValue}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Provider.propTypes = {
|
||||
store: PropTypes.shape({
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
getState: PropTypes.func.isRequired
|
||||
}),
|
||||
context: PropTypes.object,
|
||||
children: PropTypes.any
|
||||
}
|
||||
}
|
||||
|
||||
export default Provider
|
||||
+485
@@ -0,0 +1,485 @@
|
||||
import hoistStatics from 'hoist-non-react-statics'
|
||||
import React, { useContext, useMemo, useRef, useReducer } from 'react'
|
||||
import { isValidElementType, isContextConsumer } from 'react-is'
|
||||
import Subscription from '../utils/Subscription'
|
||||
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
|
||||
|
||||
import { ReactReduxContext } from './Context'
|
||||
|
||||
// Define some constant arrays just to avoid re-creating these
|
||||
const EMPTY_ARRAY = []
|
||||
const NO_SUBSCRIPTION_ARRAY = [null, null]
|
||||
|
||||
const stringifyComponent = Comp => {
|
||||
try {
|
||||
return JSON.stringify(Comp)
|
||||
} catch (err) {
|
||||
return String(Comp)
|
||||
}
|
||||
}
|
||||
|
||||
function storeStateUpdatesReducer(state, action) {
|
||||
const [, updateCount] = state
|
||||
return [action.payload, updateCount + 1]
|
||||
}
|
||||
|
||||
function useIsomorphicLayoutEffectWithArgs(
|
||||
effectFunc,
|
||||
effectArgs,
|
||||
dependencies
|
||||
) {
|
||||
useIsomorphicLayoutEffect(() => effectFunc(...effectArgs), dependencies)
|
||||
}
|
||||
|
||||
function captureWrapperProps(
|
||||
lastWrapperProps,
|
||||
lastChildProps,
|
||||
renderIsScheduled,
|
||||
wrapperProps,
|
||||
actualChildProps,
|
||||
childPropsFromStoreUpdate,
|
||||
notifyNestedSubs
|
||||
) {
|
||||
// We want to capture the wrapper props and child props we used for later comparisons
|
||||
lastWrapperProps.current = wrapperProps
|
||||
lastChildProps.current = actualChildProps
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeUpdates(
|
||||
shouldHandleStateChanges,
|
||||
store,
|
||||
subscription,
|
||||
childPropsSelector,
|
||||
lastWrapperProps,
|
||||
lastChildProps,
|
||||
renderIsScheduled,
|
||||
childPropsFromStoreUpdate,
|
||||
notifyNestedSubs,
|
||||
forceComponentUpdateDispatch
|
||||
) {
|
||||
// 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 = null
|
||||
|
||||
// We'll run this callback every time a store subscription update propagates to this component
|
||||
const checkForUpdates = () => {
|
||||
if (didUnsubscribe) {
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
|
||||
forceComponentUpdateDispatch({
|
||||
type: 'STORE_UPDATED',
|
||||
payload: {
|
||||
error
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
const initStateUpdates = () => [null, 0]
|
||||
|
||||
export default function connectAdvanced(
|
||||
/*
|
||||
selectorFactory is a func that is responsible for returning the selector function used to
|
||||
compute new props from state, props, and dispatch. For example:
|
||||
|
||||
export default connectAdvanced((dispatch, options) => (state, props) => ({
|
||||
thing: state.things[props.thingId],
|
||||
saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
|
||||
}))(YourComponent)
|
||||
|
||||
Access to dispatch is provided to the factory so selectorFactories can bind actionCreators
|
||||
outside of their selector as an optimization. Options passed to connectAdvanced are passed to
|
||||
the selectorFactory, along with displayName and WrappedComponent, as the second argument.
|
||||
|
||||
Note that selectorFactory is responsible for all caching/memoization of inbound and outbound
|
||||
props. Do not use connectAdvanced directly without memoizing results between calls to your
|
||||
selector, otherwise the Connect component will re-render on every state or props change.
|
||||
*/
|
||||
selectorFactory,
|
||||
// options object:
|
||||
{
|
||||
// the func used to compute this HOC's displayName from the wrapped component's displayName.
|
||||
// probably overridden by wrapper functions such as connect()
|
||||
getDisplayName = name => `ConnectAdvanced(${name})`,
|
||||
|
||||
// shown in error messages
|
||||
// probably overridden by wrapper functions such as connect()
|
||||
methodName = 'connectAdvanced',
|
||||
|
||||
// REMOVED: if defined, the name of the property passed to the wrapped element indicating the number of
|
||||
// calls to render. useful for watching in react devtools for unnecessary re-renders.
|
||||
renderCountProp = undefined,
|
||||
|
||||
// determines whether this HOC subscribes to store changes
|
||||
shouldHandleStateChanges = true,
|
||||
|
||||
// REMOVED: the key of props/context to get the store
|
||||
storeKey = 'store',
|
||||
|
||||
// REMOVED: expose the wrapped component via refs
|
||||
withRef = false,
|
||||
|
||||
// use React's forwardRef to expose a ref of the wrapped component
|
||||
forwardRef = false,
|
||||
|
||||
// the context consumer to use
|
||||
context = ReactReduxContext,
|
||||
|
||||
// additional options are passed through to the selectorFactory
|
||||
...connectOptions
|
||||
} = {}
|
||||
) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (renderCountProp !== undefined) {
|
||||
throw new Error(
|
||||
`renderCountProp is removed. render counting is built into the latest React Dev Tools profiling extension`
|
||||
)
|
||||
}
|
||||
if (withRef) {
|
||||
throw new Error(
|
||||
'withRef is removed. To access the wrapped instance, use a ref on the connected component'
|
||||
)
|
||||
}
|
||||
|
||||
const customStoreWarningMessage =
|
||||
'To use a custom Redux store for specific components, create a custom React context with ' +
|
||||
"React.createContext(), and pass the context object to React Redux's Provider and specific components" +
|
||||
' like: <Provider context={MyContext}><ConnectedComponent context={MyContext} /></Provider>. ' +
|
||||
'You may also pass a {context : MyContext} option to connect'
|
||||
|
||||
if (storeKey !== 'store') {
|
||||
throw new Error(
|
||||
'storeKey has been removed and does not do anything. ' +
|
||||
customStoreWarningMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Context = context
|
||||
|
||||
return function wrapWithConnect(WrappedComponent) {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
!isValidElementType(WrappedComponent)
|
||||
) {
|
||||
throw new Error(
|
||||
`You must pass a component to the function returned by ` +
|
||||
`${methodName}. Instead received ${stringifyComponent(
|
||||
WrappedComponent
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
const wrappedComponentName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || 'Component'
|
||||
|
||||
const displayName = getDisplayName(wrappedComponentName)
|
||||
|
||||
const selectorFactoryOptions = {
|
||||
...connectOptions,
|
||||
getDisplayName,
|
||||
methodName,
|
||||
renderCountProp,
|
||||
shouldHandleStateChanges,
|
||||
storeKey,
|
||||
displayName,
|
||||
wrappedComponentName,
|
||||
WrappedComponent
|
||||
}
|
||||
|
||||
const { pure } = connectOptions
|
||||
|
||||
function createChildSelector(store) {
|
||||
return selectorFactory(store.dispatch, selectorFactoryOptions)
|
||||
}
|
||||
|
||||
// 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 => callback()
|
||||
|
||||
function ConnectFunction(props) {
|
||||
const [propsContext, forwardedRef, 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 { forwardedRef, ...wrapperProps } = props
|
||||
return [props.context, forwardedRef, wrapperProps]
|
||||
}, [props])
|
||||
|
||||
const ContextToUse = 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 &&
|
||||
isContextConsumer(<propsContext.Consumer />)
|
||||
? 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 <Provider>, ` +
|
||||
`or pass a custom React context provider to <Provider> and the corresponding ` +
|
||||
`React context consumer to ${displayName} in connect options.`
|
||||
)
|
||||
}
|
||||
|
||||
// Based on the previous check, one of these must be true
|
||||
const 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 createChildSelector(store)
|
||||
}, [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 = new Subscription(
|
||||
store,
|
||||
didStoreComeFromProps ? null : 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
|
||||
}
|
||||
}, [didStoreComeFromProps, contextValue, subscription])
|
||||
|
||||
// We need to force this wrapper component to re-render whenever a Redux store update
|
||||
// causes a change to the calculated child component props (or we caught an error in mapState)
|
||||
const [
|
||||
[previousStateUpdateResult],
|
||||
forceComponentUpdateDispatch
|
||||
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
|
||||
|
||||
// Propagate any mapState/mapDispatch errors upwards
|
||||
if (previousStateUpdateResult && previousStateUpdateResult.error) {
|
||||
throw previousStateUpdateResult.error
|
||||
}
|
||||
|
||||
// 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 actualChildProps = usePureOnlyMemo(() => {
|
||||
// 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)
|
||||
}, [store, previousStateUpdateResult, 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.
|
||||
useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
|
||||
lastWrapperProps,
|
||||
lastChildProps,
|
||||
renderIsScheduled,
|
||||
wrapperProps,
|
||||
actualChildProps,
|
||||
childPropsFromStoreUpdate,
|
||||
notifyNestedSubs
|
||||
])
|
||||
|
||||
// Our re-subscribe logic only runs when the store/subscription setup changes
|
||||
useIsomorphicLayoutEffectWithArgs(
|
||||
subscribeUpdates,
|
||||
[
|
||||
shouldHandleStateChanges,
|
||||
store,
|
||||
subscription,
|
||||
childPropsSelector,
|
||||
lastWrapperProps,
|
||||
lastChildProps,
|
||||
renderIsScheduled,
|
||||
childPropsFromStoreUpdate,
|
||||
notifyNestedSubs,
|
||||
forceComponentUpdateDispatch
|
||||
],
|
||||
[store, subscription, childPropsSelector]
|
||||
)
|
||||
|
||||
// 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(
|
||||
() => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
|
||||
[forwardedRef, 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 (
|
||||
<ContextToUse.Provider value={overriddenContextValue}>
|
||||
{renderedWrappedComponent}
|
||||
</ContextToUse.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
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 = pure ? React.memo(ConnectFunction) : ConnectFunction
|
||||
|
||||
Connect.WrappedComponent = WrappedComponent
|
||||
Connect.displayName = displayName
|
||||
|
||||
if (forwardRef) {
|
||||
const forwarded = React.forwardRef(function forwardConnectRef(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
return <Connect {...props} forwardedRef={ref} />
|
||||
})
|
||||
|
||||
forwarded.displayName = displayName
|
||||
forwarded.WrappedComponent = WrappedComponent
|
||||
return hoistStatics(forwarded, WrappedComponent)
|
||||
}
|
||||
|
||||
return hoistStatics(Connect, WrappedComponent)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user