Sparkmasterflex
Sparkmasterflex

Reputation: 1847

React 16 hooks don't work in nested npm package

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.

EDIT:

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 linking 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.

END EDIT

ComponentA

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 }

ComponentB

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]

Rails project JS

  { 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

Answers (1)

Sparkmasterflex
Sparkmasterflex

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.

Cause

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.

Solution

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.

Final Note

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

Related Questions