;
type Page = 'page';
function NavLink(props: P) {
- const { to, isActive, ...rest } = props;
+ const { to, isActive, exact, strict, sensitive, className, activeClassName, ...rest } = props;
const context = useContext(Context);
const toLocation = typeof to === 'function' ? to(context.location) : to;
const { pathname } = typeof toLocation === 'string' ? parsePath(toLocation) : toLocation;
- const match = pathname ? matchPath(context.location.pathname, pathname) : null;
+ const match = pathname ? matchPath(context.location.pathname, pathname, {
+ exact: exact,
+ strictMode: strict,
+ caseSensitive: sensitive,
+ }) : null;
- const isLinkActive = match && isActive ? isActive(match, context.location) : false;
+ const isLinkActive = !!(isActive ? isActive(match, context.location) : match);
+
+ let classNames = typeof className === 'function' ? className(isLinkActive) : className;
+ if (isLinkActive) {
+ classNames = [activeClassName, classNames].filter(Boolean).join('');
+ }
const page: Page = 'page';
const otherProps = {
- 'aria-current': isLinkActive ? page : false,
+ className: classNames,
+ 'aria-current': isLinkActive ? page : undefined,
...rest,
};
diff --git a/packages/inula-router/src/router/Route.tsx b/packages/inula-router/src/router/Route.tsx
index fdf2db7f..ddd7c195 100644
--- a/packages/inula-router/src/router/Route.tsx
+++ b/packages/inula-router/src/router/Route.tsx
@@ -43,15 +43,19 @@ export type RouteProps
= {}, Path extends string =
function Route = GetURLParams>(props: RouteProps) {
const context = useContext(RouterContext);
- const { computed, location, path } = props;
- let { children, component, render } = props;
+ const { computed, location, path, component, render, strict, sensitive, exact } = props;
+ let { children } = props;
let match: Matched
| null;
const routeLocation = location || context.location;
if (computed) {
match = computed;
} else if (path) {
- match = matchPath
(routeLocation.pathname, path);
+ match = matchPath
(routeLocation.pathname, path, {
+ strictMode: strict,
+ caseSensitive: sensitive,
+ exact: exact,
+ });
} else {
match = context.match;
}
diff --git a/packages/inula-router/src/router/Router.tsx b/packages/inula-router/src/router/Router.tsx
index dd9791a1..1dcdf7e6 100644
--- a/packages/inula-router/src/router/Router.tsx
+++ b/packages/inula-router/src/router/Router.tsx
@@ -29,22 +29,27 @@ function Router
(props: P) {
const { history, children = null } = props;
const [location, setLocation] = useState(props.history.location);
const pendingLocation = useRef(null);
+ const unListen = useRef void)>(null);
+ const isMount = useRef(false);
// 在Router加载时就监听history地址变化,以保证在始渲染时重定向能正确触发
- const unListen = useRef void)>(
- history.listen(arg => {
+ if (unListen.current === null) {
+ unListen.current = history.listen(arg => {
pendingLocation.current = arg.location;
- }),
- );
+ });
+ }
// 模拟componentDidMount和componentWillUnmount
useLayoutEffect(() => {
+ isMount.current = true;
if (unListen.current) {
unListen.current();
}
// 监听history中的位置变化
unListen.current = history.listen(arg => {
- setLocation(arg.location);
+ if (isMount.current) {
+ setLocation(arg.location);
+ }
});
if (pendingLocation.current) {
@@ -53,6 +58,7 @@ function Router(props: P) {
return () => {
if (unListen.current) {
+ isMount.current = false;
unListen.current();
unListen.current = null;
pendingLocation.current = null;
diff --git a/packages/inula-router/src/router/matcher/parser.ts b/packages/inula-router/src/router/matcher/parser.ts
index 0598066c..ea75e4e5 100644
--- a/packages/inula-router/src/router/matcher/parser.ts
+++ b/packages/inula-router/src/router/matcher/parser.ts
@@ -40,7 +40,7 @@ export type Matched
= {
const defaultOption: Required = {
// url匹配时是否大小写敏感
- caseSensitive: true,
+ caseSensitive: false,
// 是否严格匹配url结尾的/
strictMode: false,
// 是否完全精确匹配
diff --git a/packages/inula-router/src/router/withRouter.tsx b/packages/inula-router/src/router/withRouter.tsx
index 3d7f58bd..04c588c8 100644
--- a/packages/inula-router/src/router/withRouter.tsx
+++ b/packages/inula-router/src/router/withRouter.tsx
@@ -20,10 +20,11 @@ import RouterContext from './context';
function withRouter(Component: C) {
function ComponentWithRouterProp(props: any) {
+ const { wrappedComponentRef, ...rest } = props;
const { history, location, match } = useContext(RouterContext);
const routeProps = { history: history, location: location, match: match };
- return ;
+ return ;
}
return ComponentWithRouterProp;
diff --git a/packages/inula/package.json b/packages/inula/package.json
index 2eb04d4b..54ae4908 100644
--- a/packages/inula/package.json
+++ b/packages/inula/package.json
@@ -24,5 +24,13 @@
"watch-test": "yarn test --watch --dev"
},
"files": ["build/**/*", "README.md"],
- "types": "./build/@types/index.d.ts"
+ "types": "./build/@types/index.d.ts",
+ "exports": {
+ ".": {
+ "default": "./index.js"
+ },
+ "./package.json":"./package.json",
+ "./jsx-runtime": "./jsx-runtime.js",
+ "./jsx-dev-runtime": "./jsx-dev-runtime.js"
+ }
}
diff --git a/packages/inula/scripts/__tests__/ComponentTest/FragmentComponent.test.js b/packages/inula/scripts/__tests__/ComponentTest/FragmentComponent.test.js
index c91c5dac..6ac08b89 100644
--- a/packages/inula/scripts/__tests__/ComponentTest/FragmentComponent.test.js
+++ b/packages/inula/scripts/__tests__/ComponentTest/FragmentComponent.test.js
@@ -473,4 +473,61 @@ describe('Fragment', () => {
expect(LogUtils.getNotClear()).toEqual([]);
expect(container.textContent).toBe('1');
});
+
+ it('Fragment 设置相同的key不会重新渲染组件', () => {
+ const { useState, useRef, act, Fragment } = Inula;
+ let setFn;
+ const didMount = jest.fn();
+ const didUpdate = jest.fn();
+
+ const App = () => {
+ const [list, setList] = useState([
+ { text: 'Apple', id: 1 },
+ { text: 'Banana', id: 2 },
+ ]);
+
+ setFn = setList;
+
+ return (
+ <>
+ {list.map(item => {
+ return (
+
+
+
+ );
+ })}
+ >
+ );
+ };
+
+ const Child = ({ val }) => {
+ const mount = useRef(false);
+ useEffect(() => {
+ if (!mount.current) {
+ didMount();
+ mount.current = true;
+ } else {
+ didUpdate();
+ }
+ });
+
+ return {val}
;
+ };
+
+ act(() => Inula.render(, container));
+
+ expect(didMount).toHaveBeenCalledTimes(2);
+ act(() => {
+ setFn([
+ { text: 'Apple', id: 1 },
+ { text: 'Banana', id: 2 },
+ { text: 'Grape', id: 3 },
+ ]);
+ });
+
+ // 数组前两项Key不变子组件更新,第三个子组件会挂载
+ expect(didMount).toHaveBeenCalledTimes(3);
+ expect(didUpdate).toHaveBeenCalledTimes(2);
+ });
});
diff --git a/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js b/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js
index ef17a0aa..0b5d18a8 100644
--- a/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js
+++ b/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js
@@ -14,7 +14,6 @@
*/
import * as Inula from '../../../src/index';
-const { useState } = Inula;
describe('FunctionComponent Test', () => {
it('渲染无状态组件', () => {
const App = props => {
@@ -90,39 +89,4 @@ describe('FunctionComponent Test', () => {
Inula.render(, container);
expect(container.querySelector('div').style['_values']['--max-segment-num']).toBe(10);
});
-
- it('函数组件渲染中重新发生setState不触发整个应用重新更新', () => {
- function CountLabel({ count }) {
- const [prevCount, setPrevCount] = useState(count);
- const [trend, setTrend] = useState(null);
- if (prevCount !== count) {
- setPrevCount(count);
- setTrend(count > prevCount ? 'increasing' : 'decreasing');
- }
- return (
- <>
- {count}
-
- {trend && The count is {trend}
}
- >
- );
- }
-
- let count = 0;
- function Child({ trend }) {
- count++;
- return {trend}
;
- }
-
- let update;
- function App() {
- const [count, setCount] = useState(0);
- update = setCount;
- return ;
- }
-
- Inula.render(, container);
- update(1);
- expect(count).toBe(2);
- })
});
diff --git a/packages/inula/scripts/__tests__/ComponentTest/StrictMode.test.js b/packages/inula/scripts/__tests__/ComponentTest/StrictMode.test.js
new file mode 100644
index 00000000..a12fbed4
--- /dev/null
+++ b/packages/inula/scripts/__tests__/ComponentTest/StrictMode.test.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2023 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import * as Inula from '../../../src/index';
+import { getLogUtils } from '../jest/testUtils';
+
+describe('StrictMode Component test', () => {
+ const LogUtils = getLogUtils();
+ const { useState, useEffect, useRef, render, act } = Inula;
+ it('StrictMode is same to Fragment', () => {
+ const Parent = () => {
+ const [, setS] = useState('1');
+ return (
+
+
+
+
+ );
+ };
+
+ const Child = () => {
+ const isMount = useRef(false);
+
+ useEffect(() => {
+ if (!isMount.current) {
+ LogUtils.log('didMount');
+ isMount.current = true;
+ } else {
+ LogUtils.log('didUpdate');
+ }
+ });
+
+ return null;
+ };
+
+ act(() => render(, container));
+ // 子组件初始化,会挂载一次
+ expect(LogUtils.getAndClear()).toStrictEqual(['didMount']);
+ const button = container.querySelector('#btn');
+ // 父组件State更新,子组件也会更新一次
+ act(() => button.click());
+ expect(LogUtils.getAndClear()).toStrictEqual(['didUpdate']);
+ });
+});
diff --git a/packages/inula/src/dom/DOMExternal.ts b/packages/inula/src/dom/DOMExternal.ts
index 076709e9..12dea7b1 100644
--- a/packages/inula/src/dom/DOMExternal.ts
+++ b/packages/inula/src/dom/DOMExternal.ts
@@ -21,6 +21,7 @@ import { findDOMByClassInst } from '../renderer/vnode/VNodeUtils';
import { listenSimulatedDelegatedEvents } from '../event/EventBinding';
import { Callback } from '../renderer/Types';
import { InulaNode } from '../types';
+import { EVENT_KEY } from './DOMInternalKeys';
function createRoot(children: any, container: Container, callback?: Callback) {
// 清空容器
@@ -89,7 +90,7 @@ function findDOMNode(domOrEle?: Element): null | Element | Text {
// 情况根节点监听器
function removeRootEventLister(container: Container) {
- const events = (container as any).$EV;
+ const events = (container as any)[EVENT_KEY];
if (events) {
Object.keys(events).forEach(event => {
const listener = events[event];
diff --git a/packages/inula/src/dom/DOMInternalKeys.ts b/packages/inula/src/dom/DOMInternalKeys.ts
index a8727261..c4f1824f 100644
--- a/packages/inula/src/dom/DOMInternalKeys.ts
+++ b/packages/inula/src/dom/DOMInternalKeys.ts
@@ -22,9 +22,12 @@ import type { Container, Props } from './DOMOperator';
import { DomComponent, DomText, TreeRoot } from '../renderer/vnode/VNodeTags';
-const INTERNAL_VNODE = '_inula_VNode';
-const INTERNAL_PROPS = '_inula_Props';
-const INTERNAL_NONDELEGATEEVENTS = '_inula_NonDelegatedEvents';
+const randomKey = Math.random().toString(16).slice(2);
+const INTERNAL_VNODE = `_inula_VNode_${randomKey}`;
+const INTERNAL_PROPS = `_inula_Props_${randomKey}`;
+const INTERNAL_NONDELEGATEEVENTS = `_inula_nonDelegatedEvents_${randomKey}`;
+export const HANDLER_KEY = `_inula_valueChangeHandler_${randomKey}`;
+export const EVENT_KEY = `_inula_ev_${randomKey}`;
// 通过 VNode 实例获取 DOM 节点
export function getDom(vNode: VNode): Element | Text | null {
diff --git a/packages/inula/src/dom/valueHandler/ValueChangeHandler.ts b/packages/inula/src/dom/valueHandler/ValueChangeHandler.ts
index cd07c35c..262dc837 100644
--- a/packages/inula/src/dom/valueHandler/ValueChangeHandler.ts
+++ b/packages/inula/src/dom/valueHandler/ValueChangeHandler.ts
@@ -18,7 +18,7 @@
* 只有值发生变化时才会触发change事件。
*/
-const HANDLER_KEY = '_valueChangeHandler';
+import { HANDLER_KEY } from '../DOMInternalKeys';
// 判断是否是 check 类型
function isCheckType(dom: HTMLInputElement): boolean {
diff --git a/packages/inula/src/event/EventBinding.ts b/packages/inula/src/event/EventBinding.ts
index f3e9f58e..52bfffcd 100644
--- a/packages/inula/src/event/EventBinding.ts
+++ b/packages/inula/src/event/EventBinding.ts
@@ -18,7 +18,7 @@
*/
import { allDelegatedInulaEvents, portalDefaultDelegatedEvents, simulatedDelegatedEvents } from './EventHub';
import { isDocument } from '../dom/utils/Common';
-import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys';
+import { EVENT_KEY, getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys';
import { asyncUpdates, runDiscreteUpdates } from '../renderer/TreeBuilder';
import { handleEventMain } from './InulaEventMain';
import { decorateNativeEvent } from './EventWrapper';
@@ -73,8 +73,8 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) {
const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent;
// 事件存储在DOM节点属性,避免多个VNode(root和portal)对应同一个DOM, 造成事件重复监听
- currentRoot.realNode.$EV = currentRoot.realNode.$EV ?? {};
- const events = currentRoot.realNode.$EV;
+ currentRoot.realNode[EVENT_KEY] = currentRoot.realNode[EVENT_KEY] ?? {};
+ const events = currentRoot.realNode[EVENT_KEY];
if (!events[nativeFullName]) {
events[nativeFullName] = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture);
diff --git a/packages/inula/src/event/InulaEventMain.ts b/packages/inula/src/event/InulaEventMain.ts
index cff523fc..817618f4 100644
--- a/packages/inula/src/event/InulaEventMain.ts
+++ b/packages/inula/src/event/InulaEventMain.ts
@@ -142,7 +142,13 @@ function triggerInulaEvents(
const target = nativeEvent.target || nativeEvent.srcElement!;
// 触发普通委托事件
- const listenerList: ListenerUnitList = getCommonListeners(nativeEvtName, vNode, nativeEvent, target, isCapture);
+ const listenerList: ListenerUnitList = getCommonListeners(
+ nativeEvtName,
+ vNode,
+ nativeEvent as MouseEvent,
+ target,
+ isCapture
+ );
let mouseEnterListeners: ListenerUnitList = [];
if (inulaEventToNativeMap.get('onMouseEnter')!.includes(nativeEvtName)) {
diff --git a/packages/inula/src/inulax/adapters/redux.ts b/packages/inula/src/inulax/adapters/redux.ts
index 97ee92f1..99e57748 100644
--- a/packages/inula/src/inulax/adapters/redux.ts
+++ b/packages/inula/src/inulax/adapters/redux.ts
@@ -91,6 +91,10 @@ function mergeData(state, data) {
}
export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: StoreEnhancer): ReduxStoreHandler {
+ if (typeof preloadedState === 'function' && typeof enhancers === 'undefined') {
+ enhancers = preloadedState;
+ preloadedState = undefined;
+ }
const store = createStoreX({
id: 'defaultStore',
state: { stateWrapper: preloadedState },
@@ -107,6 +111,7 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?:
return;
} // NOTE: reducer should never return undefined, in this case, do not change state
state.stateWrapper = result;
+ return action;
},
},
options: {
diff --git a/packages/inula/src/renderer/diff/nodeDiffComparator.ts b/packages/inula/src/renderer/diff/nodeDiffComparator.ts
index 8de6f392..e7161174 100644
--- a/packages/inula/src/renderer/diff/nodeDiffComparator.ts
+++ b/packages/inula/src/renderer/diff/nodeDiffComparator.ts
@@ -15,7 +15,7 @@
import type { VNode } from '../Types';
import { FlagUtils } from '../vnode/VNodeFlags';
-import { TYPE_COMMON_ELEMENT, TYPE_FRAGMENT, TYPE_PORTAL } from '../../external/JSXElementType';
+import { TYPE_COMMON_ELEMENT, TYPE_FRAGMENT, TYPE_PORTAL, TYPE_STRICT_MODE } from '../../external/JSXElementType';
import { DomText, DomPortal, Fragment, DomComponent } from '../vnode/VNodeTags';
import {
updateVNode,
@@ -35,9 +35,9 @@ enum DiffCategory {
ARR_NODE = 'ARR_NODE',
}
-// 检查是不是被 FRAGMENT 包裹
-function isNoKeyFragment(child: any) {
- return child != null && child.type === TYPE_FRAGMENT && child.key === null;
+// 检查是不是被 FRAGMENT 或 StrictMode 包裹
+function isNoKeyFragmentOrStrictMode(child: any) {
+ return child != null && (child.type === TYPE_FRAGMENT || child.type === TYPE_STRICT_MODE) && child.key === null;
}
// 清除单个节点
@@ -159,7 +159,7 @@ function getNewNode(parentNode: VNode, newChild: any, oldNode: VNode | null) {
const key = oldNode !== null ? oldNode.key : newChild.key;
resultNode = createFragmentVNode(key, newChild.props.children);
} else {
- resultNode = updateVNode(oldNode, newChild);
+ resultNode = updateVNode(oldNode, newChild.props.children);
}
break;
}
@@ -631,7 +631,7 @@ export function createChildrenByDiff(
newChild: any,
isComparing: boolean
): VNode | null {
- const isFragment = isNoKeyFragment(newChild);
+ const isFragment = isNoKeyFragmentOrStrictMode(newChild);
newChild = isFragment ? newChild.props.children : newChild;
// 1. 没有新节点,直接把vNode标记为删除
diff --git a/packages/inula/src/renderer/hooks/HookMain.ts b/packages/inula/src/renderer/hooks/HookMain.ts
index c2943f49..48f39f42 100644
--- a/packages/inula/src/renderer/hooks/HookMain.ts
+++ b/packages/inula/src/renderer/hooks/HookMain.ts
@@ -18,19 +18,12 @@ import type { VNode } from '../Types';
import { getLastTimeHook, setLastTimeHook, setCurrentHook, getNextHook } from './BaseHook';
import { HookStage, setHookStage } from './HookStage';
-const NESTED_UPDATE_LIMIT = 50;
-// state updated in render phrase
-let hasUpdatedInRender = false;
function resetGlobalVariable() {
setHookStage(null);
setLastTimeHook(null);
setCurrentHook(null);
}
-export function markUpdatedInRender() {
- hasUpdatedInRender = true;
-}
-
// hook对外入口
export function runFunctionWithHooks, Arg>(
funcComp: (props: Props, arg: Arg) => any,
@@ -52,14 +45,8 @@ export function runFunctionWithHooks, Arg>(
setHookStage(HookStage.Update);
}
- let comp = funcComp(props, arg);
+ const comp = funcComp(props, arg);
- if (hasUpdatedInRender) {
- resetGlobalVariable();
- processing.oldHooks = processing.hooks;
- setHookStage(HookStage.Update);
- comp = runFunctionAgain(funcComp, props, arg);
- }
// 设置hook阶段为null,用于判断hook是否在函数组件中调用
setHookStage(null);
@@ -76,22 +63,3 @@ export function runFunctionWithHooks, Arg>(
return comp;
}
-
-function runFunctionAgain, Arg>(
- funcComp: (props: Props, arg: Arg) => any,
- props: Props,
- arg: Arg
-) {
- let reRenderTimes = 0;
- let childElements;
- while (hasUpdatedInRender) {
- reRenderTimes++;
- if (reRenderTimes > NESTED_UPDATE_LIMIT) {
- throw new Error('Too many setState called in function component');
- }
- hasUpdatedInRender = false;
- childElements = funcComp(props, arg);
- }
-
- return childElements;
-}
diff --git a/packages/inula/src/renderer/hooks/UseReducerHook.ts b/packages/inula/src/renderer/hooks/UseReducerHook.ts
index e0ae2f3a..190d185f 100644
--- a/packages/inula/src/renderer/hooks/UseReducerHook.ts
+++ b/packages/inula/src/renderer/hooks/UseReducerHook.ts
@@ -21,7 +21,6 @@ import { setStateChange } from '../render/FunctionComponent';
import { getHookStage, HookStage } from './HookStage';
import type { VNode } from '../Types';
import { getProcessingVNode } from '../GlobalVar';
-import { markUpdatedInRender } from './HookMain';
// 构造新的Update数组
function insertUpdate(action: A, hook: Hook): Update {