pass ref to a class component with React.cloneElement and render prop - reactjs

I'm writing a component that handle some internal state according to a ref of it's child (a mouse event related to that child's ref for example).
This component is using a render-prop to pass on the relevant piece of state to it's child, and render the child with the ref attached via React.cloneElement util.
The problem is that when the child is a class component, for some reason the ref is not available, and i can't find a way to render it as it's a react element object with a type of function (after i clone it of course).
But if the child is just a DOM node like a div for example, it is working as expected.
My work-around is to check the type of the child, and if it is a type of function I'll wrap the cloned element with my own div, if it's just a dom node then render as is.
However, i would like to not wrap the child with an extra div as i don't want to add unnecessary DOM nodes.
Here is a basic code example, most code removed for brevity:
The Parent component:
class Parent extends Component {
attachRef = node => {
this.ref = node;
}
render() {
const { render } = this.props;
const { someValue } = this.state;
const Child = render(someValue);
const WithRef = React.cloneElement(Child, {
ref: this.attachRef
});
if (typeof WithRef.type === 'string') { // node element
return WithRef;
}
else if (typeof WithRef.type === 'function') {
// this is a react element object.. not sure how to render it
// return ?
} else {
// need to find a way to render without a wrapping div
return (
<div ref={this.attachRef}>{Child}</div>
);
}
}
}
The usage:
class App extends Component {
render() {
return (
<div>
<Parent render={someValue => <div> {someValue}</div>} />
<Parent render={someValue => <Menu someValue={someValue} />} />
</div>
);
}
}
When i render regular DOM nodes like the first example it works fine, when i try to render the Menu (which is a class component) it doesn't work as mentioned above.

I had almost an identical issue.
i chose to use findDOMNode from react-dom, you can see the full solution in react-external-click.
Although the warning notes:
findDOMNode is an escape hatch used to access the underlying DOM node.
In most cases, use of this escape hatch is discouraged because it
pierces the component abstraction.
findDOMNode only works on mounted components (that is, components that
have been placed in the DOM). If you try to call this on a component
that has not been mounted yet (like calling findDOMNode() in render()
on a component that has yet to be created) an exception will be
thrown.
findDOMNode cannot be used on functional components.
I think this is the better solution for this particular challenge.
It let's you be "transparent" to the consumer, while being able to target the component in the DOM.
Ok here it is, grabbing the ref:
componentDidMount() {
this.ref = findDOMNode(this);
// some logic ...
}
this is how i use a render function with no wrapper of my own:
render() {
const { children, render } = this.props;
const { clickedOutside } = this.state;
const renderingFunc = render || children;
if (typeof renderingFunc === 'function') {
return renderingFunc(clickedOutside);
} else {
return null
}
}
}

Related

handling props in a deconstructed child

I have a React component that clones its children with additional props. I'm using the standard childrenWithProps method that works great if your child is another react component but no clear way of doing this without a direct react component as the child.
<DataCmp>
<Fragment>
<h1>Example Code</h1>
<div>{isLoggedIn}</div>
</Fragment>
</DataCmp>
In this example, I have adding the prop myData to the props of its children. However, this doesn't work. The child doesn't see the value. It will say myData is not set when it's passed in by props.
So I attempted this:
<DataCmp>
<Fragment>
<h1>Example Code</h1>
<div>{this.props.isLoggedIn}</div>
</Fragment>
</DataCmp>
This brings up errors as it has no idea what this.props.myData is.
My next attempt was to wrap the child in an inline function and get the prop from that.
<DataCmp>
{({ isLoggedIn}) => (
<Fragment>
<h1>Example Code</h1>
<div>{isLoggedIn}</div>
</Fragment>
)}
</DataCmp>
While this doesn't bring up any errors; The child component is never rendered.
I'm working on updating and modernizing someone else old Github project. here is the link to my project and the wrapping component is Wallet.jsx the location that it's being used is index.jsx
The children are rendered as such:
renderChildren = () => {
const { children } = this.props;
const { accounts } = this.state;
const handleLogin = () => this.login();
const childrenWithProps = React.Children.map(children, (child, index) => {
if(typeof child == 'object') {
return React.cloneElement(child, {
key: index,
loginFn: () => handleLogin(),
isLoggedIn: accounts[0] !== '0x0',
accounts,
});
} else {
return child;
}
});
return childrenWithProps;
}
I guess the error may not be in the destructuring, but in how you are using childrenWithProps.
It would be useful if you shared a condesandbox representing the problem with dummy data, so we can take a look there at that part too.
React.Children.map(children, fn) only iterates over valid react elements
This excludes, for example, functions passed as child. Passing a function-to-be-rendered as prop to a component is known as Render Props pattern. React.Children.map will not iterate over this, hence, your third option returned null.
Fix it by checking whether children is a valid ReactElement first and render it accordingly:
// wallet.tsx
...
const additionalProps = { ... };
if (React.isValidElement(children)) {
return React.Children.map(children,
(child, i) => React.cloneElement(child, { key: i, ...additionalProps });
} else {
// handle null, strings, undefined, booleans etc
return typeof children === 'function' ? children(additionalProps) : children;
}
...
// index.tsx
<Wallet ...>
{/* either a function */}
{(additionalProps) => console.log(additionalProps)}
{/* or components */}
<Layout>
...
</Layout>
</Wallet>
Note that React.isValidElement() also returns true for HTML-Elements. Which will receive the props but you obviously cannot add custom-logic. But let's say you pass a style props, it will be applied.

What is the difference between forwardingRef vs callback refs in React?

As per the React.js official documentation, the below code is an example of callback refs.
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
componentDidMount(props) {
//Here, this.inputElement in Parent will be set to the DOM node corresponding to the element in the CustomTextInput
console.log(this.inputElement);
}
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el}
/>
);
}
}
Here, this.inputElement in Parent will be set to the DOM node corresponding to the element in the CustomTextInput.
In case of forwarding ref, as per the official document,
const FancyButton = React.forwardRef((props, ref) => {
return (
<button ref={ref} className="FancyButton" data-name="My button">
{props.children}
</button>
);
});
//Parent Component
class FancyButtonWrapper extends React.Component {
constructor(props) {
super(props);
this.buttonRef = React.createRef();
}
componentDidMount(props) {
//Here this.ref will point to button element. Because of this reason, ref.current will give the value of button.
console.log(this.buttonRef.current.getAttribute("data-name"));
}
render() {
return (
//Here we are passing the ref to the child component.
<FancyButton ref={this.buttonRef} data-attr="Hello">
Click me!{" "}
</FancyButton>
);
}
}
Here, in this case, this.ref will point to the button element. Because of this reason, ref.current will give the value of the button.
Is there any difference between forwardRef and callbackRefs? We can access the child node's reference from parent in both of these cases.
I am not an expert but here are something to think about:
- callback refs are used when we need to dynamically set refs.
- Forward refs are commonly used when access to child refs are needed.
Well for the difference between the use of forwardingRef vs callback ref is in the HOC.
if you pass ref prop to HOC then inside HOC you cannot further pass it down to the enclosing component(which HOC wraps) since the props attributes does not store the ref inside it. ref is not a key ,see here: https://reactjs.org/docs/forwarding-refs.html 
so apart form this use case they work in same way.
Hope that helps !!
The difference is in the case of the ref callback, you can run side-effects when the ref changes. If you use useRef, you can access ref at any time but you will not know when it is set, or run a useEffect with the ref as a dependency
Callback refs have more control - they allow you to for example set a state when the ref is set, which will rerender the component when the component mounts, and you can use the ref node to do whatever you need.
In short - generally use useRef as it is the simplest. But ref callbacks can give you more control when needed

ReactJS: Parent child partial update

I'm having a bit of a trouble getting my head around how i would communicate between my parent and child components in my specific use case.
I have a child component that renders some points using an external lib.
What I'm currently doing is implementing componentDidMount/Update and use the external lib to render the points in this.props.points (which is an array provided by the parent component).
Rendering the points involves looping through them and call something like ExternalLib.addPoint(point).
What i would like to do, instead of looping through all the points everytime this.props.points changes, is making the parent component add (or remove) individual points to the child component.
Is it React friendly to call something like this.refs.myChild.addPoint(point) in the parent component?
Are there other techniques to achieve something like this?
Update
Here's some code (https://jsfiddle.net/69z2wepo/61366/):
const ExternalLib = {
addPoint: function(el, point) {
var li = document.createElement("li");
li.innerHTML = point;
el.appendChild(li);
}
}
class Child extends React.Component {
componentDidMount() {
this.renderPoints();
}
componentDidUpdate() {
this.renderPoints();
}
renderPoints() {
const el = this.refs.points;
el.innerHTML = '';
this.props.points.forEach(function(point) {
ExternalLib.addPoint(el, point);
});
}
render() {
console.log('render')
return (
<div>
<ul ref="points"></ul>
</div>
);
}
}
class Parent extends React.Component {
constructor() {
super();
this.state = {
points: []
};
}
addPoint() {
const points = this.state.points.slice();
points.push((new Date()).getTime());
this.setState({
points: points
});
}
render() {
return (
<div>
<button onClick={this.addPoint.bind(this)}>Add Point</button>
<Child points={this.state.points} />
</div>
);
}
}
ReactDOM.render(
<Parent />,
document.getElementById('container')
);
This is simplified because in this example I could generate the markup directly making use of map and leveraging React's partial DOM update - the external lib does some extra stuff that's not in this question's scope.
Thanks!
If you want to interfere with React rendering process, you can code shouldcomponentupdate.
In your children, if you make shouldcomponentupdate return false and call ExternalLib.addPoint(point), it should do the job:
shouldComponentUpdate(nextProps){
//comparePoints() is a function able to find if a new point is present in the list, comparing existing used points with new one
var newPoint = comparePoints(this.props.points, nextProps.points)
if(newPoint){
ExternalLib.addPoint(newPoint);
//forbide re-render
return false;
}
//enable other update
return true;
}

In react can I render a component's children without a wrapper?

I want to do:
render: () ->
{#props.children}
It is making me do:
render: () ->
<div>{#props.children}</div>
The reason I want to do the former is because the rendered children have their owner-based context set. But if I render them with the wrapper then the element which is the parent does not have its context set. This then generates a warning:
owner-based and parent-based contexts differ (values: undefined vs [object Object]) for key (x)
This is discussed here: https://gist.github.com/jimfb/0eb6e61f300a8c1b2ce7
But no solution is offered.
The warning occurs because the component which renders the child is the 'owner' and that is settting context but the div wrapper element is the 'parent' and has no context. My idea was to get rid of the div. But I can't get rid of it.
I've used the following code to only use a wrapper <span> on text children:
render() {
if (!this.props.children) {
return null;
} else if (React.isValidElement(this.props.children)) {
return this.props.children;
}
return <span>{this.props.children}</span>;
}
With React 16=>
render() {
return <React.Fragment>{this.props.children}</React.Fragment>;
}
or
render() {
return <>{this.props.children}</>;
}
In situations where a single child is required:
render() {
return React.Children.only(this.props.children)
}
This is what react-redux uses in connect(), FWIW.

React context doesn't transfer when using a component as prop

I'm using react-bootstrap's ModalTrigger to show a field-heavy modal (based on react-bootstrap's Modal), which means sending it a bunch of props:
<ModalTrigger modal={<MyModal field1={value1} field2={value2} (more fields...)/>}>
Click here to open
</ModalTrigger>
The parent component that creates the trigger has the fields/values passed in via props, and the parent component of that component has it passed as props as well, by the top-level component that actually holds the data. Both are basically pipes, which is a classic childContext scenario, except that it doesn't work. Here's a simplified version of what I've tried:
var MyModal = React.createClass({
contextTypes : {foo : React.PropTypes.string},
render : function() {
return (
<Modal {...this.props} title="MyTitle">
<div className="modal-body">
The context is {this.context.foo}
</div>
</Modal>
);
}
});
var Content = React.createClass({
childContextTypes : {foo: React.PropTypes.string},
getChildContext : function() {return {foo : "bar"}},
render : function() {
return (
<ModalTrigger modal={<MyModal/>}>
<span>Show modal</span>
</ModalTrigger>
)
}
});
The modal pops up with "The context is", without showing the actual context.
I believe this is happening because the prop sent to ModalTrigger is already rendered/mounted somehow, but I'm not sure why. To the best of my understanding, the owner of MyModal is the Content component, which means that the context should be ok, but this is not the case.
Some more information: I already tried passing {...this.props} and context={this.context} to MyModal with no success. Also, maybe relevant, ModalTrigger uses cloneElement to make sure the modal's onRequestHide prop points to the trigger's hide function.
So what am I missing here? :/
React.cloneElement will change the owner of the element when the ref prop is overriden, which means context will not be passed from the previous owner. However, this does not seem to be the case with ModalTrigger.
Note that the owner-based approach won't work altogether in React 0.14, since context will be passed from parent to child and not from owner to ownee anymore. ModalTrigger renders its modal node prop in another branch of the DOM (See OverlayMixin). Thus, your Modal component is not a child nor a descendant of your Content component and won't be passed child context from Content.
As for solving your problem, you can always create a component whose sole purpose is to pass context to its children.
var PassContext = React.createClass({
childContextTypes: {
foo: React.PropTypes.string
},
getChildContext: function() {
return this.props.context;
},
render: function() {
return <MyModal />;
},
});
To use it:
<ModalTrigger modal={<PassContext context={this.getChildContext()}/>}>
As Matt Smith hinted, it turns out that react-bootstrap already includes a very similar approach to forwarding context via ModalTrigger.withContext. This allows you to create a ModalTrigger component class which will forward its context to its modal node prop, no matter its position in the VDOM tree.
// MyModalTrigger.js
module.exports = ModalTrigger.withContext({
foo: React.PropTypes.String
});
There is a much better way of passing context on to your "portal" type components which render their children into a different container outside the React tree.
Using "renderSubtreeIntoContainer" rather than "render" will pass the context into the subtree as well.
It can be used like so:
import React, {PropTypes} from 'react';
import {
unstable_renderSubtreeIntoContainer as renderSubtreeIntoContainer,
unmountComponentAtNode
} from 'react-dom';
export default class extends React.Component {
static displayName = 'ReactPortal';
static propTypes = {
isRendered: PropTypes.bool,
children: PropTypes.node,
portalContainer: PropTypes.node
};
static defaultProps = {
isRendered: true
};
state = {
mountNode: null
};
componentDidMount() {
if (this.props.isRendered) {
this._renderPortal();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isRendered && !this.props.isRendered ||
(prevProps.portalContainer !== this.props.portalContainer &&
prevProps.isRendered)) {
this._unrenderPortal();
}
if (this.props.isRendered) {
this._renderPortal();
}
}
componentWillUnmount() {
this._unrenderPortal();
}
_getMountNode = () => {
if (!this.state.mountNode) {
const portalContainer = this.props.portalContainer || document.body;
const mountNode = document.createElement('div');
portalContainer.appendChild(mountNode);
this.setState({
mountNode
});
return mountNode;
}
return this.state.mountNode;
};
_renderPortal = () => {
const mountNode = this._getMountNode();
renderSubtreeIntoContainer(
this,
(
<div>
{this.props.children}
</div>
),
mountNode,
);
};
_unrenderPortal = () => {
if (this.state.mountNode) {
unmountComponentAtNode(this.state.mountNode);
this.state.mountNode.parentElement.removeChild(this.state.mountNode);
this.setState({
mountNode: null
});
}
};
render() {
return null;
}
};
This is an example of a portal I use in my production app Casalova that render context properly into their children.
Note: this API is undocumented and is likely to change in the future. For now, though, it's the right way to render context into portal components.

Resources