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

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.

Related

how to overwrite children's props in react

i want to render a list of react components.The props of each component vary,but every element's props will be overwritten by parents props.i tried something like this
render(){
let children=this.props.children
let dis;
if(Array.isArray(children)){
dis=[]
children.forEach(c=>{let l=Object.assign(c,Object.assign(c.props,this.props))
dis.push(l)})
}
return(
{dis}
)
}
}
but got error that props in read only.
It sounds like what you're trying to do is have a parent component inject additional settings into the children it is going to render.
Take a look here:
https://stackoverflow.com/a/50786569/1541237
Basically, you would render like this:
render() {
return (
React.Children.map(this.props.children, child =>
React.cloneElement(child, { ...this.props }));
);
}

React Refs with TypeScript: Cannot read property 'current' of undefined

I'm building a React application using TypeScript.
I want to create button, that scrolls to a header of a child component on my main page.
I've created a ref in the child component, following this stack overflow answer and (tried to) use forward refs to access it on my parent component.
export class Parent extends Component {
private testTitleRef!: RefObject<HTMLHeadingElement>;
scrollToTestTitleRef = () => {
if (this.testTitleRef.current !== null) {
window.scrollTo({
behavior: "smooth",
top: this.testTitleRef.current.offsetTop
});
}
};
render() {
return <Child ref={this.testTitleRef} />
}
}
interface Props {
ref: RefObject<HTMLHeadingElement>;
}
export class Child extends Component<Props> {
render() {
return <h1 ref={this.props.ref}>Header<h1 />
}
}
Unfortunately when I trigger scrollToTestTitleRef I get the error:
Cannot read property 'current' of undefined
Meaning that the ref is undefined. Why is that? What am I doing wrong?
EDIT:
Estus helped me to create the ref. But when I trigger the scrollToTestTitleRef() event, it doesn't scroll.
When I console.log this.testTitleRef.current I get the output:
{"props":{},"context":{},"refs":{},"updater":{},"jss":{"id":1,"version":"9.8.7","plugins":{"hooks":{"onCreateRule":[null,null,null,null,null,null,null,null,null,null,null,null],"onProcessRule":[null,null,null],"onProcessStyle":[null,null,null,null,null,null],"onProcessSheet":[],"onChangeValue":[null,null,null],"onUpdate":[null]}},"options":{"plugins":[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}]}},"sheetsManager":{},"unsubscribeId":null,"stylesCreatorSaved":{"options":{"index":-99999999945},"themingEnabled":false},"sheetOptions":{},"theme":{},"_reactInternalInstance":{},"__reactInternalMemoizedUnmaskedChildContext":{"store":{},"storeSubscription":null},"state":null}
Note: I deleted the keys of cacheClasses, _reactInternalFiber and
__reactInternalMemoizedMaskedChildContext, because they contained cyclic dependencies.
So current doesn't seem to have a key of offsetTop. Does this maybe have something to do with the fact that in my real application the child component is wrapped inside material-ui's withStyle and React-Redux' connect?
! non-null assertion operator suppresses the actual problem. There is no way in JavaScript/TypeScript how testTitleRef property could be assigned from being used as <Child ref={this.titleRef} />, so it stays undefined (there's also inconsistency with testTitleRef and titleRef).
It should be something like:
private testTitleRef: React.createRef<HTMLHeadingElement>();
scrollToTestTitleRef = () => {
if (!this.testTitleRef.current) return;
window.scrollTo({
behavior: "smooth",
top: this.testTitleRef.current.getBoundingClientRect().top + window.scrollY
});
};
render() {
return <Child scrollRef={this.testTitleRef} />
}
and
export class Child extends Component<Props> {
render() {
return <h1 ref={this.props.scrollRef}>Header<h1 />
}
}

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

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
}
}
}

How to check child child component's type

I wrote a component which takes as children other components and passes to them additional props.
Inside the render method of my component I have code like this
React.Children.forEach(children, child => {
if (child.type === DataTableHeader)) {
header = React.cloneElement(child, {
filter, onFilterChange,
sort, onSortChange
})
}
})
where DataTableHeader is another component.
The problem is that child.type is never equal to DataTableHeader.
In chrome debugger I see that child.type reference DataTableHeader from here
(function(factory,instantiate
/**/) {
return function DataTableHeader() {
return instantiate(factory, this, arguments);
}
})
whereas DataTableHeader points to my component.
The render method comes from DataTable component, and I use it like this:
<DataTable
onFetchData={onFetchData}>
<DataTableHeader>
<DataTableHeaderCell sortField='name'>
Name
</DataTableHeaderCell>
</DataTableHeader>
{children}
</DataTable>
The problem is caused by react-hot-loader https://github.com/gaearon/react-hot-loader/issues/304
As a workaround I define static field on DataTableHeader and use it instead of comparing type type

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