From 8f8df908f62ab716a8292f6ac8057df7c3a6e8b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 26 Dec 2022 14:45:54 +0800 Subject: [PATCH 01/12] chore: clean up --- .dumirc.ts | 2 +- HISTORY.md | 14 --- docs/demo/follow.md | 8 -- docs/demo/point.md | 8 -- docs/examples/follow.tsx | 92 ------------------- docs/examples/point.tsx | 45 ---------- docs/examples/simple.tsx | 177 +----------------------------------- docs/index.md | 4 +- index.js | 5 -- now.json | 2 +- package.json | 20 ++--- src/Align.tsx | 187 --------------------------------------- src/hooks/useBuffer.tsx | 39 -------- src/index.ts | 6 -- src/interface.ts | 60 ------------- src/util.ts | 60 ------------- tsconfig.json | 2 +- 17 files changed, 14 insertions(+), 717 deletions(-) delete mode 100644 HISTORY.md delete mode 100644 docs/demo/follow.md delete mode 100644 docs/demo/point.md delete mode 100644 docs/examples/follow.tsx delete mode 100644 docs/examples/point.tsx delete mode 100644 index.js delete mode 100644 src/Align.tsx delete mode 100644 src/hooks/useBuffer.tsx delete mode 100644 src/interface.ts delete mode 100644 src/util.ts diff --git a/.dumirc.ts b/.dumirc.ts index 6d269dd..72baed9 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'dumi'; export default defineConfig({ themeConfig: { - name: 'Align', + name: 'Context', }, mfsu: false, }); \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md deleted file mode 100644 index 383ae19..0000000 --- a/HISTORY.md +++ /dev/null @@ -1,14 +0,0 @@ -# History ----- - -## 2.4.0 / 2018-06-04 - -- support point align - -## 2.3.4 / 2017-04-17 - -- fix `createClass` and `PropTypes` warning. - -## 2.3.0 / 2016-05-26 - -- add forceAlign method diff --git a/docs/demo/follow.md b/docs/demo/follow.md deleted file mode 100644 index c75cf5b..0000000 --- a/docs/demo/follow.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Follow -nav: - title: Demo - path: /demo ---- - - \ No newline at end of file diff --git a/docs/demo/point.md b/docs/demo/point.md deleted file mode 100644 index 1a62de8..0000000 --- a/docs/demo/point.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Point -nav: - title: Demo - path: /demo ---- - - \ No newline at end of file diff --git a/docs/examples/follow.tsx b/docs/examples/follow.tsx deleted file mode 100644 index 63928d5..0000000 --- a/docs/examples/follow.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import Align from '../../src'; - -const Demo = () => { - const [width, setWidth] = React.useState(100); - const [height, setHeight] = React.useState(100); - const [left, setLeft] = React.useState(100); - const [top, setTop] = React.useState(100); - const [visible, setVisible] = React.useState(true); - const [svg, setSvg] = React.useState(false); - - const sharedStyle: React.CSSProperties = { - width, - height, - position: 'absolute', - left, - top, - display: visible ? 'flex' : 'none', - }; - - return ( -
- - - - -
- {svg ? ( - - - - ) : ( -
- Content -
- )} - - document.getElementById('content')} align={{ points: ['tc', 'bc'] }}> -
- Popup -
-
-
-
- ); -}; - -export default Demo; diff --git a/docs/examples/point.tsx b/docs/examples/point.tsx deleted file mode 100644 index 9623b1f..0000000 --- a/docs/examples/point.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { Component } from 'react'; -import Align from '../../src'; - -const align = { - points: ['cc', 'cc'], -}; - -class Demo extends Component { - state = { - point: null, - }; - - onClick = ({ pageX, pageY }) => { - this.setState({ point: { pageX, pageY } }); - }; - - render() { - return ( -
-
- Click this region please : ) -
- - -
- Align -
-
-
- ); - } -} - -export default Demo; diff --git a/docs/examples/simple.tsx b/docs/examples/simple.tsx index 05c6be9..3d507b8 100644 --- a/docs/examples/simple.tsx +++ b/docs/examples/simple.tsx @@ -1,176 +1 @@ -import Align, { type RefAlign } from 'rc-align'; -import React, { Component } from 'react'; - -const allPoints = ['tl', 'tc', 'tr', 'cl', 'cc', 'cr', 'bl', 'bc', 'br']; - -interface TestState { - monitor: boolean; - random: boolean; - disabled: boolean; - randomWidth: number; - align: any; - sourceWidth: number; -} - -class Test extends Component<{}, TestState> { - state = { - monitor: true, - random: false, - disabled: false, - randomWidth: 100, - align: { - points: ['cc', 'cc'], - }, - sourceWidth: 50, - }; - - id: NodeJS.Timer; - $container: HTMLElement; - $align: RefAlign; - - componentDidMount() { - this.id = setInterval(() => { - const { random } = this.state; - if (random) { - this.setState({ - randomWidth: 60 + 40 * Math.random(), - }); - } - }, 1000); - } - - componentWillUnmount() { - clearInterval(this.id); - } - - getTarget = () => { - if (!this.$container) { - // parent ref not attached - this.$container = document.getElementById('container'); - } - return this.$container; - }; - - containerRef = ele => { - this.$container = ele; - }; - - alignRef = node => { - this.$align = node; - }; - - toggleMonitor = () => { - this.setState(({ monitor }) => ({ - monitor: !monitor, - })); - }; - - toggleRandom = () => { - this.setState(({ random }) => ({ - random: !random, - })); - }; - - toggleDisabled = () => { - this.setState(({ disabled }) => ({ - disabled: !disabled, - })); - }; - - randomAlign = () => { - const randomPoints = []; - randomPoints.push(allPoints[Math.floor(Math.random() * 100) % allPoints.length]); - randomPoints.push(allPoints[Math.floor(Math.random() * 100) % allPoints.length]); - this.setState({ - align: { - points: randomPoints, - }, - }); - }; - - forceAlign = () => { - this.$align.forceAlign(); - }; - - toggleSourceSize = () => { - this.setState({ - // eslint-disable-next-line react/no-access-state-in-setstate - sourceWidth: this.state.sourceWidth + 10, - }); - }; - - render() { - const { random, randomWidth } = this.state; - - return ( -
-

- -     - -     - -     - - - -

-
- -
- -
-
-
-
- ); - } -} - -export default Test; +export default () => 'Hello World'; diff --git a/docs/index.md b/docs/index.md index 59385d5..4f52b4f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ --- hero: - title: rc-align - description: align ui component for react + title: @rc-component/context + description: Context Selector for perf enhancement --- \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index 79eb820..0000000 --- a/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -// export this package's api -import Align from './src/'; -export default Align; diff --git a/now.json b/now.json index 15f6864..0b0bc3f 100644 --- a/now.json +++ b/now.json @@ -1,6 +1,6 @@ { "version": 2, - "name": "rc-align", + "name": "rc-context", "builds": [ { "src": "package.json", diff --git a/package.json b/package.json index 8d8fcec..041c981 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,19 @@ { - "name": "rc-align", - "version": "4.0.15", - "description": "align ui component for react", + "name": "@rc-component/context", + "version": "1.0.0-alpha.0", + "description": "React way perf context selector", "keywords": [ "react", "react-component", - "react-align", - "align" + "context" ], - "homepage": "http://github.com/react-component/align", + "homepage": "http://github.com/react-component/context", "bugs": { - "url": "http://github.com/react-component/align/issues" + "url": "http://github.com/react-component/context/issues" }, "repository": { "type": "git", - "url": "git@github.com:react-component/align.git" + "url": "git@github.com:react-component/context.git" }, "license": "MIT", "author": "", @@ -37,10 +36,7 @@ }, "dependencies": { "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "dom-align": "^1.7.0", - "rc-util": "^5.26.0", - "resize-observer-polyfill": "^1.5.1" + "rc-util": "^5.26.0" }, "devDependencies": { "@rc-component/father-plugin": "^1.0.0", diff --git a/src/Align.tsx b/src/Align.tsx deleted file mode 100644 index 3a075da..0000000 --- a/src/Align.tsx +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Removed props: - * - childrenProps - */ - -import { alignElement, alignPoint } from 'dom-align'; -import isEqual from 'rc-util/lib/isEqual'; -import addEventListener from 'rc-util/lib/Dom/addEventListener'; -import isVisible from 'rc-util/lib/Dom/isVisible'; -import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; -import { composeRef } from 'rc-util/lib/ref'; -import React from 'react'; - -import useBuffer from './hooks/useBuffer'; -import type { AlignResult, AlignType, TargetPoint, TargetType } from './interface'; -import { isSamePoint, monitorResize, restoreFocus } from './util'; - -type OnAlign = (source: HTMLElement, result: AlignResult) => void; - -export interface AlignProps { - align: AlignType; - target: TargetType; - onAlign?: OnAlign; - monitorBufferTime?: number; - monitorWindowResize?: boolean; - disabled?: boolean; - children: React.ReactElement; -} - -export interface RefAlign { - forceAlign: () => void; -} - -function getElement(func: TargetType) { - if (typeof func !== 'function') return null; - return func(); -} - -function getPoint(point: TargetType) { - if (typeof point !== 'object' || !point) return null; - return point; -} - -const Align: React.ForwardRefRenderFunction = ( - { children, disabled, target, align, onAlign, monitorWindowResize, monitorBufferTime = 0 }, - ref, -) => { - const cacheRef = React.useRef<{ element?: HTMLElement; point?: TargetPoint; align?: AlignType }>( - {}, - ); - - /** Popup node ref */ - const nodeRef = React.useRef(); - let childNode = React.Children.only(children); - - // ===================== Align ====================== - // We save the props here to avoid closure makes props ood - const forceAlignPropsRef = React.useRef<{ - disabled?: boolean; - target?: TargetType; - align?: AlignType; - onAlign?: OnAlign; - }>({}); - forceAlignPropsRef.current.disabled = disabled; - forceAlignPropsRef.current.target = target; - forceAlignPropsRef.current.align = align; - forceAlignPropsRef.current.onAlign = onAlign; - - const [forceAlign, cancelForceAlign] = useBuffer(() => { - const { - disabled: latestDisabled, - target: latestTarget, - align: latestAlign, - onAlign: latestOnAlign, - } = forceAlignPropsRef.current; - - const source = nodeRef.current; - - if (!latestDisabled && latestTarget && source) { - let result: AlignResult; - const element = getElement(latestTarget); - const point = getPoint(latestTarget); - - cacheRef.current.element = element; - cacheRef.current.point = point; - cacheRef.current.align = latestAlign; - - // IE lose focus after element realign - // We should record activeElement and restore later - const { activeElement } = document; - - // We only align when element is visible - if (element && isVisible(element)) { - result = alignElement(source, element, latestAlign); - } else if (point) { - result = alignPoint(source, point, latestAlign); - } - - restoreFocus(activeElement, source); - - if (latestOnAlign && result) { - latestOnAlign(source, result); - } - - return true; - } - - return false; - }, monitorBufferTime); - - // ===================== Effect ===================== - // Handle props change - const [element, setElement] = React.useState(); - const [point, setPoint] = React.useState(); - - useLayoutEffect(() => { - setElement(getElement(target)); - setPoint(getPoint(target)); - }); - - React.useEffect(() => { - if ( - cacheRef.current.element !== element || - !isSamePoint(cacheRef.current.point, point) || - !isEqual(cacheRef.current.align, align) - ) { - forceAlign(); - } - }); - - // Watch popup element resize - React.useEffect(() => { - const cancelFn = monitorResize(nodeRef.current, forceAlign); - return cancelFn; - }, [nodeRef.current]); - - // Watch target element resize - React.useEffect(() => { - const cancelFn = monitorResize(element, forceAlign); - return cancelFn; - }, [element]); - - // Listen for disabled change - React.useEffect(() => { - if (!disabled) { - forceAlign(); - } else { - cancelForceAlign(); - } - }, [disabled]); - - // Listen for window resize - React.useEffect(() => { - if (monitorWindowResize) { - const cancelFn = addEventListener(window, 'resize', forceAlign); - - return cancelFn.remove; - } - }, [monitorWindowResize]); - - // Clear all if unmount - React.useEffect( - () => () => { - cancelForceAlign(); - }, - [], - ); - - // ====================== Ref ======================= - React.useImperativeHandle(ref, () => ({ - forceAlign: () => forceAlign(true), - })); - - // ===================== Render ===================== - if (React.isValidElement(childNode)) { - childNode = React.cloneElement(childNode, { - ref: composeRef((childNode as any).ref, nodeRef), - }); - } - - return childNode; -}; - -const RcAlign = React.forwardRef(Align); -RcAlign.displayName = 'Align'; - -export default RcAlign; diff --git a/src/hooks/useBuffer.tsx b/src/hooks/useBuffer.tsx deleted file mode 100644 index 72cc711..0000000 --- a/src/hooks/useBuffer.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -export default (callback: (force?: boolean) => boolean, buffer: number) => { - const calledRef = React.useRef(false); - const timeoutRef = React.useRef(null); - - function cancelTrigger() { - window.clearTimeout(timeoutRef.current); - } - - function trigger(force?: boolean) { - cancelTrigger(); - - if (!calledRef.current || force === true) { - if (callback(force) === false) { - // Not delay since callback cancelled self - return; - } - - calledRef.current = true; - timeoutRef.current = window.setTimeout(() => { - calledRef.current = false; - }, buffer); - } else { - timeoutRef.current = window.setTimeout(() => { - calledRef.current = false; - trigger(); - }, buffer); - } - } - - return [ - trigger, - () => { - calledRef.current = false; - cancelTrigger(); - }, - ]; -}; diff --git a/src/index.ts b/src/index.ts index 020f8e8..e69de29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +0,0 @@ -// export this package's api -import Align from './Align'; - -export type { RefAlign } from './Align'; - -export default Align; diff --git a/src/interface.ts b/src/interface.ts deleted file mode 100644 index ab91f17..0000000 --- a/src/interface.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** Two char of 't' 'b' 'c' 'l' 'r'. Example: 'lt' */ -export type AlignPoint = string; - -export interface AlignType { - /** - * move point of source node to align with point of target node. - * Such as ['tr','cc'], align top right point of source node with center point of target node. - * Point can be 't'(top), 'b'(bottom), 'c'(center), 'l'(left), 'r'(right) */ - points?: AlignPoint[]; - /** - * offset source node by offset[0] in x and offset[1] in y. - * If offset contains percentage string value, it is relative to sourceNode region. - */ - offset?: number[]; - /** - * offset target node by offset[0] in x and offset[1] in y. - * If targetOffset contains percentage string value, it is relative to targetNode region. - */ - targetOffset?: number[]; - /** - * If adjustX field is true, will adjust source node in x direction if source node is invisible. - * If adjustY field is true, will adjust source node in y direction if source node is invisible. - */ - overflow?: { - adjustX?: boolean | number; - adjustY?: boolean | number; - }; - /** - * Whether use css right instead of left to position - */ - useCssRight?: boolean; - /** - * Whether use css bottom instead of top to position - */ - useCssBottom?: boolean; - /** - * Whether use css transform instead of left/top/right/bottom to position if browser supports. - * Defaults to false. - */ - useCssTransform?: boolean; -} - -export interface AlignResult { - points: AlignPoint[]; - offset: number[]; - targetOffset: number[]; - overflow: { - adjustX: boolean | number; - adjustY: boolean | number; - }; -} - -export interface TargetPoint { - clientX?: number; - clientY?: number; - pageX?: number; - pageY?: number; -} - -export type TargetType = (() => HTMLElement) | TargetPoint; diff --git a/src/util.ts b/src/util.ts deleted file mode 100644 index 30b1f4b..0000000 --- a/src/util.ts +++ /dev/null @@ -1,60 +0,0 @@ -import ResizeObserver from 'resize-observer-polyfill'; -import contains from 'rc-util/lib/Dom/contains'; -import type { TargetPoint } from './interface'; - -export function isSamePoint(prev: TargetPoint, next: TargetPoint) { - if (prev === next) return true; - if (!prev || !next) return false; - - if ('pageX' in next && 'pageY' in next) { - return prev.pageX === next.pageX && prev.pageY === next.pageY; - } - - if ('clientX' in next && 'clientY' in next) { - return prev.clientX === next.clientX && prev.clientY === next.clientY; - } - - return false; -} - -export function restoreFocus(activeElement, container) { - // Focus back if is in the container - if ( - activeElement !== document.activeElement && - contains(container, activeElement) && - typeof activeElement.focus === 'function' - ) { - activeElement.focus(); - } -} - -export function monitorResize(element: HTMLElement, callback: Function) { - let prevWidth: number = null; - let prevHeight: number = null; - - function onResize([{ target }]: ResizeObserverEntry[]) { - if (!document.documentElement.contains(target)) return; - const { width, height } = target.getBoundingClientRect(); - const fixedWidth = Math.floor(width); - const fixedHeight = Math.floor(height); - - if (prevWidth !== fixedWidth || prevHeight !== fixedHeight) { - // https://webkit.org/blog/9997/resizeobserver-in-webkit/ - Promise.resolve().then(() => { - callback({ width: fixedWidth, height: fixedHeight }); - }); - } - - prevWidth = fixedWidth; - prevHeight = fixedHeight; - } - - const resizeObserver = new ResizeObserver(onResize); - if (element) { - resizeObserver.observe(element); - } - - return () => { - resizeObserver.disconnect(); - }; -} diff --git a/tsconfig.json b/tsconfig.json index 9e4d9bf..22fce2e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "@@/*": [ "src/.umi/*" ], - "rc-align": [ + "@rc-component/context": [ "src/index.ts" ] } From 34c8b022a74b1efa97995486d1fd202301b43023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 26 Dec 2022 15:56:45 +0800 Subject: [PATCH 02/12] feat: basic func --- docs/examples/simple.tsx | 47 ++++++++++++++- package.json | 2 +- src/context.tsx | 122 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 5 ++ 4 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 src/context.tsx diff --git a/docs/examples/simple.tsx b/docs/examples/simple.tsx index 3d507b8..108e97a 100644 --- a/docs/examples/simple.tsx +++ b/docs/examples/simple.tsx @@ -1 +1,46 @@ -export default () => 'Hello World'; +import { createContext, useContextSelector } from '@rc-component/context'; +import React from 'react'; + +const CountContext = createContext<{ + cnt1: number; + cnt2: number; +}>(); + +const useRenderTimes = () => { + const renderRef = React.useRef(0); + renderRef.current += 1; + + return renderRef.current; +}; + +const MyConsumer = React.memo(({ name }: { name: any }) => { + const value = useContextSelector(CountContext, name); + const renderTimes = useRenderTimes(); + + return ( +
+ + {value} ({renderTimes} times) +
+ ); +}); + +export default () => { + const [cnt1, setCnt1] = React.useState(0); + const [cnt2, setCnt2] = React.useState(0); + const renderTimes = useRenderTimes(); + + return ( + + + + {renderTimes} times + + + + ); +}; diff --git a/package.json b/package.json index 041c981..40e6443 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@babel/runtime": "^7.10.1", - "rc-util": "^5.26.0" + "rc-util": "^5.27.0" }, "devDependencies": { "@rc-component/father-plugin": "^1.0.0", diff --git a/src/context.tsx b/src/context.tsx new file mode 100644 index 0000000..5ee8d4a --- /dev/null +++ b/src/context.tsx @@ -0,0 +1,122 @@ +import useEvent from 'rc-util/lib/hooks/useEvent'; +import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; +import isEqual from 'rc-util/lib/isEqual'; +import * as React from 'react'; +import { unstable_batchedUpdates } from 'react-dom'; + +export type Selector = ( + value: ContextProps, +) => SelectorValue; + +export type Trigger = (value: ContextProps) => void; + +export type Listeners = Set>; + +export interface Context { + getValue: () => ContextProps; + listeners: Listeners; +} + +export interface ContextSelectorProviderProps { + value: T; + children?: React.ReactNode; +} + +export interface SelectorContext { + Context: React.Context>; + Provider: React.ComponentType>; +} + +export function createContext( + defaultContext?: ContextProps, +): SelectorContext { + const Context = React.createContext>(defaultContext as any); + + const Provider = ({ value, children }: ContextSelectorProviderProps) => { + const valueRef = React.useRef(value); + valueRef.current = value; + + const [context] = React.useState>(() => ({ + getValue: () => valueRef.current, + listeners: new Set(), + })); + + useLayoutEffect(() => { + unstable_batchedUpdates(() => { + context.listeners.forEach(listener => { + listener(value); + }); + }); + }, [value]); + + return {children}; + }; + + return { Context, Provider }; +} + +/** e.g. useSelect(userContext, user => user.name) => user.name */ +export function useContextSelector( + holder: SelectorContext, + selector: Selector, +): SelectorValue; + +/** e.g. useSelect(userContext, ['name', 'age']) => user { name, age } */ +export function useContextSelector>( + holder: SelectorContext, + selector: (keyof ContextProps)[], +): SelectorValue; + +/** e.g. useSelect(userContext, 'name') => user.name */ +export function useContextSelector( + holder: SelectorContext, + selector: PropName, +): ContextProps[PropName]; + +export function useContextSelector( + holder: SelectorContext, + selector: Selector | (keyof ContextProps)[] | keyof ContextProps, +) { + const eventSelector = useEvent>( + typeof selector === 'function' + ? selector + : ctx => { + if (!Array.isArray(selector)) { + return ctx[selector]; + } + + const obj = {} as SelectorValue; + selector.forEach(key => { + (obj as any)[key] = ctx[key]; + }); + return obj; + }, + ); + const context = React.useContext(holder?.Context); + const { listeners, getValue } = context || {}; + + const valueRef = React.useRef(); + valueRef.current = eventSelector(context ? getValue() : null); + const [, forceUpdate] = React.useState({}); + + useLayoutEffect(() => { + if (!context) { + return; + } + + function trigger(nextValue: ContextProps) { + const nextSelectorValue = eventSelector(nextValue); + if (!isEqual(valueRef.current, nextSelectorValue, true)) { + forceUpdate({}); + } + } + + listeners.add(trigger); + + return () => { + listeners.delete(trigger); + }; + }, [context]); + + return valueRef.current; +} diff --git a/src/index.ts b/src/index.ts index e69de29..da03336 100644 --- a/src/index.ts +++ b/src/index.ts @@ -0,0 +1,5 @@ +import type { SelectorContext } from './context'; +import { createContext, useContextSelector } from './context'; + +export { createContext, useContextSelector }; +export type { SelectorContext }; From eb6bd25ccfb7d3076b5582daff0be805d6212967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 26 Dec 2022 16:02:25 +0800 Subject: [PATCH 03/12] docs: update --- docs/examples/simple.tsx | 8 +------- docs/examples/useRenderTimes.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 docs/examples/useRenderTimes.ts diff --git a/docs/examples/simple.tsx b/docs/examples/simple.tsx index 108e97a..8e87462 100644 --- a/docs/examples/simple.tsx +++ b/docs/examples/simple.tsx @@ -1,18 +1,12 @@ import { createContext, useContextSelector } from '@rc-component/context'; import React from 'react'; +import useRenderTimes from './useRenderTimes'; const CountContext = createContext<{ cnt1: number; cnt2: number; }>(); -const useRenderTimes = () => { - const renderRef = React.useRef(0); - renderRef.current += 1; - - return renderRef.current; -}; - const MyConsumer = React.memo(({ name }: { name: any }) => { const value = useContextSelector(CountContext, name); const renderTimes = useRenderTimes(); diff --git a/docs/examples/useRenderTimes.ts b/docs/examples/useRenderTimes.ts new file mode 100644 index 0000000..d9bfa0f --- /dev/null +++ b/docs/examples/useRenderTimes.ts @@ -0,0 +1,10 @@ +import React from 'react'; + +const useRenderTimes = () => { + const renderRef = React.useRef(0); + renderRef.current += 1; + + return renderRef.current; +}; + +export default useRenderTimes; From 87c369ecb4b3851c08b6f6e7f37b1e222b989573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 26 Dec 2022 16:38:37 +0800 Subject: [PATCH 04/12] feat: add immutable --- src/Immutable.tsx | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/Immutable.tsx diff --git a/src/Immutable.tsx b/src/Immutable.tsx new file mode 100644 index 0000000..22bd167 --- /dev/null +++ b/src/Immutable.tsx @@ -0,0 +1,45 @@ +import { supportRef } from 'rc-util/lib/ref'; +import * as React from 'react'; + +const RenderContext = React.createContext(0); + +export function makeImmutable>(Component: T): T { + const refAble = supportRef(Component); + + const ImmutableComponent = function (props: any, ref: any) { + const refProps = refAble ? { ref } : {}; + const renderTimesRef = React.useRef(0); + renderTimesRef.current += 1; + + return ( + + + + ); + }; + + if (process.env.NODE_ENV !== 'production') { + ImmutableComponent.displayName = `ImmutableRoot(${Component.displayName || Component.name})`; + } + + return refAble ? React.forwardRef(ImmutableComponent) : (ImmutableComponent as any); +} + +export function responseImmutable>(Component: T): T { + const refAble = supportRef(Component); + + const ImmutableComponent = function (props: any, ref: any) { + const refProps = refAble ? { ref } : {}; + const renderTimes = React.useContext(RenderContext); + + return React.useMemo(() => , [renderTimes]); + }; + + if (process.env.NODE_ENV !== 'production') { + ImmutableComponent.displayName = `ImmutableResponse(${ + Component.displayName || Component.name + })`; + } + + return refAble ? React.forwardRef(ImmutableComponent) : (ImmutableComponent as any); +} From 921a08f9f09bd5489ec31a4c39a84e060dbbe554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 26 Dec 2022 16:55:44 +0800 Subject: [PATCH 05/12] docs: immutable wrapper --- docs/demo/immutable.md | 8 +++++ docs/examples/immutable.tsx | 66 +++++++++++++++++++++++++++++++++++++ src/index.ts | 3 +- 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 docs/demo/immutable.md create mode 100644 docs/examples/immutable.tsx diff --git a/docs/demo/immutable.md b/docs/demo/immutable.md new file mode 100644 index 0000000..c268809 --- /dev/null +++ b/docs/demo/immutable.md @@ -0,0 +1,8 @@ +--- +title: Immutable +nav: + title: Demo + path: /demo +--- + + \ No newline at end of file diff --git a/docs/examples/immutable.tsx b/docs/examples/immutable.tsx new file mode 100644 index 0000000..f13be2f --- /dev/null +++ b/docs/examples/immutable.tsx @@ -0,0 +1,66 @@ +import { + createContext, + makeImmutable, + responseImmutable, + useContextSelector, +} from '@rc-component/context'; +import React from 'react'; +import useRenderTimes from './useRenderTimes'; + +const AppContext = createContext<{ + appCnt: number; + appUpdateCnt: number; +}>(); + +const MyApp = ({ rootCnt, children }: { rootCnt: number; children?: React.ReactNode }) => { + const [appCnt, setAppCnt] = React.useState(0); + const [appUpdateCnt, setAppUpdateCnt] = React.useState(0); + const renderTimes = useRenderTimes(); + + return ( + + + + App Render Times: {renderTimes} / Root CNT: {rootCnt} +
{children}
+
+ ); +}; +const ImmutableMyApp = makeImmutable(MyApp); + +const MyComponent = ({ name }: { name: any }) => { + const renderTimes = useRenderTimes(); + const value = useContextSelector(AppContext, name); + + return ( +
+ {name}: {value} / Component Render Times: {renderTimes} +
+ ); +}; +const ImmutableMyComponent = responseImmutable(MyComponent); + +export default () => { + const [rootCnt, setRootCnt] = React.useState(0); + const renderTimes = useRenderTimes(); + + return ( + <> + {' '} + + Root RenderTimes: {renderTimes}{' '} +
+ + + + +
+ + ); +}; diff --git a/src/index.ts b/src/index.ts index da03336..57ef14a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import type { SelectorContext } from './context'; import { createContext, useContextSelector } from './context'; +import { makeImmutable, responseImmutable } from './Immutable'; -export { createContext, useContextSelector }; +export { createContext, useContextSelector, makeImmutable, responseImmutable }; export type { SelectorContext }; From bfc84c1d455df90f95e356bf41c53295389a90be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 26 Dec 2022 16:57:08 +0800 Subject: [PATCH 06/12] refactor: use React.memo instead --- src/Immutable.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Immutable.tsx b/src/Immutable.tsx index 22bd167..bcbb384 100644 --- a/src/Immutable.tsx +++ b/src/Immutable.tsx @@ -28,12 +28,12 @@ export function makeImmutable>(Component: T): export function responseImmutable>(Component: T): T { const refAble = supportRef(Component); - const ImmutableComponent = function (props: any, ref: any) { + const ImmutableComponent = React.memo(function (props: any, ref: any) { const refProps = refAble ? { ref } : {}; - const renderTimes = React.useContext(RenderContext); + React.useContext(RenderContext); - return React.useMemo(() => , [renderTimes]); - }; + return ; + }); if (process.env.NODE_ENV !== 'production') { ImmutableComponent.displayName = `ImmutableResponse(${ From a6b85adf2dedea1253e6914513f9fd4e37c863ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 26 Dec 2022 17:28:05 +0800 Subject: [PATCH 07/12] test: Prop Name test --- docs/examples/immutable.tsx | 4 +- docs/examples/simple.tsx | 4 +- src/context.tsx | 19 +++-- src/index.ts | 4 +- tests/common.tsx | 28 +++++++ tests/context.test.tsx | 113 +++++++++++++++++++++++++ tests/element.test.tsx | 162 ------------------------------------ tests/point.test.js | 56 ------------- tests/strict.test.tsx | 78 ----------------- tests/util.test.tsx | 110 ------------------------ 10 files changed, 161 insertions(+), 417 deletions(-) create mode 100644 tests/common.tsx create mode 100644 tests/context.test.tsx delete mode 100644 tests/element.test.tsx delete mode 100644 tests/point.test.js delete mode 100644 tests/strict.test.tsx delete mode 100644 tests/util.test.tsx diff --git a/docs/examples/immutable.tsx b/docs/examples/immutable.tsx index f13be2f..46d5cd6 100644 --- a/docs/examples/immutable.tsx +++ b/docs/examples/immutable.tsx @@ -2,7 +2,7 @@ import { createContext, makeImmutable, responseImmutable, - useContextSelector, + useContext, } from '@rc-component/context'; import React from 'react'; import useRenderTimes from './useRenderTimes'; @@ -34,7 +34,7 @@ const ImmutableMyApp = makeImmutable(MyApp); const MyComponent = ({ name }: { name: any }) => { const renderTimes = useRenderTimes(); - const value = useContextSelector(AppContext, name); + const value = useContext(AppContext, name); return (
diff --git a/docs/examples/simple.tsx b/docs/examples/simple.tsx index 8e87462..7ef11db 100644 --- a/docs/examples/simple.tsx +++ b/docs/examples/simple.tsx @@ -1,4 +1,4 @@ -import { createContext, useContextSelector } from '@rc-component/context'; +import { createContext, useContext } from '@rc-component/context'; import React from 'react'; import useRenderTimes from './useRenderTimes'; @@ -8,7 +8,7 @@ const CountContext = createContext<{ }>(); const MyConsumer = React.memo(({ name }: { name: any }) => { - const value = useContextSelector(CountContext, name); + const value = useContext(CountContext, name); const renderTimes = useRenderTimes(); return ( diff --git a/src/context.tsx b/src/context.tsx index 5ee8d4a..cca0b9e 100644 --- a/src/context.tsx +++ b/src/context.tsx @@ -55,32 +55,41 @@ export function createContext( return { Context, Provider }; } +/** e.g. useSelect(userContext) => user */ +export function useContext( + holder: SelectorContext, +): ContextProps; + /** e.g. useSelect(userContext, user => user.name) => user.name */ -export function useContextSelector( +export function useContext( holder: SelectorContext, selector: Selector, ): SelectorValue; /** e.g. useSelect(userContext, ['name', 'age']) => user { name, age } */ -export function useContextSelector>( +export function useContext>( holder: SelectorContext, selector: (keyof ContextProps)[], ): SelectorValue; /** e.g. useSelect(userContext, 'name') => user.name */ -export function useContextSelector( +export function useContext( holder: SelectorContext, selector: PropName, ): ContextProps[PropName]; -export function useContextSelector( +export function useContext( holder: SelectorContext, - selector: Selector | (keyof ContextProps)[] | keyof ContextProps, + selector?: Selector | (keyof ContextProps)[] | keyof ContextProps, ) { const eventSelector = useEvent>( typeof selector === 'function' ? selector : ctx => { + if (selector === undefined) { + return ctx; + } + if (!Array.isArray(selector)) { return ctx[selector]; } diff --git a/src/index.ts b/src/index.ts index 57ef14a..0adb1aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import type { SelectorContext } from './context'; -import { createContext, useContextSelector } from './context'; +import { createContext, useContext } from './context'; import { makeImmutable, responseImmutable } from './Immutable'; -export { createContext, useContextSelector, makeImmutable, responseImmutable }; +export { createContext, useContext, makeImmutable, responseImmutable }; export type { SelectorContext }; diff --git a/tests/common.tsx b/tests/common.tsx new file mode 100644 index 0000000..1bae1bb --- /dev/null +++ b/tests/common.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +export const useRenderTimes = () => { + const renderRef = React.useRef(0); + renderRef.current += 1; + + return renderRef.current; +}; + +export function RenderTimer({ id }: { id?: string }) { + const renderTimes = useRenderTimes(); + + return ( +
+ {renderTimes} +
+ ); +} + +export function Value({ id, value }: { id?: string; value: any }) { + const str = JSON.stringify(value); + + return ( +
+ {str.replace(/^"/, '').replace(/"$/, '')} +
+ ); +} diff --git a/tests/context.test.tsx b/tests/context.test.tsx new file mode 100644 index 0000000..c1ef872 --- /dev/null +++ b/tests/context.test.tsx @@ -0,0 +1,113 @@ +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { createContext, useContext } from '../src'; +import { RenderTimer, Value } from './common'; + +describe('Basic', () => { + interface User { + name: string; + age: number; + } + + const UserContext = createContext(); + + const Root = ({ children }: { children?: React.ReactNode }) => { + const [name, setName] = React.useState('bamboo'); + const [age, setAge] = React.useState(30); + + return ( + + setName(e.target.value)} /> + setAge(Number(e.target.value))} /> + + {children} + + ); + }; + + function changeValue(container: HTMLElement, id: string, value: string) { + fireEvent.change(container.querySelector(`#${id}`), { + target: { + value, + }, + }); + } + + it('raw', () => { + const Raw = () => { + const user = useContext(UserContext); + + return ( + <> + + + + ); + }; + + const { container } = render( + + + , + ); + + // Mount + expect(container.querySelector('#raw')!.textContent).toEqual('1'); + + // Update `name`: Full Update + changeValue(container, 'name', 'light'); + expect(container.querySelector('#raw')!.textContent).toEqual('2'); + expect(container.querySelector('#value')!.textContent).toEqual( + JSON.stringify({ + name: 'light', + age: 30, + }), + ); + + // Update `age`: Full Update + changeValue(container, 'age', '20'); + expect(container.querySelector('#raw')!.textContent).toEqual('3'); + expect(container.querySelector('#value')!.textContent).toEqual( + JSON.stringify({ + name: 'light', + age: 20, + }), + ); + }); + + it('PropName', () => { + const PropName = ({ name }: { name: keyof User }) => { + const value = useContext(UserContext, name); + + return ( + <> + + + + ); + }; + + const { container } = render( + + + + , + ); + + // Mount + expect(container.querySelector('#name-times')!.textContent).toEqual('1'); + expect(container.querySelector('#age-times')!.textContent).toEqual('1'); + + // Update `name`: Partial Update + changeValue(container, 'name', 'light'); + expect(container.querySelector('#name-times')!.textContent).toEqual('2'); + expect(container.querySelector('#name-value')!.textContent).toEqual('light'); + expect(container.querySelector('#age-times')!.textContent).toEqual('1'); + + // Update `age`: Partial Update + changeValue(container, 'age', '20'); + expect(container.querySelector('#name-times')!.textContent).toEqual('2'); + expect(container.querySelector('#age-times')!.textContent).toEqual('2'); + expect(container.querySelector('#age-value')!.textContent).toEqual('20'); + }); +}); diff --git a/tests/element.test.tsx b/tests/element.test.tsx deleted file mode 100644 index 102765f..0000000 --- a/tests/element.test.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import { render } from '@testing-library/react'; -import { spyElementPrototype } from 'rc-util/lib/test/domHook'; -import React from 'react'; -import { renderToString } from 'react-dom/server'; -import Align from '../src'; - -describe('element align', () => { - beforeAll(() => { - spyElementPrototype(HTMLElement, 'offsetParent', { - get: () => ({}), - }); - }); - - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - jest.useRealTimers(); - }); - - const align = { - points: ['bc', 'tc'], - }; - - class Test extends React.Component { - $target: any; - - getTarget = () => this.$target; - - targetRef = ele => { - this.$target = ele; - }; - - render() { - return ( -
-
- target -
- -
- source -
-
-
- ); - } - } - - it('resize', () => { - const onAlign = jest.fn(); - - const { unmount, rerender } = render(); - expect(onAlign).toHaveBeenCalled(); - - // Window resize - onAlign.mockReset(); - window.dispatchEvent(new Event('resize')); - jest.runAllTimers(); - expect(onAlign).toHaveBeenCalled(); - - // Not listen resize - onAlign.mockReset(); - rerender(); - window.dispatchEvent(new Event('resize')); - jest.runAllTimers(); - expect(onAlign).not.toHaveBeenCalled(); - - // Remove should not crash - rerender(); - unmount(); - }); - - it('disabled should trigger align', () => { - const onAlign = jest.fn(); - - const { rerender } = render(); - expect(onAlign).not.toHaveBeenCalled(); - - rerender(); - jest.runAllTimers(); - expect(onAlign).toHaveBeenCalled(); - }); - - // https://github.com/ant-design/ant-design/issues/31717 - it('changing align should trigger onAlign', () => { - const onAlign = jest.fn(); - const { rerender } = render(); - expect(onAlign).toHaveBeenCalledTimes(1); - expect(onAlign).toHaveBeenLastCalledWith( - expect.any(HTMLElement), - expect.objectContaining({ points: ['cc', 'cc'] }), - ); - // wrapper.setProps({ align: { points: ['cc', 'tl'] } }); - rerender(); - jest.runAllTimers(); - expect(onAlign).toHaveBeenCalledTimes(2); - expect(onAlign).toHaveBeenLastCalledWith( - expect.any(HTMLElement), - expect.objectContaining({ points: ['cc', 'tl'] }), - ); - }); - - it('should switch to the correct align callback after starting the timers', () => { - // This test case is tricky. An error occurs if the following things happen - // exactly in this order: - // * Render with `onAlign1`. - // * The callback in useBuffer is queued using setTimeout, to trigger after - // `monitorBufferTime` ms (which even when it's set to 0 is queued and - // not synchronously executed). - // * The onAlign prop is changed to `onAlign2`. - // * The callback from useBuffer is called. The now correct onAlign - // callback would be `onAlign2`, and `onAlign1` should not be called. - // This changing of the prop in between a 0 ms timeout is extremely rare. - // It does however occur more often in real-world applications with - // react-component/trigger, when its requestAnimationFrame and this timeout - // race against each other. - - const onAlign1 = jest.fn(); - const onAlign2 = jest.fn(); - - const { rerender } = render(); - - // Make sure the initial render's call to onAlign does not matter. - onAlign1.mockReset(); - onAlign2.mockReset(); - - // Re-render the component with the new callback. Expect from here on all - // callbacks to call the new onAlign2. - rerender(); - - // Now the timeout is executed, and we expect the onAlign2 callback to - // receive the call, not onAlign1. - jest.runAllTimers(); - - expect(onAlign1).not.toHaveBeenCalled(); - expect(onAlign2).toHaveBeenCalled(); - }); - - it('SSR no break', () => { - const str = renderToString( - { - throw new Error('Not Call In Render'); - }} - />, - ); - expect(str).toBeTruthy(); - }); -}); -/* eslint-enable */ diff --git a/tests/point.test.js b/tests/point.test.js deleted file mode 100644 index 19ae99b..0000000 --- a/tests/point.test.js +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import { render } from '@testing-library/react'; -import React from 'react'; -import Align from '../src'; - -describe('point align', () => { - function createAlign(props) { - return ( - -
- - ); - } - - it('not pass point', () => { - const onAlign = jest.fn(); - - render( - createAlign({ - align: { points: ['cc'] }, - target: null, - onAlign, - }), - ); - - expect(onAlign).not.toHaveBeenCalled(); - }); - - it('pass point', () => { - jest.useFakeTimers(); - const onAlign = jest.fn(); - - const sharedProps = { - align: { points: ['tc'] }, - target: null, - onAlign, - }; - - const { rerender } = render(createAlign(sharedProps)); - - expect(onAlign).not.toHaveBeenCalled(); - - rerender( - createAlign({ - ...sharedProps, - target: { pageX: 1128, pageY: 903 }, - }), - ); - - jest.runAllTimers(); - expect(onAlign).toHaveBeenCalled(); - - jest.useRealTimers(); - }); -}); -/* eslint-enable */ diff --git a/tests/strict.test.tsx b/tests/strict.test.tsx deleted file mode 100644 index fa87689..0000000 --- a/tests/strict.test.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable class-methods-use-this */ -import { act, render } from '@testing-library/react'; -import { spyElementPrototype } from 'rc-util/lib/test/domHook'; -import React from 'react'; -import Align from '../src'; - -(global as any).watchCnt = 0; - -jest.mock('../src/util', () => { - const originUtil = jest.requireActual('../src/util'); - - return { - ...originUtil, - monitorResize: (...args: any[]) => { - (global as any).watchCnt += 1; - const cancelFn = originUtil.monitorResize(...args); - - return () => { - (global as any).watchCnt -= 1; - cancelFn(); - }; - }, - }; -}); - -describe('element align', () => { - beforeAll(() => { - spyElementPrototype(HTMLElement, 'offsetParent', { - get: () => ({}), - }); - }); - - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - jest.useRealTimers(); - }); - - it('StrictMode should keep resize observer', () => { - const Demo = () => { - const targetRef = React.useRef(null); - - return ( - <> -
- targetRef.current} align={{ points: ['bc', 'tc'] }}> -
- - - ); - }; - - const { unmount } = render( - - - , - ); - - act(() => { - jest.runAllTimers(); - }); - - expect((global as any).watchCnt).toBeGreaterThan(0); - - unmount(); - expect((global as any).watchCnt).toEqual(0); - }); -}); -/* eslint-enable */ diff --git a/tests/util.test.tsx b/tests/util.test.tsx deleted file mode 100644 index 86c415b..0000000 --- a/tests/util.test.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/* eslint-disable @typescript-eslint/no-this-alias */ -import 'resize-observer-polyfill'; -import { isSamePoint, monitorResize } from '../src/util'; - -let observer: any; - - -jest.mock('resize-observer-polyfill', () => { - return class ResizeObserverMock { - onResize: any; - element: any; - - constructor(onResize) { - this.onResize = onResize; - observer = this; - } - - observe(element) { - this.element = element; - } - - disconnect() { - this.element = null; - this.onResize = null; - } - - triggerResize() { - this.onResize([{ target: this.element }]); - } - }; -}); - -describe('util', () => { - describe('isSamePoint', () => { - it('by page', () => { - expect( - isSamePoint( - { pageX: 1, pageY: 2, clientX: 3, clientY: 4 }, - { pageX: 1, pageY: 2, clientX: 1, clientY: 5 }, - ), - ).toBeTruthy(); - expect( - isSamePoint( - { pageX: 1, pageY: 2, clientX: 3, clientY: 4 }, - { pageX: 5, pageY: 6, clientX: 3, clientY: 4 }, - ), - ).toBeFalsy(); - }); - - it('by client', () => { - expect( - isSamePoint( - { pageX: 0, pageY: 2, clientX: 3, clientY: 4 }, - { pageY: 2, clientX: 3, clientY: 4 }, - ), - ).toBeTruthy(); - expect( - isSamePoint({ pageX: 0, pageY: 2, clientX: 3, clientY: 4 }, { clientX: 5, clientY: 4 }), - ).toBeFalsy(); - }); - - it('null should be false', () => { - expect(isSamePoint({ pageX: 0, pageY: 2, clientX: 3, clientY: 4 }, null)).toBeFalsy(); - expect(isSamePoint(null, { pageX: 0, pageY: 2, clientX: 3, clientY: 4 })).toBeFalsy(); - }); - it('2 empty should be false', () => { - expect(isSamePoint({}, {})).toBeFalsy(); - }); - }); - - describe('monitorResize', () => { - let element; - - beforeEach(() => { - element = document.createElement('div'); - element.getBoundingClientRect = jest.fn().mockReturnValueOnce({ - width: 100, - height: 100, - }); - document.body.appendChild(element); - jest.useFakeTimers(); - (global as any).requestAnimationFrame = fn => { - setTimeout(fn, 16); - }; - }); - - afterEach(() => { - if (element) element.remove(); - jest.useRealTimers(); - }); - - it('should defer callback to next frame', async () => { - const callback = jest.fn(); - monitorResize(element, callback); - observer.triggerResize(); - jest.runAllTimers(); - await Promise.resolve(); - expect(callback).toHaveBeenCalled(); - }); - - it('should skip calling if target is removed already', () => { - const callback = jest.fn(); - monitorResize(element, callback); - element.remove(); - observer.triggerResize(); - jest.runAllTimers(); - expect(callback).not.toHaveBeenCalled(); - }); - }); -}); From e05558a8f4ed224d505a015532ecbc325290319c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 26 Dec 2022 17:37:28 +0800 Subject: [PATCH 08/12] test: Context test --- tests/context.test.tsx | 75 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/context.test.tsx b/tests/context.test.tsx index c1ef872..b93f07c 100644 --- a/tests/context.test.tsx +++ b/tests/context.test.tsx @@ -110,4 +110,79 @@ describe('Basic', () => { expect(container.querySelector('#age-times')!.textContent).toEqual('2'); expect(container.querySelector('#age-value')!.textContent).toEqual('20'); }); + + it('PropNameArray', () => { + const PropName = ({ name }: { name: (keyof User)[] }) => { + const value = useContext(UserContext, name); + + return ( + <> + + + + ); + }; + + const { container } = render( + + + + , + ); + + // Mount + expect(container.querySelector('#name-times')!.textContent).toEqual('1'); + expect(container.querySelector('#name_age-times')!.textContent).toEqual('1'); + + // Update `name`: Partial Update + changeValue(container, 'name', 'light'); + expect(container.querySelector('#name-times')!.textContent).toEqual('2'); + expect(container.querySelector('#name-value')!.textContent).toEqual( + JSON.stringify({ name: 'light' }), + ); + expect(container.querySelector('#name_age-times')!.textContent).toEqual('2'); + expect(container.querySelector('#name_age-value')!.textContent).toEqual( + JSON.stringify({ name: 'light', age: 30 }), + ); + + // Update `age`: Partial Update + changeValue(container, 'age', '20'); + expect(container.querySelector('#name-times')!.textContent).toEqual('2'); + expect(container.querySelector('#name_age-times')!.textContent).toEqual('3'); + expect(container.querySelector('#name_age-value')!.textContent).toEqual( + JSON.stringify({ name: 'light', age: 20 }), + ); + }); + + it('function', () => { + const Func = () => { + const value = useContext(UserContext, v => v.name); + + return ( + <> + + + + ); + }; + + const { container } = render( + + + , + ); + + // Mount + expect(container.querySelector('#times')!.textContent).toEqual('1'); + + // Update `name`: Update + changeValue(container, 'name', 'light'); + expect(container.querySelector('#times')!.textContent).toEqual('2'); + expect(container.querySelector('#value')!.textContent).toEqual('light'); + + // Update `age`: Not Update + changeValue(container, 'age', '20'); + expect(container.querySelector('#times')!.textContent).toEqual('2'); + expect(container.querySelector('#value')!.textContent).toEqual('light'); + }); }); From a638ca42a787d85cd1c22cb43c001a1a8712eefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 26 Dec 2022 17:51:12 +0800 Subject: [PATCH 09/12] test: full test --- tests/immutable.test.tsx | 77 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/immutable.test.tsx diff --git a/tests/immutable.test.tsx b/tests/immutable.test.tsx new file mode 100644 index 0000000..d910f17 --- /dev/null +++ b/tests/immutable.test.tsx @@ -0,0 +1,77 @@ +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { createContext, makeImmutable, responseImmutable, useContext } from '../src'; +import { RenderTimer, Value } from './common'; + +describe('Immutable', () => { + const CountContext = createContext(); + + describe('makeImmutable', () => { + const Root = ({ children }: { children?: React.ReactNode; trigger?: string }) => { + const [count, setCount] = React.useState(0); + const [selfState, setSelfState] = React.useState(0); + + return ( + +