Reputation: 1847
This is sorta complicated to explain but here goes:
I have 2 npm packages that I've created that get imported and rendered in a non-React rails project. I've recently made some modifications to allow this to be used in another more modern react project where they're using react 16 and hooks.
The first package, we'll call ComponentA can be used standalone or nested inside the second, which we'll call ComponentB. Both ComponentA and ComponentB were created in react 15.x and were classes and not using hooks.
After some fighting with this and trying to not have to change any of the child components we got both these packages working using Context and Providers. ComponentA works standalone and ComponentB works standalone with ComponentA nested within it.
So after some debugging I think I have a theory. It's sorta looking like it might be the multiple versions of react. I have webpack setup for both ComponentA and ComponentB to use peerDependencies and rely it's parent's react. (or at least think I did) But I'm getting conflicting stories between projects.
When running ComponentB in development w/ ComponentA nested within it, I added window.React# = React
to both and then checked React1 === React2
in the js console and got true.
When npm link
ing ComponentB into my Rails app I added another window.React#
in there and now all the different react variables are not equal. This also happened when I published ComponentA and B and used the published versions in my Rails app. I am thoroughly confused.
Setting up Context/Provider
import React from "react"
ComponentAContext = React.createContext({})
export class ComponentAProvider extends React.Component
constructor: (props) ->
super(props)
this.state = {...}
get_context: ->
Object.assign({ ... }, this.state, this.props)
render: ->
ctx = this.get_context()
<ComponentAContext.Provider value={ctx}>
{this.props.children}
</ComponentA.Provider>
export default ComponentAContext
Actually using Context and Provider for Component
import { useContext, useEffect, useRef } from "react"
import ReactDOM from 'react-dom'
import ComponentAContext, { ComponentAProvider } from "./contexts/componentAContext"
ComponentA = (props) =>
ctx = useContext(ComponentAContext)
{ ... } = ctx
useEffect () =>
...
, [attr_changed]
<div>
Hello World
{props.children}
</div>
# just a wrapper for provider and component
ComponentAPackage = (props) =>
dup_props = Object.assign({}, props)
children = props.children
delete dup_props.children
<ComponentAProvider {...dup_props}>
<ComponentA>
{children}
</ComponentA>
</ComponentAProvider>
export { ComponentA, ComponentAPackage }
Setting up Context/Provider
import React from "react"
ComponentBContext = React.createContext({})
export class ComponentBProvider extends React.Component
constructor: (props) ->
super(props)
this.state = {}
render: () ->
ctx = {
...
}
<ComponentBContext.Provider value={ Object.assign(ctx, this.state) }>
{this.props.children}
</ComponentBContext.Provider>
export default ComponentBContext
Actually using Context and Provider for Component
import React, { useContext, useEffect } from "react"
import ReactDOM from 'react-dom'
import { ComponentAPackage } from 'componentA' # npm package
import ComponentBContext, { ComponentBProvider } from "./contexts/componentBContext"
ComponentB = () =>
ctx = useContext(ComponentBContext)
{ showCompA } = ctx
useEffect () =>
...
, [changed_attr]
if showCompA
<div>
With nested
<ComponentAPackage />
</div>
else
<div>Standalone</div>
class ComponentBPlugin
constructor: (configs, elm) ->
compB = <ComponentBProvider {...configs}><ComponentB /></ComponentBProvider>
ReactDOM.render compB, elm
this
export { ComponentBPlugin, ComponentB }
I'm trying to keep this a simple as I can, but it's a bit of a complicated scenario. So running ComponentB simply using the new ComponentBPlugin({...}, elm)
works just fine, but adding this to an existing Ruby on Rails project ComponentA barfs on the hook while ComponentB is fine. If I replace <ComponentA />
with just a <div />
everything renders without an error in the rails project.
Uncaught Error: Minified React error #321; visit https://reactjs.org/docs/error-decoder.html?invariant=321 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
React is in peerDependencies
on both ComponentA and ComponentB and is in the package.json of the Rails project. They're all expecting [email protected]
{ ComponentBPlugin } = require("componentB") # installed npm package
setup = {...}
compB = new ComponentBPlugin setup, $(".some-div")[0]
I know there's a lot going on here but I'm trying to allow a legacy project and a new React project to use the same component(s) and state and trying to avoid rewriting too much due to time constraints and preventing a possible large amount of bugs on the legacy project already in production.
Thanks in advance
Upvotes: 2
Views: 793
Reputation: 1847
So after about ~2 days of debugging this I finally found the issue. It was a problem with multiple instantiated reacts. They were all the same version but it was being instantiated twice.
Turns out that the Rails app and ComponentB were sharing the same instance but ComponentA nested in ComponentB had it's own instance of react. Here's my theory: ComponentA was a dependency of ComponentB and both ComponentA and ComponentB have react as a peerDependency, so both were looking elsewhere for their react. When ComponentB was compiled/bundled for npm webpack saw one of it's dependencies (ComponentA) requiring react and went ahead and bundled react along side it. This was while ComponentB was still planning on getting it's react from whatever was going to include it. (Rails App)
TL; DR
ComponentA and React got bundled inside ComponentB and ComponentA relied on this while ComponentB was using react from the Rails App.
The solution was to make ComponentA a peerDependency of ComponentB and both ComponentA and ComponentB dependencies of the rails app. This way both ComponentA/B will be looking to the rails app for their react.
I feel like there should be a way to handle this situation as I originally had it and would really like a solution but for now this works for my situation and I'm moving on.
Upvotes: 3