Skip to content

Commit

Permalink
feat(react): 新增 renderComponent
Browse files Browse the repository at this point in the history
  • Loading branch information
fjc0k committed Jul 20, 2020
1 parent a368e51 commit ceecb28
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"@types/fs-extra": "9.0.1",
"@types/prompts": "2.0.8",
"@types/react": "16.9.43",
"@types/react-dom": "16.9.8",
"@types/standard-version": "7.0.0",
"codecov": "3.7.1",
"cross-env": "7.0.2",
Expand Down
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from 'react-use'
// @index(['./**/*.ts', '!./**/*.{test,taro}.*', '!./{useToggle,createGlobalState,useTitle,useInterval,useSearchParam,useLocalStorage,useWindowSize}.*'], f => `export * from '${f.path}'`)
export * from './defineComponent'
export * from './isVisibleValue'
export * from './renderComponent'
export * from './useClassName'
export * from './useLoadMore'
export * from './useReachBottom'
Expand Down
121 changes: 121 additions & 0 deletions src/react/renderComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React from 'react'
import { renderComponent } from './renderComponent'
import { wait } from '../utils'

describe('renderComponent', () => {
describe('渲染正常', () => {
function Test(props: { a?: number; b?: number }) {
return !props.a && !props.b ? null : (
<div className='test'>{(props.a || 0) + (props.b || 0)}</div>
)
}

const {
incrementalRerender,
partialRerender,
fullRerender,
destroy,
} = renderComponent(Test, { a: -2 })

const getSum = () =>
Number(document.querySelector('.test')?.innerHTML.trim() || -1)

test('基本渲染', () => {
expect(getSum()).toEqual(-2)
})

test('增量重渲染', () => {
incrementalRerender({ a: 1 })
expect(getSum()).toEqual(1)
incrementalRerender({ b: 2 })
expect(getSum()).toEqual(3)
incrementalRerender({ a: 9 })
expect(getSum()).toEqual(11)
incrementalRerender({})
expect(getSum()).toEqual(11)
})

test('部分重渲染', () => {
partialRerender({ a: 1 })
expect(getSum()).toEqual(1)
partialRerender({ b: 2 })
expect(getSum()).toEqual(0)
partialRerender({ a: 9 })
expect(getSum()).toEqual(9)
partialRerender({})
expect(getSum()).toEqual(-2)
})

test('全量重渲染', () => {
fullRerender({ a: 1 })
expect(getSum()).toEqual(1)
fullRerender({ b: 2 })
expect(getSum()).toEqual(2)
fullRerender({ a: 9 })
expect(getSum()).toEqual(9)
fullRerender({})
expect(getSum()).toEqual(-1)
})

test('销毁组件', () => {
fullRerender({ a: 1 })
expect(getSum()).toEqual(1)
destroy()
expect(getSum()).toEqual(-1)
})
})

describe('逻辑正常', () => {
function Test(props: { onClick?: () => any }) {
return (
<div className='test2' onClick={props.onClick}>
1
</div>
)
}
const handleClick = jest.fn()
const afterClick = jest.fn()
const {
incrementalRerender,
partialRerender,
fullRerender,
} = renderComponent(Test, { onClick: handleClick }, { onClick: afterClick })

test('注入回调正常', async () => {
document.querySelector<HTMLDivElement>('.test2')?.click()
await wait(0)
expect(handleClick).toBeCalled().toBeCalledTimes(1)
expect(afterClick).toBeCalled().toBeCalledTimes(1)
})

test('增量重渲染后注入回调正常', async () => {
const handleClick2 = jest.fn()
incrementalRerender({ onClick: handleClick2 })
document.querySelector<HTMLDivElement>('.test2')?.click()
await wait(0)
expect(handleClick).toBeCalled().toBeCalledTimes(1)
expect(afterClick).toBeCalled().toBeCalledTimes(2)
expect(handleClick2).toBeCalled().toBeCalledTimes(1)
})

test('部分重渲染后注入回调正常', async () => {
const handleClick3 = jest.fn()
partialRerender({ onClick: handleClick3 })
document.querySelector<HTMLDivElement>('.test2')?.click()
await wait(0)
expect(handleClick).toBeCalled().toBeCalledTimes(1)
expect(afterClick).toBeCalled().toBeCalledTimes(3)
expect(handleClick3).toBeCalled().toBeCalledTimes(1)
})

test('全量重渲染后注入回调正常', async () => {
const handleClick4 = jest.fn()
fullRerender({ onClick: handleClick4 })
document.querySelector<HTMLDivElement>('.test2')?.click()
await wait(0)
expect(handleClick).toBeCalled().toBeCalledTimes(1)
expect(afterClick).toBeCalled().toBeCalledTimes(4)
expect(handleClick4).toBeCalled().toBeCalledTimes(1)
})
})
})
100 changes: 100 additions & 0 deletions src/react/renderComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { PickBy } from 'vtils/types'

export interface RenderComponentResult<
TComponent extends React.ComponentType<any>
> {
/**
* 增量重渲染,新属性将与增量属性合并并成为新的增量属性。
*
* @param props 新属性
*/
incrementalRerender(props: Partial<React.ComponentProps<TComponent>>): void

/**
* 部分重渲染,新属性将与初始属性合并。
*
* @param props 新属性
*/
partialRerender(props: Partial<React.ComponentProps<TComponent>>): void

/**
* 完全重渲染,新属性将直接作为全部属性传给组件。
*
* @param props 新属性
*/
fullRerender(props: React.ComponentProps<TComponent>): void

/**
* 销毁组件并移除 DOM 节点。
*/
destroy(): void
}

/**
* 独立渲染一个组件在 document.body 下,常应用于弹窗类组件。
*
* @param Component 要渲染的组件
* @param initialProps 初始属性
* @param injectCallbacks 回调函数注入
*/
export function renderComponent<TComponent extends React.ComponentType<any>>(
Component: TComponent,
initialProps: React.ComponentProps<TComponent>,
injectCallbacks?: PickBy<
React.ComponentProps<TComponent>,
Function | undefined
>,
): RenderComponentResult<TComponent> {
let container: HTMLDivElement | null = document.createElement('div')
document.body.appendChild(container)

const render = (props: Partial<React.ComponentProps<TComponent>>) => {
props = { ...props }
if (injectCallbacks) {
for (const key of Object.keys(injectCallbacks)) {
const originalCallback = props[key]
;(props as any)[key] = async () => {
await originalCallback?.()
await (injectCallbacks as any)[key]()
}
}
}
ReactDOM.render(React.createElement(Component, props), container)
}

render(initialProps)

let incrementalProps: React.ComponentProps<TComponent> = { ...initialProps }

return {
incrementalRerender(props: Partial<React.ComponentProps<TComponent>>) {
if (!container) return
incrementalProps = {
...incrementalProps,
...props,
}
render(incrementalProps)
},
partialRerender(props: Partial<React.ComponentProps<TComponent>>) {
if (!container) return
render({
...initialProps,
...props,
})
},
fullRerender(props: React.ComponentProps<TComponent>) {
if (!container) return
render(props)
},
destroy() {
if (!container) return
const unmountResult = ReactDOM.unmountComponentAtNode(container)
if (unmountResult) {
container.parentNode?.removeChild(container)
}
container = null
},
}
}

0 comments on commit ceecb28

Please sign in to comment.