react-beautiful-dnd-next
Version:
Beautiful and accessible drag and drop for lists with React
190 lines (167 loc) • 5.76 kB
JSX
// @flow
import React, { useEffect, useRef, type Node } from 'react';
import invariant from 'tiny-invariant';
import { bindActionCreators } from 'redux';
import { Provider } from 'react-redux';
import { useMemo, useCallback } from 'use-memo-one';
import createStore from '../../state/create-store';
import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal';
import canStartDrag from '../../state/can-start-drag';
import scrollWindow from '../window/scroll-window';
import createAutoScroller from '../../state/auto-scroller';
import useStyleMarshal from '../use-style-marshal/use-style-marshal';
import type { AutoScroller } from '../../state/auto-scroller/auto-scroller-types';
import type { StyleMarshal } from '../use-style-marshal/style-marshal-types';
import type {
DimensionMarshal,
Callbacks as DimensionMarshalCallbacks,
} from '../../state/dimension-marshal/dimension-marshal-types';
import type { DraggableId, State, Responders, Announce } from '../../types';
import type { Store, Action } from '../../state/store-types';
import StoreContext from '../context/store-context';
import {
clean,
move,
publishWhileDragging,
updateDroppableScroll,
updateDroppableIsEnabled,
updateDroppableIsCombineEnabled,
collectionStarting,
} from '../../state/action-creators';
import isMovementAllowed from '../../state/is-movement-allowed';
import useAnnouncer from '../use-announcer';
import AppContext, { type AppContextValue } from '../context/app-context';
import useStartupValidation from './use-startup-validation';
import usePrevious from '../use-previous-ref';
import { warning } from '../../dev-warning';
type Props = {|
...Responders,
uniqueId: number,
setOnError: (onError: Function) => void,
// we do not technically need any children for this component
children: Node | null,
|};
const createResponders = (props: Props): Responders => ({
onBeforeDragStart: props.onBeforeDragStart,
onDragStart: props.onDragStart,
onDragEnd: props.onDragEnd,
onDragUpdate: props.onDragUpdate,
});
// flow does not support MutableRefObject
// type LazyStoreRef = MutableRefObject<?Store>;
type LazyStoreRef = {| current: ?Store |};
function getStore(lazyRef: LazyStoreRef): Store {
invariant(lazyRef.current, 'Could not find store from lazy ref');
return lazyRef.current;
}
export default function App(props: Props) {
const { uniqueId, setOnError } = props;
const lazyStoreRef: LazyStoreRef = useRef<?Store>(null);
useStartupValidation();
// lazy collection of responders using a ref - update on ever render
const lastPropsRef = usePrevious<Props>(props);
const getResponders: () => Responders = useCallback(() => {
return createResponders(lastPropsRef.current);
}, [lastPropsRef]);
const announce: Announce = useAnnouncer(uniqueId);
const styleMarshal: StyleMarshal = useStyleMarshal(uniqueId);
const lazyDispatch: Action => void = useCallback((action: Action): void => {
getStore(lazyStoreRef).dispatch(action);
}, []);
const callbacks: DimensionMarshalCallbacks = useMemo(
() =>
bindActionCreators(
{
publishWhileDragging,
updateDroppableScroll,
updateDroppableIsEnabled,
updateDroppableIsCombineEnabled,
collectionStarting,
},
// $FlowFixMe - not sure why this is wrong
lazyDispatch,
),
[lazyDispatch],
);
const dimensionMarshal: DimensionMarshal = useMemo<DimensionMarshal>(
() => createDimensionMarshal(callbacks),
[callbacks],
);
const autoScroller: AutoScroller = useMemo<AutoScroller>(
() =>
createAutoScroller({
scrollWindow,
scrollDroppable: dimensionMarshal.scrollDroppable,
...bindActionCreators(
{
move,
},
// $FlowFixMe - not sure why this is wrong
lazyDispatch,
),
}),
[dimensionMarshal.scrollDroppable, lazyDispatch],
);
const store: Store = useMemo<Store>(
() =>
createStore({
dimensionMarshal,
styleMarshal,
announce,
autoScroller,
getResponders,
}),
[announce, autoScroller, dimensionMarshal, getResponders, styleMarshal],
);
// Checking for unexpected store changes
if (process.env.NODE_ENV !== 'production') {
if (lazyStoreRef.current && lazyStoreRef.current !== store) {
warning('unexpected store change');
}
}
// assigning lazy store ref
lazyStoreRef.current = store;
const tryResetStore = useCallback(() => {
const current: Store = getStore(lazyStoreRef);
const state: State = current.getState();
if (state.phase !== 'IDLE') {
current.dispatch(clean({ shouldFlush: true }));
}
}, []);
// doing this in render rather than a side effect so any errors on the
// initial mount are caught
setOnError(tryResetStore);
const getCanLift = useCallback(
(id: DraggableId) => canStartDrag(getStore(lazyStoreRef).getState(), id),
[],
);
const getIsMovementAllowed = useCallback(
() => isMovementAllowed(getStore(lazyStoreRef).getState()),
[],
);
const appContext: AppContextValue = useMemo(
() => ({
marshal: dimensionMarshal,
style: styleMarshal.styleContext,
canLift: getCanLift,
isMovementAllowed: getIsMovementAllowed,
}),
[
dimensionMarshal,
getCanLift,
getIsMovementAllowed,
styleMarshal.styleContext,
],
);
// Clean store when unmounting
useEffect(() => {
return tryResetStore;
}, [tryResetStore]);
return (
<AppContext.Provider value={appContext}>
<Provider context={StoreContext} store={store}>
{props.children}
</Provider>
</AppContext.Provider>
);
}