skip to Main Content

I’m working on a development tool that lets you create React components.

A user will start off with their component looking like:

function MyComponent() {
  var rand = useRef(Math.random());

  return <div>{rand}</div>;
}

but then might want to modify it like so:

function MyComponent() {
  var rand = useRef(Math.random());

  return <span>{rand}</span>;
}

The only meaningful change here was <span>, so ideally for me (to prevent full tree remounts for larger apps/preserve application state), that random number wouldn’t change because the ref would be preserved – but React seems to detect the change in reference to the component definition when considering its identity: MyComponent !== MyComponent (new) and outputs a different random value.

This is similar to implementing HMR (Hot Module Reloading) myself – but constrained to just react component definitions.

Here is a working example to play with: https://jsfiddle.net/83xruyL5/1/

Is there any way of modifying this React behavior with something like a name/id/key attribute on the definition? Or perhaps a more generic JS approach of preserving/reusing a function/component reference while modifying its definition?

I assume this may not be possible, and am just going to be limited by React here, but am asking just in case it is.

2

Answers


  1. The useState set function will trigger a re-render anyway, and by default, when a component re-renders, React re-renders all of its children recursively. In some cases, if you need to skip the re-rendering of components, useMomo could help.

    Also, avoiding some re-computation for this example is possible by passing refs or state props.

    Pass ref:

    const { useState, useRef } = React;
    
    const AComponent = ({ rand }) => <div>{rand.current} A</div>
    const BComponent= ({ rand }) => <span>{rand.current} B</span>
    
    let isAComponent    
    
    const getComponent = () => {
     const MyComponent = isAComponent? BComponent : AComponent
     isAComponent = !isAComponent
     return MyComponent
     }
    
    function App() {      
      const [MyComponent, setMyComponent] = useState(getComponent);
      const rand = useRef(Math.random());   
      
        return (
        <div>
          {React.createElement(MyComponent,{ rand })}
          <div 
            onClick={() => {
              setMyComponent(getComponent)          
            }}
          >hotswap component</div>
           <div 
            onClick={() => {
              rand.current = Math.random()
            }}
          >change value for next hotswap</div>
        </div>
      );
    }
    
    ReactDOM.render(<App />, document.querySelector("#app"))
    

    Pass state:

    const { useState, useRef } = React;
    
    
    const AComponent = ({ rand }) => <div>{rand} A</div>
    const BComponent = ({ rand }) => <span>{rand} B</span>
    
    let isAComponent    
    
    const getComponent = () => {
     const MyComponent = isAComponent? BComponent : AComponent
     isAComponent = !isAComponent
     return MyComponent
     }
    
    function App() {      
      const [MyComponent, setMyComponent] = useState(getComponent);
      const [rand, setRand] = useState(Math.random());   
      
        return (
        <div>
          {React.createElement(MyComponent,{ rand })}
          <div 
            onClick={() => {
              setMyComponent(getComponent)          
            }}
          >hotswap component</div>
           <div 
            onClick={() => {
              setRand(Math.random())
            }}
          >change value</div>
        </div>
      );
    }
    
    ReactDOM.render(<App />, document.querySelector("#app"))
    
    Login or Signup to reply.
  2. You could use

    function Wrapper(props) {
        const [Component, setComponent] = useState(MyComponent);
        return Component(props);
    }
    

    Calling setComponent(MyComponent2) will re-render the wrapper, with the new Component but the same ref. The trick is to call Component as a function not to create a <Component {...props}> element. Of course, this will fail horribly if the old and new component use different hooks.


    Instead of this hack,
    I suggest you look into the internals of React to see how they implemented hot module reloading themselves. There is react-refresh to allow "Fast Refresh", provided by the React team.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search