Is it possible to deep traverse React Children without rendering? - javascript

Is there any way to grab all of the bar properties in <Wrapper/> below 'statically', e.g. without rendering?
import React from 'react';
import ReactDOM from 'react-dom';
class Foo extends React.Component {
render() {
return(
<div>
<span bar="1" /> // want to collect this 'bar'
<span bar="2" /> // want to collect this 'bar'
</div>;
);
}
}
class FooTuple extends React.Component {
render() {
return(
<div>
<Foo />
<Foo />
</div>;
);
}
}
class Wrapper extends React.Component {
render() {
React.Children.forEach(this.props.children, child => {
console.log(child.props); // can only see <FooTuple/> not <Foo/>
});
return(
<div>
{this.props.children}
</div>;
);
}
}
ReactDOM.render(
<Wrapper>
<FooTuple />
</Wrapper>,
document.getElementById('app'));
Here's a webpackbin with a naive attempt that tries to iterate over child.children which obviously doesn't work, but it's here if it's helpful:
http://www.webpackbin.com/EySeQ-ihg

TL;DR; Nope that's not possible.
--
I've once encountered the same problem trying to traverse a tree of deeply nested children. Here are my scoop outs:
Required knowledge
children are what's placed inside the jsx open and close tags, or injected directly in the children prop. other than that children prop would be undefined.
<div className="wrapper">
// Children
<img src="url" />
</div>
/* OR */
<div classname="wrapper" children={<img src="url" />}>
children are an opaque tree-like data structure that represents the react elements' tree, it's likely the output of React.createElement that the jsx implements when transpiling.
{
$$typeof: Symbol(react.element),
type: 'div',
key: null,
ref: null,
props: {
className: "wrapper",
children: {
$$typeof: Symbol(react.element),
type: 'img',
key: null,
ref: null,
props: { src: 'url' },
}
}
}
Creating React elements doesn't mean that they are instantiated, think of them like a descriptor that React uses to render those elements. in other words, instances are taken care off by React itself behind the scenes.
Traversing children
Let's take your example and try to traverse the whole tree.
<Wrapper>
<FooTuple />
</Wrapper>
The opaque children object of these elements would be something like this:
{
$$typeof: Symbol(react.element),
type: Wrapper,
key: null,
ref: null,
props: {
children: {
$$typeof: Symbol(react.element),
type: FooTuple,
key: null,
ref: null,
props: {},
}
}
}
As you can see FooTuple props are empty for the reason you should know by now. The only way to reach it's child elements is to instantiate the element using it's type to be able to call it's render method to grab it's underlying child elements, something like this:
class Wrapper extends React.Component {
render() {
React.Children.forEach(this.props.children, child => {
const nestedChildren = new child.type(child.props).render();
console.log(nestedChildren); // `FooTuple` children
});
return(
<div>
{this.props.children}
</div>;
);
}
}
This is obviously not something to consider at all.
Conclusion
There is no clean way to augment deeply nested children or grab something from them (like your case). Refactor your code to do that in a different manner. Maybe provide a setter function in the context to set the data you need from any deep child.

Related

Passing a Nested Object as React.Component Props

I'm sure this was asked before but I can't find it. I'm trying to pass a nested object as props into a React.Component class but I keep getting this error:
react-dom.development.js:13231 Uncaught Error: Objects are not valid as a React child (found: object with keys {props, context, refs, updater}). If you meant to render a collection of children, use an array instead.
I have an App.tsx file created like this:
import React from "react";
export type Child = {
id: number
}
export type Parent = {
id: number,
child: Child
}
export class Card extends React.Component<Parent> {
render() {
return <div>
Parent ID: {this.props.id}
Child ID: {this.props.child.id}
</div>;
}
}
function App() {
const data: any = {
"id": 1,
"child": {
"id": 2
}
}
const card: Card = new Card(data);
return (
<div className="app">
{card}
</div>
);
}
export default App;
Is this sort of thing not possible? It seems like it should be but maybe I am missing something. Is there a way to make something like this work or a correct pattern that I am not using? Thanks!
Theere is no "one" correct way to pass an object down to a child component, there are multiple approaches, one of which is the following (using your example):
import React from "react";
export type Child = {
id: number
}
export type Parent = {
id: number,
child: Child
}
export class Card extends React.Component<Parent> {
render() {
return <div>
Parent ID: {this.props.id}
Child ID: {this.props.child.id}
</div>;
}
}
function App() {
const data: any = {
"id": 1,
"child": {
"id": 2
}
}
return (
<div className="app">
<Card {...data} />
</div>
);
}
export default App;
I'm pretty sure for this to work you'd have to change
<div className="app">
{card}
</div>
TO:
<div className="app">
<card />
</div>
BUT, like Jack said in the comments, you shouldn't be instantiating React components like this anyway. Instead of doing:
const card: Card = new Card(data);
You should just do:
<div className="app">
<Card {...data} />
</div>

.map undefined on array of object

I am passing array of object as a prop from App.js -> searchResult -> TrackList.js. But when I apply .map function on the array of object it shows Cannot read property 'map' of undefined . I have tried different ways to solve this but none of them worked. In console I am getting the value of prop. And my TRackList.js component is rendering four times on a single run. Here is the code
App.js
this.state = {
searchResults: [
{
id: 1,
name: "ritik",
artist: "melilow"
},
{
id: 2,
name: "par",
artist: "ron"
},
{
id: 3,
name: "make",
artist: "zay z"
}
]
return ( <SearchResults searchResults={this.state.searchResults} /> )
In Searchresult .js
<TrackList tracked={this.props.searchResults} />
In TrackList.js
import React from "react";
import Track from "./Track";
export default class TrackList extends React.Component {
constructor(props) {
super();
}
render() {
console.log("here", this.props.tracked);
return (
<div>
<div className="TrackList">
{this.props.tracked.map(track => {
return (<Track track={track} key={track.id} />);
})}
</div>
</div>
);
}
}
Here is the full code -- https://codesandbox.io/s/jamming-ygs5n?file=/src/components/TrackList.js:0-431
You were loading the Component TrackList twice. One time with no property passed, that's why it was first set in console then looks like it's unset, but it's just a second log. I have updated your code. Take a look here https://codesandbox.io/s/jamming-ddc6l?file=/src/components/PlayList.js
You need to check this.props.tracked.map is exists before the loop.
Solution Sandbox link:https://codesandbox.io/s/jamming-spf7f?file=/src/components/TrackList.js
import React from "react";
import Track from "./Track";
import PropTypes from 'prop-types';
export default class TrackList extends React.Component {
constructor(props) {
super();
}
render() {
console.log("here", typeof this.props.tracked);
return (
<div>
<div className="TrackList">
{this.props.tracked && this.props.tracked.map(track => {
return <Track track={track} key={track.id} />;
})}
</div>
</div>
);
}
}
TrackList.propTypes = {
tracked: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
artist: PropTypes.string,
}))
};
You need to check this.props.tracked value before implementing the map function.
you can simply check using this.props.tracked && follow your code.
You should add searchResults={this.state.searchResults} in your app.js to Playlist, take it in Playlist with props, and then set it in TrackList from Playlist (tracked={props.searchResults}).
Also, Typescript helps me not to do such mistakes.
Also, add a key prop to your component that you return in the map function.

React: parent component props in child without passing explicitly

Is it possible to have the props of the parent component to be available in child component without passing them down?
I am trying to implement a provider pattern, so that to access all the provider props in its child components.
EX:
Suppose the below provider comp FetchProvider will fetch the data and theme props on its own, and when any child component is enclosed by it, I want to access both props "data" and "theme" in the child component as well. How can we achieve it?
class FetchProvider
{
proptypes= {
data: PropTypes.shape({}),
theme: PropTypes.shape({})
}
render()
{
// do some
}
mapStateToProps()
{
return {data, theme};
}
}
class ChildComponent
{
proptypes= {
name: PropTypes.shape({})
}
render()
{
const{data, them} = this.props; // is this possible here?
// do some
}
}
and if I try to above components as below.
<FetchProvider>
<ChildComponent name="some value"/> //how can we access parent component props here? without passing them down
<FetchProvider/>
This is exactly what react context is all about.
A Consumer can access data the a Provider exposes no matter how deeply nested it is.
// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// Use a Provider to pass the current theme to the tree below.
// Any component can read it, no matter how deep it is.
// In this example, we're passing "dark" as the current value.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton(props) {
// Use a Consumer to read the current theme context.
// React will find the closest theme Provider above and use its value.
// In this example, the current theme is "dark".
return (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
}
Here is a small running example:
Note This is the react v16 context API.
Your use case can be solved with the usage of React context. With the help of Context, any child that is wrapped by a provided can be a consumer for the data that is provided by the Provider
In your case, you can use it like
context.js
export const FetchContext = React.createContext();
Provider.js
import { FetchContext } from 'path/to/context.js';
class FetchProvider extends React.Component
{
proptypes= {
data: PropTypes.shape({}),
theme: PropTypes.shape({})
}
render()
{
const { data, theme, children } = this.props;
return (
<FetchContext.Provider value={{ data, theme}}>
{children}
</FetchContext.Provider>
)
}
mapStateToProps()
{
return {data, theme};
}
}
ChildComponent.js
class ChildComponent extends React.Component
{
proptypes= {
name: PropTypes.shape({})
}
render()
{
const{data, them} = this.props; // use it from props here
// do some
}
}
export default (props) => (
<FetchContext.Consumer>
{({ data, theme }) => <ChildComponent {...props} data={data} theme={theme} />}
</FetchContext.Consumer>
)
However given the fact that you are already using Redux, which is build on the concept of Context, you might as well use redux and access the values within the child component since they are the same values that are supplied from the Redux store to the child by parent.
class ChildComponent extends React.Component
{
proptypes= {
name: PropTypes.shape({})
}
render()
{
const{data, them} = this.props; // use it from props here
// do some
}
}
const mapStateToProps = (state) => {
return {
data: state.data,
theme: state.theme
}
}
You can use React.Children to iterate over the children and pass whatever props you want to send to the new cloned elements using React.cloneElement.
EX:
class Parent extends React.Component {
constructor(props) {
super(props);
}
render() {
const { children } = this.props;
const newChildren = React.Children.map(children, child =>
React.cloneElement(child, { myProp: 'test' }));
return(
<View>
{newChildren}
</View>
)
}
}
Are you looking for:
class MyParent extends Component {
render() {
return <MyChild {...this.props}>
// child components
</MyChild>
}
}
This would pass all of the props passed into MyParent to the MyChild being rendered.

Rerendering React props.children

I have this kind of a setup:
// inside Parent.js
class Parent extends React.Component {
render() {
return { this.props.children }
}
}
// inside Child.js
class Child extends React.Component {
render() {
let message;
const greet = this.context.store.parentState.greet;
if(greet) message = 'Hello, world!';
return (
<div>
{ message }
</div>
)
}
}
Child.contextTypes = {
store: PropTypes.object.isRequired
}
// inside App.js
<Parent>
<Route path='/a-path' component={ Child } />
</Parent>
When Parent receives new state through setState, its render method is called but the render method in Child is not called!
The reason I want to achieve that is because some logic in Child is dependent on the state of Parent.
If I pass the state of Parent via context like how the store is passed and then access it in Child via this.context.parentState, this seems to be working and causing a call on Child's render method, I guess it's because we're receiving new context.
Why is this? context is great but is there a good way around this particular issue without needing context?
If you are rendering components as children, which aren't components to Route, you can make use of React.cloneElement with React.Children.map like
// inside Parent.js
class Parent extends React.Component {
render() {
return React.Children.map(this.props.children, (child) =>
React.cloneElement(child, {parentState: this.state.parentState})
);
}
}
However if elements rendered as Children to Parent are Routes then either you need to make use of context or write a wrapper around Route so that any extra props that the Route receives are passed on to the component
const RouteWrapper = ({exact, path, component: Component, anyOtherRouterProp, ...rest}) =>
<Route exact={exact} path={path} {...otherRouterProps} render={(props) => <Component {...props} {...rest} />} />
Where in the above case anyOtherRouterProp are the props that are applicable to the Route and needs to be destructured separately. After this you can make use of React.Children.map and React.cloneElement to pass on the props to children.
Although this is one such way, I would still recommend you to make use of context, specially with the introduction of new context API which makes it extremely easy to implement
You can do like this....
// inside Parent.js
class Parent extends React.Component {
render() {
return (
<Child children={this.props.children} />
)
}
}
// inside Child.js
class Child extends React.Component {
render() {
let message;
const greet = this.context.store.parentState.greet;
if(greet) message = 'Hello, world!';
return (
<div>
{ message }
{ this.props.children }
</div>
)
}
}
Child.contextTypes = {
store: PropTypes.object.isRequired
}
// inside App.js
<Parent>
<Route path='/a-path' component={ Child } />
</Parent>

Incorrect casing error with dynamically rendered component in React

So, i’d like to spare time later and want to do a dynamically generated page. For that reason i want to read component data from an object, like this:
layout: {
toolbar: {
components: [
{
type: "Testcomp",
theme: "default",
data: "div1"
},
{
type: "Testcomp",
theme: "pro",
data: "div2"
},
]}}
The component would be dynamically imported, enabled/activated and besides that this is the jsx code supposed to render components dynamically:
render() {
const toolbarComponents = userSession.layout.toolbar.components.map(Component => (
<Component.type theme={Component.theme} data={Component.data} key={this.getKey()} />
));
return (
<div>
<div className="toolbar">
toolbar
{toolbarComponents}
</div>
. . .
</div>
);
}
However i get the following warning in Chrome’s devtool, also the component is not displayed:
Warning: is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.
Warning: The tag is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
What’s wrong with my code?
You are getting those errors because you are not referencing the component itself here, instead using a string as name. So, maybe you need to think another way to create the components dynamically. Like starting with a base component and only give some props and data to it.
// Define above main component or elsewhere then import.
const MyComponent = ( props ) => <div>{props.data}</div>
// Main component
render() {
const toolbarComponents = userSession.layout.toolbar.components.map(
component => <MyComponent theme={component.theme} data={component.data} />
);
return <div className="App">{toolbarComponents}</div>;
}
Here we are not using a type key anymore. If you want to use different components like that, you can create every base component and then use as its name with type key but not with string, directly referencing the component.
I tried using the same method as you, wherein I was using a string to reference a React component by name. However, it doesn't appear designed to be used outside of standard tags like div.
Instead, what worked for me is to import the component I wanted to show and then set that in the state.
import MyComponent from 'mycomponent';
class Parent extends React.Component {
constructor() {
super();
this.state = {
selectedComponent: null
};
}
render() {
return (
<div className="Parent">
<h2>Parent Component</h2>
<button onClick={() => this.setState({ selectedComponent: MyComponent })}>Show my component</button>
{this.state.selectedComponent !== null && React.createElement(this.state.selectedComponent, null, null)}
</div>
);
}
}

Categories

Resources