TelegramPWA/src/lib/teact/teactn.tsx
2021-07-13 17:31:30 +03:00

237 lines
6.3 KiB
TypeScript

import React, {
FC, FC_withDebug, Props, useEffect, useState,
} from './teact';
import { DEBUG, DEBUG_MORE } from '../../config';
import useForceUpdate from '../../hooks/useForceUpdate';
import generateIdFor from '../../util/generateIdFor';
import { throttleWithRaf } from '../../util/schedulers';
import arePropsShallowEqual, { getUnequalProps } from '../../util/arePropsShallowEqual';
import { orderBy } from '../../util/iteratees';
import { GlobalState, GlobalActions, ActionTypes } from '../../global/types';
import { handleError } from '../../util/handleError';
export default React;
type ActionPayload = AnyLiteral;
type Reducer = (
global: GlobalState,
actions: GlobalActions,
payload: any,
) => GlobalState | void;
type MapStateToProps<OwnProps = undefined> = ((global: GlobalState, ownProps: OwnProps) => AnyLiteral | null);
type MapActionsToProps = ((setGlobal: Function, actions: GlobalActions) => Partial<GlobalActions> | null);
let currentGlobal = {} as GlobalState;
const reducers: Record<string, Reducer[]> = {};
const callbacks: Function[] = [updateContainers];
const actions = {} as GlobalActions;
const containers = new Map<string, {
mapStateToProps: MapStateToProps<any>;
mapReducersToProps: MapActionsToProps;
ownProps: Props;
mappedProps?: Props;
forceUpdate: Function;
areMappedPropsChanged: boolean;
DEBUG_updates: number;
DEBUG_componentName: string;
}>();
function runCallbacks() {
callbacks.forEach((cb) => cb(currentGlobal));
}
const runCallbacksThrottled = throttleWithRaf(runCallbacks);
// `noThrottle = true` is used as a workaround for iOS gesture history navigation
export function setGlobal(newGlobal?: GlobalState, noThrottle = false) {
if (typeof newGlobal === 'object' && newGlobal !== currentGlobal) {
currentGlobal = newGlobal;
if (!noThrottle) {
runCallbacksThrottled();
} else {
runCallbacks();
}
}
}
export function getGlobal() {
return currentGlobal;
}
export function getDispatch() {
return actions;
}
function onDispatch(name: string, payload?: ActionPayload, noThrottle?: boolean) {
if (reducers[name]) {
reducers[name].forEach((reducer) => {
const newGlobal = reducer(currentGlobal, actions, payload);
if (newGlobal) {
setGlobal(newGlobal, noThrottle);
}
});
}
}
function updateContainers() {
let DEBUG_startAt: number | undefined;
if (DEBUG) {
DEBUG_startAt = performance.now();
}
// eslint-disable-next-line no-restricted-syntax
for (const container of containers.values()) {
const {
mapStateToProps, mapReducersToProps, ownProps, mappedProps, forceUpdate,
} = container;
let newMappedProps;
try {
newMappedProps = {
...mapStateToProps(currentGlobal, ownProps),
...mapReducersToProps(setGlobal, actions),
};
} catch (err) {
handleError(err);
return;
}
if (DEBUG) {
if (Object.values(newMappedProps).some(Number.isNaN)) {
// eslint-disable-next-line no-console
console.warn(
// eslint-disable-next-line max-len
`[TeactN] Some of \`${container.DEBUG_componentName}\` mappers contain NaN values. This may cause redundant updates because of incorrect equality check.`,
);
}
}
if (Object.keys(newMappedProps).length && !arePropsShallowEqual(mappedProps!, newMappedProps)) {
if (DEBUG_MORE) {
// eslint-disable-next-line no-console
console.log(
'[TeactN] Will update',
container.DEBUG_componentName,
'caused by',
getUnequalProps(mappedProps!, newMappedProps).join(', '),
);
}
container.mappedProps = newMappedProps;
container.areMappedPropsChanged = true;
container.DEBUG_updates++;
forceUpdate();
}
}
if (DEBUG) {
const updateTime = performance.now() - DEBUG_startAt!;
if (updateTime > 7) {
// eslint-disable-next-line no-console
console.warn(`[TeactN] Slow containers update: ${Math.round(updateTime)} ms`);
}
}
}
export function addReducer(name: ActionTypes, reducer: Reducer) {
if (!reducers[name]) {
reducers[name] = [];
actions[name] = (payload?: ActionPayload, noThrottle = false) => {
onDispatch(name, payload, noThrottle);
};
}
reducers[name].push(reducer);
}
export function addCallback(cb: Function) {
callbacks.push(cb);
}
export function removeCallback(cb: Function) {
const index = callbacks.indexOf(cb);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
export function withGlobal<OwnProps>(
mapStateToProps: MapStateToProps<OwnProps> = () => ({}),
mapReducersToProps: MapActionsToProps = () => ({}),
) {
return (Component: FC) => {
return function TeactNContainer(props: OwnProps) {
(TeactNContainer as FC_withDebug).DEBUG_contentComponentName = Component.name;
const [id] = useState(generateIdFor(containers));
const forceUpdate = useForceUpdate();
useEffect(() => {
return () => {
containers.delete(id);
};
}, [id]);
let container = containers.get(id);
if (!container) {
container = {
mapStateToProps,
mapReducersToProps,
ownProps: props,
areMappedPropsChanged: false,
forceUpdate,
DEBUG_updates: 0,
DEBUG_componentName: Component.name,
};
containers.set(id, container);
}
if (container.areMappedPropsChanged) {
container.areMappedPropsChanged = false;
}
if (!container.mappedProps || !arePropsShallowEqual(container.ownProps, props)) {
container.ownProps = props;
try {
container.mappedProps = {
...mapStateToProps(currentGlobal, props),
...mapReducersToProps(setGlobal, actions),
};
} catch (err) {
handleError(err);
}
}
// eslint-disable-next-line react/jsx-props-no-spreading
return <Component {...container.mappedProps} {...props} />;
};
};
}
if (DEBUG) {
(window as any).getGlobal = getGlobal;
document.addEventListener('dblclick', () => {
// eslint-disable-next-line no-console
console.log(
'GLOBAL CONTAINERS',
orderBy(
Array.from(containers.values())
.map(({ DEBUG_componentName, DEBUG_updates }) => ({ DEBUG_componentName, DEBUG_updates })),
'DEBUG_updates',
'desc',
),
);
});
}