Реакция ожидания / отложенная задержка?
Я пытаюсь использовать новые React Lazy и Suspense для создания резервного компонента загрузки. Это прекрасно работает, но запасной вариант показывает только несколько мс. Есть ли способ добавить дополнительную задержку или минимальное время, чтобы я мог показывать анимации из этого компонента до того, как будет визуализирован следующий компонент?
Ленивый импорт сейчас
const Home = lazy(() => import("./home"));
const Products = lazy(() => import("./home/products"));
Компонент ожидания:
function WaitingComponent(Component) {
return props => (
<Suspense fallback={<Loading />}>
<Component {...props} />
</Suspense>
);
}
Могу ли я сделать что-то подобное?
const Home = lazy(() => {
setTimeout(import("./home"), 300);
});
10 ответов
lazy
функция должна вернуть обещание { default: ... }
объект, который возвращается import()
модуля с экспортом по умолчанию. setTimeout
не возвращает обещание и не может быть использовано таким образом. Пока произвольное обещание может:
const Home = lazy(() => {
return new Promise(resolve => {
setTimeout(() => resolve(import("./home"), 300);
});
});
Если цель состоит в том, чтобы обеспечить минимальную задержку, это не очень хороший выбор, потому что это приведет к дополнительной задержке.
Минимальная задержка будет:
const Home = lazy(() => {
return Promise.all([
import("./home"),
new Promise(resolve => setTimeout(resolve, 300))
])
.then(([moduleExports]) => moduleExports);
});
Как упоминалось в loopmode, резервный компонент должен иметь тайм-аут.
import React, { useState, useEffect } from 'react'
const DelayedFallback = () => {
const [show, setShow] = useState(false)
useEffect(() => {
let timeout = setTimeout(() => setShow(true), 300)
return () => {
clearTimeout(timeout)
}
}, [])
return (
<>
{show && <h3>Loading ...</h3>}
</>
)
}
export default DelayedFallback
Затем просто импортируйте этот компонент и используйте его как запасной вариант.
<Suspense fallback={<DelayedFallback />}>
<LazyComponent />
</Suspense>
Анимация резервного компонента с Suspense
а также lazy
У @Akrom Sprinter есть хорошее решение в случае быстрой загрузки, поскольку он скрывает резервный счетчик и позволяет избежать общей задержки. Вот расширение для более сложных анимаций, запрошенных OP:
1. Простой вариант: постепенное появление + отображение с задержкой.
const App = () => {
const [isEnabled, setEnabled] = React.useState(false);
return (
<div>
<button onClick={() => setEnabled(b => !b)}>Toggle Component</button>
<React.Suspense fallback={<Fallback />}>
{isEnabled && <Home />}
</React.Suspense>
</div>
);
};
const Fallback = () => {
const containerRef = React.useRef();
return (
<p ref={containerRef} className="fallback-fadein">
<i className="fa fa-spinner spin" style={{ fontSize: "64px" }} />
</p>
);
};
/*
Technical helpers
*/
const Home = React.lazy(() => fakeDelay(2000)(import_("./routes/Home")));
// import_ is just a stub for the stack snippet; use dynamic import in real code.
function import_(path) {
return Promise.resolve({ default: () => <p>Hello Home!</p> });
}
// add some async delay for illustration purposes
function fakeDelay(ms) {
return promise =>
promise.then(
data =>
new Promise(resolve => {
setTimeout(() => resolve(data), ms);
})
);
}
ReactDOM.render(<App />, document.getElementById("root"));
/* Delay showing spinner first, then gradually let it fade in. */
.fallback-fadein {
visibility: hidden;
animation: fadein 1.5s;
animation-fill-mode: forwards;
animation-delay: 0.5s; /* no spinner flickering for fast load times */
}
@keyframes fadein {
from {
visibility: visible;
opacity: 0;
}
to {
visibility: visible;
opacity: 1;
}
}
.spin {
animation: spin 2s infinite linear;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<div id="root"></div>
Вы просто добавляете немного @keyframes
анимации для Fallback
компонент, и отложить его отображение на setTimeout
и государственный флаг, или чистым CSS (animation-fill-mode
а также -delay
используется здесь).
2. Сложный вариант: нарастание и затухание + отображение с задержкой.
Это возможно, но нужна обертка. У нас нет прямого API дляSuspense
чтобы ждать в исчезать из анимации, прежде чемFallback
компонент отключен.
Создадим кастом useSuspenseAnimation
Крюк, который откладывает обещание, данное React.lazy
достаточно долго, чтобы наша финальная анимация была полностью видна:
// inside useSuspenseAnimation
const DeferredHomeComp = React.lazy(() => Promise.all([
import("./routes/Home"),
deferred.promise // resolve this promise, when Fallback animation is complete
]).then(([imp]) => imp)
)
const App = () => {
const { DeferredComponent, ...fallbackProps } = useSuspenseAnimation(
"./routes/Home"
);
const [isEnabled, setEnabled] = React.useState(false);
return (
<div>
<button onClick={() => setEnabled(b => !b)}>Toggle Component</button>
<React.Suspense fallback={<Fallback {...fallbackProps} />}>
{isEnabled && <DeferredComponent />}
</React.Suspense>
</div>
);
};
const Fallback = ({ hasImportFinished, enableComponent }) => {
const ref = React.useRef();
React.useEffect(() => {
const current = ref.current;
current.addEventListener("animationend", handleAnimationEnd);
return () => {
current.removeEventListener("animationend", handleAnimationEnd);
};
function handleAnimationEnd(ev) {
if (ev.animationName === "fadeout") {
enableComponent();
}
}
}, [enableComponent]);
const classes = hasImportFinished ? "fallback-fadeout" : "fallback-fadein";
return (
<p ref={ref} className={classes}>
<i className="fa fa-spinner spin" style={{ fontSize: "64px" }} />
</p>
);
};
/*
Possible State transitions: LAZY -> IMPORT_FINISHED -> ENABLED
- LAZY: React suspense hasn't been triggered yet.
- IMPORT_FINISHED: dynamic import has completed, now we can trigger animations.
- ENABLED: Deferred component will now be displayed
*/
function useSuspenseAnimation(path) {
const [state, setState] = React.useState(init);
const enableComponent = React.useCallback(() => {
if (state.status === "IMPORT_FINISHED") {
setState(prev => ({ ...prev, status: "ENABLED" }));
state.deferred.resolve();
}
}, [state]);
return {
hasImportFinished: state.status === "IMPORT_FINISHED",
DeferredComponent: state.DeferredComponent,
enableComponent
};
function init() {
const deferred = deferPromise();
// component object reference is kept stable, since it's stored in state.
const DeferredComponent = React.lazy(() =>
Promise.all([
// again some fake delay for illustration
fakeDelay(2000)(import_(path)).then(imp => {
// triggers re-render, so containing component can react
setState(prev => ({ ...prev, status: "IMPORT_FINISHED" }));
return imp;
}),
deferred.promise
]).then(([imp]) => imp)
);
return {
status: "LAZY",
DeferredComponent,
deferred
};
}
}
/*
technical helpers
*/
// import_ is just a stub for the stack snippet; use dynamic import in real code.
function import_(path) {
return Promise.resolve({ default: () => <p>Hello Home!</p> });
}
// add some async delay for illustration purposes
function fakeDelay(ms) {
return promise =>
promise.then(
data =>
new Promise(resolve => {
setTimeout(() => resolve(data), ms);
})
);
}
function deferPromise() {
let resolve;
const promise = new Promise(_resolve => {
resolve = _resolve;
});
return { resolve, promise };
}
ReactDOM.render(<App />, document.getElementById("root"));
/* Delay showing spinner first, then gradually let it fade in. */
.fallback-fadein {
visibility: hidden;
animation: fadein 1.5s;
animation-fill-mode: forwards;
animation-delay: 0.5s; /* no spinner flickering for fast load times */
}
@keyframes fadein {
from {
visibility: visible;
opacity: 0;
}
to {
visibility: visible;
opacity: 1;
}
}
.fallback-fadeout {
animation: fadeout 1s;
animation-fill-mode: forwards;
}
@keyframes fadeout {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.spin {
animation: spin 2s infinite linear;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<div id="root"></div>
Ключевые моменты для сложного варианта
1.) useSuspenseAnimation
Хук возвращает три значения:
hasImportFinished
(boolean
) → еслиtrue
,Fallback
может начать анимацию затуханияenableComponent
(обратный вызов) → вызвать его, чтобы размонтироватьFallback
, когда анимация завершена.DeferredComponent
→ расширенный ленивый компонент, загружаемый динамическимimport
2.) Слушайте animationend
Событие DOM, поэтому мы знаем, когда анимация закончилась.
спасибо @Estus Flask за очень полезный ответ. Раньше я использовал только функциональность setTimeout, но вообще не мог заставить тесты работать. Вызов
import React, { ReactElement, Suspense } from 'react';
import { Outlet, Route, Routes } from 'react-router-dom';
import Loader from 'app/common/components/Loader';
const Navigation = React.lazy(() => {
return Promise.all([
import("./Navigation"),
new Promise(resolve => setTimeout(resolve, 300))
])
.then(([moduleExports]) => moduleExports);
});
const Home = React.lazy(() => {
return Promise.all([
import("./Home"),
new Promise(resolve => setTimeout(resolve, 300))
])
.then(([moduleExports]) => moduleExports);
});
interface PagesProps {
toggleTheme: () => void;
}
const Pages = (props: PagesProps): ReactElement => (
<Suspense fallback={<Loader />}>
<Routes>
<Route path="/" element={
<>
<Navigation toggleTheme={props.toggleTheme}/>
<Outlet />
</>
}>
<Route index element={<Home />} />
</Route>
</Routes>
</Suspense>
);
export default Pages;
Я смог успешно протестировать его следующим образом. Обратите внимание, что если вы не включите
/**
* @jest-environment jsdom
*/
import { render, screen, cleanup, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
import Pages from './';
describe('Pages component', () => {
beforeEach(() => {
jest.useFakeTimers();
})
const history = createMemoryHistory();
it('displays loader when lazy', async () => {
render(
<Router location={history.location} navigator={history} navigationType={history.action}>
<Pages toggleTheme={function (): void { return null; } } />
</Router>,
);
const lazyElement = await screen.findByText(/please wait/i);
expect(lazyElement).toBeInTheDocument();
});
it('displays "Welcome!" on Home page lazily', async () => {
render(
<Router location={history.location} navigator={history} navigationType={history.action}>
<Pages toggleTheme={function (): void { return null; } } />
</Router>,
);
const fallbackLoader = await screen.findByText(/please wait/i);
expect(fallbackLoader).toBeInTheDocument();
jest.runAllTimers();
const lazyElement = await screen.findByText('Welcome!');
expect(lazyElement).toBeInTheDocument();
});
afterEach(cleanup);
});
Вы должны создать резервный компонент, который сам имеет тайм-аут и видимое состояние. Изначально вы устанавливаете visible false. Когда резервный компонент монтируется, он должен setTimeout включить флаг видимого состояния. Либо убедитесь, что ваш компонент все еще смонтирован, либо сбросьте тайм-аут, когда компонент будет отключен. Наконец, если видимое состояние равно false, отобразите null в резервном компоненте (или, например, просто блокирующее / полупрозрачное наложение, но без счетчика / анимации)
Затем используйте такой компонент, например,
Если кто-то ищет машинописное, абстрактное решение:
import { ComponentType, lazy } from 'react';
export const lazyMinLoadTime = <T extends ComponentType<any>>(factory: () => Promise<{ default: T }>, minLoadTimeMs = 2000) =>
lazy(() =>
Promise.all([factory(), new Promise((resolve) => setTimeout(resolve, minLoadTimeMs))]).then(([moduleExports]) => moduleExports)
);
Использование:
const ImportedComponent = lazyMinLoadTime(() => import('./component'), 2000)
Чтобы избежать перепрошивки загрузчика при очень быстрой загрузке, вы можете использовать функцию p-min-delay , которая задерживает обещание на минимальное время. Полезно, когда у вас есть обещание, которое может исполниться немедленно или может занять некоторое время, и вы хотите убедиться, что оно не исполнится слишком быстро.
Например:
import { Suspense, lazy } from 'react';
import { PageLoadingIndicator } from 'components';
import pMinDelay from 'p-min-delay';
const HomePage = lazy(() => pMinDelay(import('./pages/Home'), 500));
function App() {
return (
<Suspense fallback={<PageLoadingIndicator />}>
<HomePage />
</Suspense>
);
}
export default App;
let shouldNotDelay = false;
export const DelayLoading = () => {
if (shouldNotDelay) {
return null;
}
throw new Promise((resolve) => {
setTimeout(() => {
shouldNotDelay = true;
resolve(1);
}, 2000);
});
};
Вот полная реализация:https://codesandbox.io/s/suspense-delay-7i5b34?file=/src/index.js .
Я столкнулся с подобной проблемой, к тому же я использовал TypeScript вместе с React. Итак, я должен был уважать компилятор машинописного текста, и я пошел дальше с подходом, имеющим бесконечную задержку, а также без жалоб со стороны машинописного текста. Обещание, которое никогда не решалось
const LazyRoute = lazy(() => {
return new Promise(resolve => () =>
import(
'../../abc'
).then(x => x e => null as never),
);
});
Вы можете написать новую функцию, которая ожидает как компонента, так и задержки. Этотawait Promise.all
необходимо дождаться разрешения обоих промисов, поэтому время загрузки занимает не менееdelayMs
поэтому больше не мерцает.
export const importDelay = (importFn: () => Promise<any>, delayMs = 500) =>
async () => {
const [ result ] = await Promise.all([
importFn(),
new Promise((resolve) => setTimeout(resolve, delayMs))
]);
return result as { default: ComponentType<any> };
};
И используйте его так:
const Component = React.lazy(importDelay(import("./component")));