Skip to content
This repository has been archived by the owner on Sep 19, 2023. It is now read-only.

Optimize React-in-Vue of immediate react children. #41

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/VuePlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import VueResolver from './resolvers/Vue'
/**
* This mixin automatically wraps all React components into Vue.
*/

const reactRegistry = {}

export default {
install (Vue, options) {
/**
Expand All @@ -13,15 +16,20 @@ export default {
const originalComponentsMergeStrategy = Vue.config.optionMergeStrategies.components
Vue.config.optionMergeStrategies.components = function (parent, ...args) {
const mergedValue = originalComponentsMergeStrategy(parent, ...args)

const wrappedComponents = mergedValue
? Object.entries(mergedValue).reduce(
(acc, [k, v]) => ({
? Object.entries(mergedValue).reduce((acc, [k, v]) => {
const components = {
...acc,
[k]: isReactComponent(v) ? VueResolver(v) : v,
}),
{}
)
[k]: isReactComponent(v) ? VueResolver(v, reactRegistry) : v
}
if (isReactComponent(v)) {
reactRegistry[k] = v
}
return components
}, {})
: mergedValue

return Object.assign(parent, wrappedComponents)
}
},
Expand Down
3 changes: 2 additions & 1 deletion src/resolvers/Vue.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ReactWrapper } from '../'

export default function VueResolver (component) {
export default function VueResolver (component, reactRegistry) {
return {
components: { ReactWrapper },
props: ['passedProps'],
Expand All @@ -11,6 +11,7 @@ export default function VueResolver (component) {
{
props: {
component,
reactRegistry,
passedProps: this.$props.passedProps,
},
attrs: this.$attrs,
Expand Down
65 changes: 61 additions & 4 deletions src/wrappers/React.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import React from 'react'
import ReactDOM from 'react-dom'
import Vue from 'vue'
import VueWrapper from './Vue'

const makeVueContainer = () => {
return class ReactInReact extends VueWrapper {};
}

const makeReactContainer = Component => {
return class ReactInVue extends React.Component {
static displayName = `ReactInVue${Component.displayName || Component.name || 'Component'}`
Expand All @@ -23,32 +28,80 @@ const makeReactContainer = Component => {
}
}

render () {
wrapReactChildren(children, reactChildren) {
return children.map((child, index) => {
const wrappedChildren = this.wrapVueChildren(child.componentOptions.children)
const childrenComponent = child.componentOptions.children
? React.createElement(makeVueContainer(), { component: wrappedChildren })
: null
return React.createElement(
reactChildren[index],
{
key: `${this.constructor.displayName}Child${index}`,
...child.componentOptions.propsData
},
childrenComponent
)
})
}

render() {
const {
children,
// Vue attaches an event handler, but it is missing an event name, so
// it ends up using an empty string. Prevent passing an empty string
// named prop to React.
'': _invoker,
__vueraReactChildren,
...rest
} = this.state
const wrappedChildren = this.wrapVueChildren(children)

return (
<Component {...rest}>
{children && <VueWrapper component={wrappedChildren} />}
{(children && __vueraReactChildren.length === children.length)
? this.wrapReactChildren(children, __vueraReactChildren)
: (children && <VueWrapper component={wrappedChildren} />)}
</Component>
)
}
}
}

const camelizeRE = /-(\w)/g
const camelize = function(str) {
return str.replace(camelizeRE, function(_, c) {
return c ? c.toUpperCase() : ''
})
}
const capitalize = function(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}

export default {
props: ['component', 'passedProps'],
props: ['component', 'passedProps', 'reactRegistry'],
render (createElement) {
return createElement('div', { ref: 'react' })
},
methods: {
computeReactChildren () {
const reactChildren = []
if (this.$slots.default && this.$slots.default.length) {
this.$slots.default.forEach(child => {
if (child.componentOptions && child.componentOptions.tag) {
const id = child.componentOptions.tag
const camelizedId = camelize(id)
const pascalCaseId = capitalize(camelizedId)
reactChildren.push(
this.reactRegistry[id] ||
this.reactRegistry[camelizedId] ||
this.reactRegistry[pascalCaseId]
)
}
})
}
return reactChildren
},
mountReactComponent (component) {
const Component = makeReactContainer(component)
const children = this.$slots.default !== undefined ? { children: this.$slots.default } : {}
Expand All @@ -59,6 +112,7 @@ export default {
{...this.$listeners}
{...children}
ref={ref => (this.reactComponentRef = ref)}
__vueraReactChildren={this.computeReactChildren()}
/>,
this.$refs.react
)
Expand All @@ -76,7 +130,10 @@ export default {
* `$slots` or `$children`.
*/
if (this.$slots.default !== undefined) {
this.reactComponentRef.setState({ children: this.$slots.default })
this.reactComponentRef.setState({
children: this.$slots.default,
__vueraReactChildren: this.computeReactChildren()
})
} else {
this.reactComponentRef.setState({ children: null })
}
Expand Down
14 changes: 9 additions & 5 deletions src/wrappers/Vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export default class VueContainer extends React.Component {
this.vueInstance.$destroy()
}

componentName() {
return `vuera-internal-component-name-${this.vueraComponentName}`
}

/**
* Creates and mounts the Vue instance.
* NOTE: since we need to access the current instance of VueContainer, as well as the Vue instance
Expand All @@ -67,16 +71,16 @@ export default class VueContainer extends React.Component {
data: props,
render (createElement) {
return createElement(
VUE_COMPONENT_NAME,
reactThisBinding.componentName(),
{
props: this.$data,
props: this.$data
},
[wrapReactChildren(createElement, this.children)]
)
},
components: {
[VUE_COMPONENT_NAME]: component,
'vuera-internal-react-wrapper': ReactWrapper,
[reactThisBinding.componentName()]: component,
'vuera-internal-react-wrapper': ReactWrapper
},
})
}
Expand All @@ -87,7 +91,7 @@ export default class VueContainer extends React.Component {
/**
* Replace the component in the Vue instance and update it.
*/
this.vueInstance.$options.components[VUE_COMPONENT_NAME] = nextComponent
this.vueInstance.$options.components[this.componentName()] = nextComponent
this.vueInstance.$forceUpdate()
}

Expand Down