12

hey all I want to pass in a ref into my component this way I can access the variables on said compenent like state. only issue is i can't seem to get it to work. It needs to be able to work for both classes and functional

what I am receiving back from the console.log is null every time

  const testRef = createRef();
  console.log(testRef)
  const elementToCopy = singleScreenState.screen.peek().element;
  const Element = React.cloneElement(elementToCopy as ReactElement, { ...elementToCopy?.props, forwardRef: testRef })
keil
  • 395
  • 1
  • 3
  • 15

1 Answers1

8

Refs to a React Element are never populated until the Element in question is mounted. So, the issue was that you were logging too early.

I have an example below running in a function component to demonstrate creating the refs and logging them once the element in question is mounted using useEffect.

The other issue you might have is that based on the code I see, you might be creating the ref in the render function of a class component, which wouldn't work well because you wouldn't have access to the ref variable once it's actually rendered. Typically, you keep ref variable as an instance property of the class so you can access it when you need it.

For working with function components, you need to use forwardRef on the function component as part of it's definition. The forwarded ref can go to the useImperativeHandle hook, or to another element.

A little more info in React's Documentation on Accessing Refs:

When a ref is passed to an element in render, a reference to the node becomes accessible at the current attribute of the ref.

const node = this.myRef.current;

The value of the ref differs depending on the type of the node:

  • When the ref attribute is used on an HTML element, the ref created in the constructor with React.createRef() receives the underlying DOM element as its current property.

  • When the ref attribute is used on a custom class component, the ref object receives the mounted instance of the component as its current.

  • You may not use the ref attribute on function components because they don’t have instances.

That last point is the key to note here: React.ForwardRef allows you to give the function components the ability to decide what a ref should do because otherwise the ref would be meaningless anyway.

In general in class components, if you want to pass a ref through it, you generally have to pass it down with a separate prop name. One of the ways shown here: How to use React.forwardRef in a class based component?

const { useRef, useEffect, useImperativeHandle } = React;

const TestFunction = React.forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    shout: () => console.log("I'm Yelling over here!")
  }));
  return <div>TestFunction</div>;
});

class TestComponent extends React.Component {
  testFunct = () => {
    console.log("testFunct works!");
  };
  render() {
    return <div>TestComponent</div>;
  }
}
function App() {
  const elementRef = useRef();
  const element = <div>Test</div>;
  const clonedElement = React.cloneElement(element, { ref: elementRef });
  const classRef = useRef();
  const classElement = <TestComponent />;
  const clonedClass = React.cloneElement(classElement, { ref: classRef });
  const functionRef = useRef();
  const functionElement = <TestFunction />;
  const clonedFunction = React.cloneElement(functionElement, {
    ref: functionRef
  });

  useEffect(() => {
    console.log('reference to an element',elementRef.current);
    // This produces weird output in the stack overflow console.
    // console.log(classRef.current);
    console.log('function from a reference to a class component', classRef.current.testFunct);
    classRef.current.testFunct();
    console.log('reference to a function component',functionRef.current);
    functionRef.current.shout();
  });
  return (
    <div className="App">
      {clonedElement}
      {clonedClass}
      {clonedFunction}
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

<div id="root" />
Zachary Haber
  • 10,376
  • 1
  • 17
  • 31
  • how would one apply your forward ref to a cloned element? say the function component doesn't have a referenced attached to it so everything needs to be done on clone – keil May 28 '20 at 22:08
  • Unfortunately, if you are working with a function component, you need to have forward ref used on the function component as part of it's definition. The forwarded ref can go to the `useIImperativeHandle` hook, or to another element. A little more info in [React's documentation](https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-to-dom-components). The reason classes work differently is because the ref goes to the class instance itself by default, but that would be meaningless for function components as they don't have an instance in the same way. – Zachary Haber May 28 '20 at 22:14
  • is it possible to check when a cloned element has changed? IE if we clone an element, render said element and that element does an async call that then changes how the component looks or the values inside, what is a possible method to track that change? – keil May 29 '20 at 15:22
  • Add an onChange property to the component definition that the component calls if there's a change of note that you want the parent to be able to know about. Then in the cloning, you can add that onChange property in with the ref. Other than that, there's no real easy way of tracking changes. All data flows down the tree in react, the only way for data to go back up is using callback functions. – Zachary Haber May 29 '20 at 15:32
  • hmm do you think its possible to add a useimperitive at the time of cloning that somehow watches the component and have it trigger on change? the issue is trying to allow anyone to add any component and i take care of all of this for them silently. so im just trying to think of the least intrusive way of watching component changes, hopefully where the customer wouldn't even know its happening in the background – keil May 29 '20 at 15:37
  • `useImperativeHandle` and `React.forwardRef` both require it to be used as part of the definition of the component. Your customer would have to know some things about the process in order to set up the components for it. It might be a good idea to set up parts of it within a custom hook so you can abstract out as much of it as possible. – Zachary Haber May 29 '20 at 15:49
  • https://stackoverflow.com/questions/29041034/how-to-detect-child-renders-in-a-parent-component-in-react-js has some ideas for what you are asking, not all of them are fully fleshed out, though. The [Mutation Observer](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) one seems interesting, but you might need [findDomNode](https://reactjs.org/docs/react-dom.html#finddomnode) or a wrapper element around each child to get access to their DOM element without modifying definitions of the children. Edit: findDomNode doesn't work on function components. – Zachary Haber May 29 '20 at 15:52
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/214920/discussion-between-keil-and-zachary-haber). – keil May 29 '20 at 15:57