vitessce
Version:
Vitessce app and React component library
436 lines (400 loc) • 14.2 kB
JavaScript
import { useRef, useCallback, useMemo } from 'react';
import create from 'zustand';
import createContext from 'zustand/context';
import shallow from 'zustand/shallow';
import { fromEntries, capitalize } from '../../utils';
// Reference: https://github.com/pmndrs/zustand#react-context
// Reference: https://github.com/pmndrs/zustand/blob/e47ea03/tests/context.test.tsx#L60
const {
Provider: ViewConfigProviderLocal,
useStore: useViewConfigStoreLocal,
useStoreApi: useViewConfigStoreApiLocal,
} = createContext();
export const ViewConfigProvider = ViewConfigProviderLocal;
export const useViewConfigStore = useViewConfigStoreLocal;
export const useViewConfigStoreApi = useViewConfigStoreApiLocal;
const {
Provider: AuxiliaryProviderLocal,
useStore: useAuxiliaryStoreLocal,
} = createContext();
export const AuxiliaryProvider = AuxiliaryProviderLocal;
export const useAuxiliaryStore = useAuxiliaryStoreLocal;
/**
* The useViewConfigStore hook is initialized via the zustand
* create() function, which sets up both the state variables
* and the reducer-type functions.
* Reference: https://github.com/react-spring/zustand
* @returns {function} The useStore hook.
*/
export const createViewConfigStore = () => create(set => ({
// State:
// The viewConfig is an object which must conform to the schema
// found in src/schemas/config.schema.json.
viewConfig: null,
// The loaders object is a mapping from dataset ID to
// data type to loader object instance.
loaders: null,
// Reducer functions which update the state
// (although technically also part of state):
setViewConfig: viewConfig => set({ viewConfig }),
setLoaders: loaders => set({ loaders }),
setCoordinationValue: ({ parameter, scope, value }) => set(state => ({
viewConfig: {
...state.viewConfig,
coordinationSpace: {
...state.viewConfig.coordinationSpace,
[parameter]: {
...state.viewConfig.coordinationSpace[parameter],
[scope]: value,
},
},
},
})),
removeComponent: i => set((state) => {
const newLayout = state.viewConfig.layout.slice();
newLayout.splice(i, 1);
return {
viewConfig: {
...state.viewConfig,
layout: newLayout,
},
};
}),
changeLayout: newComponentProps => set((state) => {
const newLayout = state.viewConfig.layout.slice();
newComponentProps.forEach(([i, newProps]) => {
newLayout[i] = {
...newLayout[i],
...newProps,
};
});
return {
viewConfig: {
...state.viewConfig,
layout: newLayout,
},
};
}),
}));
/**
* Hook for getting components' layout from the view config based on
* matching all coordination scopes.
* @returns {Object} The components' layout.
*/
export const useComponentLayout = (component, scopes, coordinationScopes) => useViewConfigStore(
state => state.viewConfig.layout.filter(l => l.component === component).filter(
l => scopes.every(scope => l.coordinationScopes[scope]
=== coordinationScopes[scope]),
),
);
/**
* The useAuxiliaryStore hook is initialized via the zustand
* create() function, which sets up both the state variables
* and the reducer-type functions.
* Reference: https://github.com/react-spring/zustand
* It is meant to be used for non-viewconfig-based coordination between components.
* For example, as currently happens, the layer controller can coordinate
* on-load callbacks with spatial view based on whether or not they are
* coordinated via `spatialRasterLayers` - the callbacks are not part of the view config
* though so they live here.
* @returns {function} The useStore hook.
*/
export const createAuxiliaryStore = () => create(set => ({
auxiliaryStore: null,
setCoordinationValue: ({ parameter, scope, value }) => set(state => ({
auxiliaryStore: {
...state.auxiliaryStore,
[parameter]: {
[scope]: value,
},
},
})),
}));
/**
* The hover store can be used to store global state
* related to which component is currently hovered,
* which is required for tooltip / crossover elements.
* @returns {function} The useStore hook.
*/
const useHoverStore = create(set => ({
// Components may need to know if they are the "hover source"
// for tooltip interactions. This value should be a unique
// component ID, such as its index in the view config layout.
componentHover: null,
setComponentHover: componentHover => set({ componentHover }),
}));
/**
* The warning store can be used to store global state
* related to app warning messages.
* @returns {function} The useStore hook.
*/
const useWarnStore = create(set => ({
// Want a global state to collect warning messages
// that occur anywhere in the app.
warning: null,
setWarning: warning => set({ warning }),
}));
/**
* The view info store can be used to store component-level
* viewInfo objects,
* which are required for tooltip / crossover elements.
* @returns {function} The useStore hook.
*/
const useViewInfoStore = create(set => ({
// The viewInfo object is a mapping from
// component IDs to component view info objects.
// Each view info object must have a project() function.
viewInfo: {},
setComponentViewInfo: (uuid, viewInfo) => set(state => ({
viewInfo: {
...state.viewInfo,
[uuid]: viewInfo,
},
})),
}));
/**
* The grid size store can be used to store a
* counter which updates on each window or react-grid-layout
* resize event.
* @returns {function} The useStore hook.
*/
const useGridSizeStore = create(set => ({
resizeCount: {},
incrementResizeCount: () => set(state => ({
resizeCount: state.resizeCount + 1,
})),
}));
/**
* The useCoordination hook returns both the
* values and setter functions for the coordination objects
* in a particular coordination scope mapping.
* This hook is intended to be used within the ___Subscriber
* components to allow them to "hook into" only those coordination
* objects and setter functions of relevance.
* @param {string[]} parameters Array of coordination types.
* @param {object} coordinationScopes Mapping of coordination types
* to scope names.
* @returns {array} Returns a tuple [values, setters]
* where values is an object containing all coordination values,
* and setters is an object containing all coordination setter
* functions for the values in `values`, named with a "set"
* prefix.
*/
export function useCoordination(parameters, coordinationScopes) {
const setCoordinationValue = useViewConfigStore(state => state.setCoordinationValue);
const values = useViewConfigStore((state) => {
const { coordinationSpace } = state.viewConfig;
return fromEntries(parameters.map((parameter) => {
if (coordinationSpace && coordinationSpace[parameter]) {
const value = coordinationSpace[parameter][coordinationScopes[parameter]];
return [parameter, value];
}
return [parameter, undefined];
}));
}, shallow);
const setters = useMemo(() => fromEntries(parameters.map((parameter) => {
const setterName = `set${capitalize(parameter)}`;
const setterFunc = value => setCoordinationValue({
parameter,
scope: coordinationScopes[parameter],
value,
});
return [setterName, setterFunc];
// eslint-disable-next-line react-hooks/exhaustive-deps
})), [parameters, coordinationScopes]);
return [values, setters];
}
const AUXILIARY_COORDINATION_TYPES_MAP = {
spatialRasterLayers: ['rasterLayersCallbacks', 'areLoadingRasterChannnels'],
};
/**
* The maps the coordination types of incoming scopes to new types
* for the auxiliary store.
* @param {object} coordinationScopes Mapping of coordination types
* to scope names.
* @returns {object} Mapping of coordination types
* to new scope names for the auxiliary store.
*/
const mapCoordinationScopes = (coordinationScopes) => {
const newCoordinationScopes = {};
Object.keys(coordinationScopes).forEach((key) => {
const newCoordinationTypes = AUXILIARY_COORDINATION_TYPES_MAP[key] || [];
newCoordinationTypes.forEach((coordinationType) => {
newCoordinationScopes[coordinationType || key] = coordinationScopes[key];
});
});
return newCoordinationScopes;
};
const mapParameters = parameters => parameters
.map(parameter => AUXILIARY_COORDINATION_TYPES_MAP[parameter]).filter(Boolean).flat();
/**
* The useAuxiliaryCoordination hook returns both the
* values and setter functions for the auxiliary coordination objects
* in a particular coordination scope mapping.
* This hook is intended to be used within the ___Subscriber
* components to allow them to "hook into" only those auxiliary coordination
* objects and setter functions of relevance, for example "on load" callbacks.
* @param {string[]} parameters Array of coordination types.
* @param {object} coordinationScopes Mapping of coordination types
* to scope names.
* @returns {array} Returns a tuple [values, setters]
* where values is an object containing all coordination values,
* and setters is an object containing all coordination setter
* functions for the values in `values`, named with a "set"
* prefix.
*/
export function useAuxiliaryCoordination(parameters, coordinationScopes) {
const setCoordinationValue = useAuxiliaryStore(state => state.setCoordinationValue);
const mappedCoordinationScopes = mapCoordinationScopes(coordinationScopes);
const mappedParameters = mapParameters(parameters);
const values = useAuxiliaryStore((state) => {
const { auxiliaryStore } = state;
return fromEntries(mappedParameters.map((parameter) => {
if (auxiliaryStore && auxiliaryStore[parameter]) {
const value = auxiliaryStore[parameter][mappedCoordinationScopes[parameter]];
return [parameter, value];
}
return [parameter, undefined];
}));
}, shallow);
const setters = useMemo(() => fromEntries(mappedParameters.map((parameter) => {
const setterName = `set${capitalize(parameter)}`;
const setterFunc = value => setCoordinationValue({
parameter,
scope: mappedCoordinationScopes[parameter],
value,
});
return [setterName, setterFunc];
// eslint-disable-next-line react-hooks/exhaustive-deps
})), [parameters, coordinationScopes]);
return [values, setters];
}
/**
* Obtain the loaders object from
* the global app state.
* @returns {object} The loaders object
* in the `useViewConfigStore` store.
*/
export function useLoaders() {
return useViewConfigStore(state => state.loaders);
}
/**
* Obtain the view config layout array from
* the global app state.
* @returns {object[]} The layout array
* in the `useViewConfigStore` store.
*/
export function useLayout() {
return useViewConfigStore(state => state.viewConfig?.layout);
}
/**
* Obtain the component removal function from
* the global app state.
* @returns {function} The remove component function
* in the `useViewInfoStore` store.
*/
export function useRemoveComponent() {
return useViewConfigStore(state => state.removeComponent);
}
/**
* Obtain the component prop setter function from
* the global app state.
* @returns {function} The set component props function
* in the `useViewInfoStore` store.
*/
export function useChangeLayout() {
return useViewConfigStore(state => state.changeLayout);
}
/**
* Obtain the loaders setter function from
* the global app state.
* @returns {function} The loaders setter function
* in the `useViewConfigStore` store.
*/
export function useSetLoaders() {
return useViewConfigStore(state => state.setLoaders);
}
/**
* Obtain the view config setter function from
* the global app state.
* @returns {function} The view config setter function
* in the `useViewConfigStore` store.
*/
export function useSetViewConfig(viewConfigStoreApi) {
const setViewConfigRef = useRef(viewConfigStoreApi.getState().setViewConfig);
const setViewConfig = setViewConfigRef.current;
return setViewConfig;
}
/**
* Obtain the component hover value from
* the global app state.
* @returns {number} The hovered component ID
* in the `useHoverStore` store.
*/
export function useComponentHover() {
return useHoverStore(state => state.componentHover);
}
/**
* Obtain the component hover setter function from
* the global app state.
* @returns {function} The component hover setter function
* in the `useHoverStore` store.
*/
export function useSetComponentHover() {
return useHoverStore(state => state.setComponentHover);
}
/**
* Obtain the warning message from
* the global app state.
* @returns {string} The warning message
* in the `useWarnStore` store.
*/
export function useWarning() {
return useWarnStore(state => state.warning);
}
/**
* Obtain the warning setter function from
* the global app state.
* @returns {function} The warning setter function
* in the `useWarnStore` store.
*/
export function useSetWarning() {
return useWarnStore(state => state.setWarning);
}
/**
* Obtain the component view info value from
* the global app state.
* @returns {object} The view info object for the component
* in the `useViewInfoStore` store.
*/
export function useComponentViewInfo(uuid) {
return useViewInfoStore(useCallback(state => state.viewInfo[uuid], [uuid]));
}
/**
* Obtain the component view info setter function from
* the global app state.
* @returns {function} The component view info setter function
* in the `useViewInfoStore` store.
*/
export function useSetComponentViewInfo(uuid) {
const setViewInfoRef = useRef(useViewInfoStore.getState().setComponentViewInfo);
const setComponentViewInfo = viewInfo => setViewInfoRef.current(uuid, viewInfo);
return setComponentViewInfo;
}
/**
* Obtain the grid resize count value
* from the global app state.
* @returns {number} The grid resize increment value.
*/
export function useGridResize() {
return useGridSizeStore(state => state.resizeCount);
}
/**
* Obtain the grid resize count increment function
* from the global app state.
* @returns {function} The grid resize count increment
* function.
*/
export function useEmitGridResize() {
return useGridSizeStore(state => state.incrementResizeCount);
}