Files
flipper/desktop/flipper-ui-core/src/utils/sideEffect.tsx
Michel Weststrate 5df34a337c Unshare global types
Summary:
This diff adds `types` fields on the compiler config for every project. This way we can make sure that for example node types and packages are not available in flipper-ui-core. Without an explicit types field, all types would be shared between all packages, and implicitly included into the compilation of everything. For the same reason `types/index.d.ts` has been removed, we want to be intentional on which types are being used in which package.

This diff does most of the work, the next diff will fine tune the globals, and do some further cleanup.

As an alternative solution I first tried a `nohoist: **/node_modules/types/**` and make sure every package list explicitly the types used in package json, which works but is much more error prone, as for example two different react types versions in two packages will cause the most unreadable compiler error due to the types not being shared and not literally the same.

Reviewed By: lawrencelomax

Differential Revision: D33124441

fbshipit-source-id: c2b9d768f845ac28005d8331ef5fa1066c7e4cd7
2021-12-17 07:36:07 -08:00

110 lines
3.0 KiB
TypeScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Store as ReduxStore} from 'redux';
import {shallowEqual} from 'react-redux';
/**
* Registers a sideEffect for the given redux store. Use this utility rather than subscribing to the Redux store directly, which fixes a few problems:
* 1. It decouples and throttles the effect so that no arbitrary expensive burden is added to every store update.
* 2. It makes sure that a crashing side effect doesn't crash the entire store update.
* 3. It helps with tracing and monitoring perf problems.
* 4. It puts the side effect behind a selector so that the side effect is only triggered if a relevant part of the store changes, like we do for components.
*
* @param store
* @param options
* @param selector
* @param effect
*/
export function sideEffect<
Store extends ReduxStore<any, any>,
V,
State = Store extends ReduxStore<infer S, any> ? S : never,
>(
store: Store,
options: {
name: string;
throttleMs: number;
fireImmediately?: boolean;
noTimeBudgetWarns?: boolean;
runSynchronously?: boolean;
},
selector: (state: State) => V,
effect: (selectedState: V, store: Store) => void,
): () => void {
let scheduled = false;
let lastRun = -1;
let lastSelectedValue: V = selector(store.getState());
let timeout: any;
function run() {
scheduled = false;
const start = performance.now();
try {
// Future idea: support effects that return promises as well
lastSelectedValue = selector(store.getState());
effect(lastSelectedValue, store);
} catch (e) {
console.error(
`Error while running side effect '${options.name}': ${e}`,
e,
);
}
lastRun = performance.now();
const duration = lastRun - start;
if (
!options.noTimeBudgetWarns &&
duration > 15 &&
duration > options.throttleMs / 10
) {
console.warn(
`Side effect '${options.name}' took ${Math.round(
duration,
)}ms, which exceeded its budget of ${Math.floor(
options.throttleMs / 10,
)}ms. Please make the effect faster or increase the throttle time.`,
);
}
}
const unsubscribe = store.subscribe(() => {
if (scheduled) {
return;
}
const newValue = selector(store.getState());
if (
newValue === lastSelectedValue ||
shallowEqual(newValue, lastSelectedValue)
) {
return; // no new value, no need to schedule
}
scheduled = true;
if (options.runSynchronously) {
run();
} else {
timeout = setTimeout(
run,
// Run ASAP (but async) or, if we recently did run, delay until at least 'throttle' time has expired
lastRun === -1
? 1
: Math.max(1, lastRun + options.throttleMs - performance.now()),
);
}
});
if (options.fireImmediately) {
run();
}
return () => {
clearTimeout(timeout);
unsubscribe();
};
}