Skip to content

Instantly share code, notes, and snippets.

@bilalbhojani24
Created January 7, 2026 10:01
Show Gist options
  • Select an option

  • Save bilalbhojani24/1114a00328e79da7d8d6db95e5f5a72c to your computer and use it in GitHub Desktop.

Select an option

Save bilalbhojani24/1114a00328e79da7d8d6db95e5f5a72c to your computer and use it in GitHub Desktop.
// @ts-nocheck
/* eslint-disable rippling-eslint/prefer-function-component */
import React from 'react';
import styled from '@emotion/styled';
import classNames from 'classnames';
import _ from 'lodash';
import { useTranslation } from '@rippling/lib-i18n';
import ActionCard from '@rippling/pebble/ActionCard';
import Animation from '@rippling/pebble/Animation';
import Atoms from '@rippling/pebble/Atoms';
import Button from '@rippling/pebble/Button';
import Spinner from '@rippling/pebble/Spinner';
import { ErrorBoundary } from '@rippling/pebble/WithErrorBoundary';
import { logWithSpinnerAntiPattern } from 'app/lib/log/patterns/withSpinnerAntiPatterns';
import Sentry from 'app/lib/log/sentry/sentry';
import useIsLoggedInAs from 'app/products/it/Identity/hooks/useIsLoggedInAs';
import { apiNavigateToPage as navigateToPage } from 'app/routes/routes.helpers';
import { handleGetDataErrors } from './getDataErrorHandler';
import WithSpinnerContainer from './withSpinnerContainer';
import './withSpinner.scss';
const OverlayPageContainer = styled.div`
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: ${({ theme }) => theme.colorSurfaceBright};
opacity: 0.5;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
`;
function ErrorMessage({ error }) {
const { ready, t } = useTranslation('core');
const isLoggedInAs = useIsLoggedInAs();
if (!ready) {
return null;
}
if (isLoggedInAs && error?.status === 403) {
return (
<ActionCard
title={t('withSpinner.permissionIssue')}
caption={t('withSpinner.noPermission')}
animation={Animation.TYPES.EXCLAMATION_MARK}
primaryAction={{
title: t('withSpinner.returnToDashboard'),
onClick: () => navigateToPage('/dashboard'),
}}
/>
);
}
return (
<div className="withSpinner__apiError">
<Atoms.ApiError message={t('withSpinner.apiErrorMessage')} />
<div className="textAlign--center">
<Button onClick={() => navigateToPage('/dashboard')}>
{t('withSpinner.returnToDashboard')}
</Button>
</div>
</div>
);
}
class RuntimeErrorBoundary extends ErrorBoundary {
constructor(props) {
super(props);
this.state = {
hasError: true,
};
}
}
/**
* @deprecated Use ErrorBoundary and Tanstack Query instead
* https://rippling.atlassian.net/wiki/spaces/UP/pages/5310022126/Sunsetting+withSpinner
*/
export default (Component, Loader: React.FC | null = null, meta = {}) => {
class ComponentWithSpinner extends Component {
context: any;
constructor(props, context) {
super(props, context);
this.state = {
...this.state,
spinner: true,
showSpinner: false,
hasAPIFailed: false,
pickedFormData: {}, // typeform input changes of only picked keys
spinnerDelayElapsed: false,
};
this.isUnMounted = false;
this.isFetching = false;
this._initalRenderCbCalled = false;
this.antiPatternId =
Date.now().toString() + Math.random().toString(36).substring(2);
this.spinnerDelayTimeout = null;
this.onFormChangeSetter = this.onFormChangeSetter.bind(this);
}
/**
* Modified default setState function to absorb warning due to setState invoke after component unmount.
* @param {object} nextState - object to apply on current state.
* @param {function} afterStateUpdateCallback - the function will be called after state update
*/
setState(...args) {
/** Passing call to the original function if the component is mounted */
if (!this.isUnMounted) {
return super.setState(...args);
}
/** else, not doing anything because updating state
* doesn't make sense at all as the component is already unmounted */
return undefined;
}
onFormChangeSetter(formInputChanges) {
this.setState({
pickedFormData: formInputChanges,
});
}
async UNSAFE_componentWillMount() {
if (!Component) return;
if (_.isFunction(super.componentWillMount)) {
await super.componentWillMount();
}
this.isFetching = true;
let hasAPIFailed = false;
let dataFetcher = this.componentGetData || (() => {});
dataFetcher = dataFetcher.bind(this);
try {
await dataFetcher();
} catch (error) {
hasAPIFailed = true;
this.setState({ error });
const throwError = await handleGetDataErrors(error, meta);
if (throwError) {
throw error;
}
} finally {
this.isFetching = false;
if (!this.isUnMounted) {
this.setState(
{
spinner: hasAPIFailed,
hasAPIFailed,
},
this.componentDidRender
);
}
}
}
componentDidRender = () => {
let cb = super.componentDidInitialRender || _.noop;
if (!this._initalRenderCbCalled) {
cb = cb.bind(this);
cb();
if (this.props.onInitialRender) {
this.props.onInitialRender();
}
}
};
componentDidMount = () => {
this.isUnMounted = false;
if (_.isFunction(super.componentDidMount)) {
super.componentDidMount();
}
const componentName =
Component.displayName || Component.name || 'UnknownComponent';
logWithSpinnerAntiPattern({ componentName, id: this.antiPatternId });
// Delay spinner rendering by 1000ms to prevent flashing for quick operations
this.spinnerDelayTimeout = setTimeout(() => {
if (!this.isUnMounted) {
this.setState({ spinnerDelayElapsed: true });
}
}, 1000);
};
componentWillUnmount() {
this.isUnMounted = true;
if (this.spinnerDelayTimeout) {
clearTimeout(this.spinnerDelayTimeout);
this.spinnerDelayTimeout = null;
}
if (super.componentWillUnmount) {
super.componentWillUnmount();
}
}
handleRenderCrash = e => {
Sentry.captureRenderCrash(e, meta.executionNamespace);
};
renderSpinnerAndApiError = () => {
const { spinner, hasAPIFailed, error, spinnerDelayElapsed } = this.state;
if (!spinner) {
return null;
}
if (spinner && hasAPIFailed) {
return <ErrorMessage error={error} />;
}
// Only show spinner after 1000ms delay to prevent flashing
if (!spinnerDelayElapsed) {
return null;
}
const spinnerText = super.getSpinnerText && super.getSpinnerText();
return <Spinner windowCentered title={spinnerText} />;
};
render() {
const { spinner, showSpinner, spinnerDelayElapsed } = this.state;
const { isInOverlay } = this.props;
const renderComponent = () => {
try {
return super.render();
} catch (e) {
this.handleRenderCrash(e);
return <RuntimeErrorBoundary />;
}
};
const getChildren = () => {
if (Loader && spinner) {
return <Loader />;
}
/**
* Wrapper stylings are not required in overlay
*/
if (isInOverlay && !spinner && !showSpinner) {
return renderComponent();
}
return (
<WithSpinnerContainer isSpinning={spinner}>
<div
className={classNames(
{ 'swippable-area': !this.props.disableSwipeAnimation },
{ 'hide-child-spinner': spinner }
)}
>
{!spinner && renderComponent()}
</div>
{this.renderSpinnerAndApiError()}
{showSpinner && spinnerDelayElapsed && (
<OverlayPageContainer>
<Spinner />
</OverlayPageContainer>
)}
</WithSpinnerContainer>
);
};
return (
<ErrorBoundary onError={this.handleRenderCrash}>
{getChildren()}
</ErrorBoundary>
);
}
}
ComponentWithSpinner.isWithSpinner = true;
ComponentWithSpinner.defaultProps = _.extend({}, Component.defaultProps, {
disableSwipeAnimation: false,
isWrappedWithSpinner: true,
onInitialRender: _.noop,
});
const I18N_SKIP_UNKNOWN = 'Unknown';
ComponentWithSpinner.displayName = `WithSpinner(${
Component.name || Component.displayName || I18N_SKIP_UNKNOWN
})`;
return ComponentWithSpinner as React.ComponentClass<any>;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment