Feat: support keepalive without experimental version of react (#6768)

* feat: support keepalive without experimental version of react

* feat: add keep alive example

* fix: optimize code
This commit is contained in:
ClarkXia 2024-02-20 11:22:19 +08:00 committed by GitHub
parent 45bf24bf3c
commit 591a9abe96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 254 additions and 40 deletions

View File

@ -0,0 +1,3 @@
import { defineConfig } from '@ice/app';
export default defineConfig(() => ({}));

View File

@ -0,0 +1,21 @@
{
"name": "@examples/with-keep-alive-react",
"private": true,
"version": "1.0.0",
"scripts": {
"start": "ice start",
"build": "ice build"
},
"dependencies": {
"react": "0.0.0-experimental-0cdfef19b-20231211",
"react-dom": "0.0.0-experimental-0cdfef19b-20231211"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2"
},
"resolutions": {
"react": "0.0.0-experimental-0cdfef19b-20231211",
"react-dom": "0.0.0-experimental-0cdfef19b-20231211"
}
}

View File

@ -0,0 +1,3 @@
import { defineAppConfig } from 'ice';
export default defineAppConfig(() => ({}));

View File

@ -0,0 +1,22 @@
import { Meta, Title, Links, Main, Scripts } from 'ice';
function Document() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="description" content="ICE Demo" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Title />
<Links />
</head>
<body>
<Main />
<Scripts />
</body>
</html>
);
}
export default Document;

View File

@ -0,0 +1,18 @@
import { Link } from 'ice';
import Counter from '@/components/Counter';
export default function Home() {
return (
<main>
<h2>Home</h2>
<Counter />
<Link to="/about">About</Link>
</main>
);
}
export function pageConfig() {
return {
title: 'Home',
};
}

View File

@ -0,0 +1,10 @@
import { KeepAliveOutlet } from 'ice';
export default function Layout() {
return (
<>
<h1>I'm Keep Alive</h1>
<KeepAliveOutlet />
</>
);
}

View File

@ -0,0 +1,32 @@
{
"compileOnSave": false,
"buildOnSave": false,
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"module": "esnext",
"target": "es6",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"rootDir": "./",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": false,
"importHelpers": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"],
"ice": [".ice"]
}
},
"include": ["src", ".ice", "ice.config.*"],
"exclude": ["build", "public"]
}

View File

@ -7,15 +7,13 @@
"build": "ice build"
},
"dependencies": {
"react": "0.0.0-experimental-0cdfef19b-20231211",
"react-dom": "0.0.0-experimental-0cdfef19b-20231211"
"@ice/app": "workspace:*",
"@ice/runtime": "workspace:*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2"
},
"resolutions": {
"react": "0.0.0-experimental-0cdfef19b-20231211",
"react-dom": "0.0.0-experimental-0cdfef19b-20231211"
}
}

View File

@ -0,0 +1,11 @@
import { useState } from 'react';
export default function Count() {
const [count, setCount] = useState(0);
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
</div>
);
}

View File

@ -0,0 +1,12 @@
import { Link } from 'ice';
import Count from '@/components/Count';
export default function Home() {
return (
<div>
<h4>Home</h4>
<Count />
<Link to="/">Index</Link>
</div>
);
}

View File

@ -1,18 +1,23 @@
import { Link } from 'ice';
import Counter from '@/components/Counter';
import { useEffect } from 'react';
import { useActive, Link } from 'ice';
import Count from '@/components/Count';
export default function Home() {
const active = useActive();
useEffect(() => {
if (active) {
console.log('Page Index is actived');
} else {
console.log('Page Index is deactived');
}
}, [active]);
return (
<main>
<h2>Home</h2>
<Counter />
<Link to="/about">About</Link>
</main>
<div>
<h4>Index</h4>
<Count />
<Link to="/home">Home</Link>
</div>
);
}
export function pageConfig() {
return {
title: 'Home',
};
}

View File

@ -2,9 +2,9 @@ import { KeepAliveOutlet } from 'ice';
export default function Layout() {
return (
<>
<h1>I'm Keep Alive</h1>
<div>
<h2>Layout</h2>
<KeepAliveOutlet />
</>
</div>
);
}

View File

@ -19,7 +19,6 @@
"noImplicitAny": false,
"importHelpers": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"paths": {
@ -29,4 +28,4 @@
},
"include": ["src", ".ice", "ice.config.*"],
"exclude": ["build", "public"]
}
}

View File

@ -59,6 +59,7 @@ export const RUNTIME_EXPORTS = [
'defineAppConfig',
'useAppData',
'history',
'useActive',
'KeepAliveOutlet',
'useMounted',
'ClientOnly',

View File

@ -0,0 +1,33 @@
import React from 'react';
interface ActivityProps {
mode: string;
children: React.ReactElement | null;
}
interface ActivityContext {
active: boolean;
}
const Context = React.createContext<ActivityContext>(null);
const ActivityProvider = Context.Provider;
export const useActive = () => {
const data = React.useContext(Context);
return data?.active;
};
export default function Activity({ mode, children }: ActivityProps) {
const active = mode === 'visible';
return (
<ActivityProvider value={{
active,
}}
>
{/* Additional wrapper for hidden elements */}
<div style={{ display: active ? 'block' : 'none' }}>
{children}
</div>
</ActivityProvider>
);
}

View File

@ -1,36 +1,58 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useOutlet, useLocation } from 'react-router-dom';
import ActivityComponent from './Activity.js';
// @ts-ignore
const Activity = React.unstable_Activity;
const Activity = React.unstable_Activity || ActivityComponent;
interface ActivityItem {
outlet: React.ReactElement | null;
key: string;
pathname: string;
}
// ref: https://leomyili.github.io/react-stillness-component/docs/examples/react-router/v6
export default function KeepAliveOutlet() {
if (!Activity) {
throw new Error('`<KeepAliveOutlet />` now requires react experimental version. Please install it first.');
}
const [outlets, setOutlets] = useState([]);
const [outlets, setOutlets] = useState<ActivityItem[]>([]);
const location = useLocation();
const outlet = useOutlet();
// Save the first outlet for SSR hydration.
const outletRef = useRef({
key: location.key,
pathname: location.pathname,
outlet,
});
useEffect(() => {
const result = outlets.some(o => o.pathname === location.pathname);
if (!result) {
setOutlets([
...outlets,
{
key: location.key,
pathname: location.pathname,
outlet,
},
]);
// If outlets is empty, save the first outlet for SSR hydration,
// and should not call setOutlets to avoid re-render.
if (outlets.length !== 0 ||
outletRef.current?.pathname !== location.pathname) {
let currentOutlets = outletRef.current ? [outletRef.current] : outlets;
const result = currentOutlets.some(o => o.pathname === location.pathname);
if (!result) {
setOutlets([
// TODO: the max length of outlets should be configurable.
...currentOutlets,
{
key: location.key,
pathname: location.pathname,
outlet,
},
]);
outletRef.current = null;
}
}
}, [location.pathname, location.key, outlet, outlets]);
// Render initail outlet for SSR hydration.
const renderOutlets = outlets.length === 0 ? [outletRef.current] : outlets;
return (
<>
{
outlets.map((o) => {
renderOutlets.map((o) => {
return (
<Activity key={o.key} mode={location.pathname === o.pathname ? 'visible' : 'hidden'}>
{o.outlet}

View File

@ -44,6 +44,7 @@ import AppErrorBoundary from './AppErrorBoundary.js';
import getAppConfig, { defineAppConfig } from './appConfig.js';
import { routerHistory as history } from './history.js';
import KeepAliveOutlet from './KeepAliveOutlet.js';
import { useActive } from './Activity.js';
import ClientOnly from './ClientOnly.js';
import useMounted from './useMounted.js';
import usePageLifecycle from './usePageLifecycle.js';
@ -117,6 +118,7 @@ export {
getRequestContext,
history,
useActive,
KeepAliveOutlet,
AppErrorBoundary,
ClientOnly,

View File

@ -993,6 +993,28 @@ importers:
specifier: ^18.0.6
version: 18.0.11
examples/with-keep-alive:
dependencies:
'@ice/app':
specifier: workspace:*
version: link:../../packages/ice
'@ice/runtime':
specifier: workspace:*
version: link:../../packages/runtime
react:
specifier: ^18.0.0
version: 18.2.0
react-dom:
specifier: ^18.0.0
version: 18.2.0(react@18.2.0)
devDependencies:
'@types/react':
specifier: ^18.0.0
version: 18.0.34
'@types/react-dom':
specifier: ^18.0.2
version: 18.0.11
examples/with-nested-routes:
dependencies:
'@ice/app':

View File

@ -2,4 +2,4 @@ packages:
- 'packages/*'
- 'examples/*'
- 'website'
- '!examples/with-keep-alive/'
- '!examples/with-keep-alive-react/'