/** * 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, V, State = Store extends ReduxStore ? 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(); }; }