How can I pass a prop by child index in React - javascript

I'm trying to develop a generic container for React, that would work like this:
<PanelContainer>
<PanelConsole />
<PanelMemory />
<PanelLog />
</PanelContainer>
I want to dynamically create a tab system within the container, this works as follows:
renderTabs = () => {
return (
<ul className="panel_tabs">
{React.Children.map(this.props.children, (child, i) =>
<li key={child.type.display_name} onClick={() => this.handleClickTab(i)}>
{child.type.display_name}
</li>
)}
</ul>
);
}
This allows me to render the tabs with the display_name property within the class. This so far works, but now I'm trying to get the click to work. I want it to work dynamically so I don't have to build specialized containers for each instance of the panel. I'd ideally like to set the property of a child in this.props.children by index, so for example:
this.props.children[0].props.shown = false;
Is this possible?

I think React.Children.map and React.cloneElement works for you:
render() {
const { children } = this.props;
const tabs = this._renderTabs();
const childrenWithProps = React.Children.map(children, (child, id) =>
React.cloneElement(child, { shown: this.state.shows[i] }));
return (
<div>
<div>{tabs}</div>
<div>{childrenWithProps}</div>
</div>
)
}

Related

React setting the state of all rendered items with .map in parent component

I have a parent component with a handler function:
const folderRef = useRef();
const handleCollapseAllFolders = () => {
folderRef.current.handleCloseAllFolders();
};
In the parent, I'm rendering multiple items (folders):
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
name={folder.name}
content={folder.content}
id={folder.id}
ref={folderRef}
/>
))}
In the child component I'm using the useImperativeHandle hook to be able to access the child function in the parent:
const [isFolderOpen, setIsFolderOpen] = useState(false);
// Collapse all
useImperativeHandle(ref, () => ({
handleCloseAllFolders: () => setIsFolderOpen(false),
}));
The problem is, when clicking the button in the parent, it only collapses the last opened folder and not all of them.
Clicking this:
<IconButton
onClick={handleCollapseAllFolders}
>
<UnfoldLessIcon />
</IconButton>
Only collapses the last opened folder.
When clicking the button, I want to set the state of ALL opened folders to false not just the last opened one.
Any way to solve this problem?
You could create a "multi-ref" - ref object that stores an array of every rendered Folder component. Then, just iterate over every element and call the closing function.
export default function App() {
const ref = useRef([]);
const content = data.map(({ id }, idx) => (
<Folder key={id} ref={(el) => (ref.current[idx] = el)} />
));
return (
<div className="App">
<button
onClick={() => {
ref.current.forEach((el) => el.handleClose());
}}
>
Close all
</button>
{content}
</div>
);
}
Codesandbox: https://codesandbox.io/s/magical-cray-9ylred?file=/src/App.js
For each map you generate new object, they do not seem to share state. Try using context
You are only updating the state in one child component. You need to lift up the state.
Additionally, using the useImperativeHandle hook is a bit unnecessary here. Instead, you can simply pass a handler function to the child component.
In the parent:
const [isAllOpen, setAllOpen] = useState(false);
return (
// ...
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
isOpen={isAllOpen}
toggleAll={setAllOpen(!isAllOpen)}
// ...
/>
))}
)
In the child component:
const Child = ({ isOpen, toggleAll }) => {
const [isFolderOpen, setIsFolderOpen] = useState(false);
useEffect(() => {
setIsFolderOpen(isOpen);
}, [isOpen]);
return (
// ...
<IconButton
onClick={toggleAll}
>
<UnfoldLessIcon />
</IconButton>
)
}

How to add class to a passed Component

When trying to pass a component as a prop of another component, everything works fine.
But if i want instead pass a Component and handle its css classes inside the children, I'm currently lost.
In my mind im trying to achieve something similar to this:
import Navbar from 'what/ever/path/Navbar/is/in/Navbar.js';
export default function ParentComponent {
return(
<Navbar NavIcon={<MyIcon/>} />
)
}
.... Imports etc...
export default function Navbar(props) {
const {NavIcon} = props;
return(
<Navigation>
// Now use the Prop as a Component and pass default classNames to it.
// So that we don't need to wrap everything inside a span / div etc.
<NavIcon className="AddCustomStylesAlwaysHere" />
</Navigation>
)
}
Two approaches come to my mind:
Passing a component
Just pass the component and let the parent take care of its instantiation. This way, the only changes you need is making sure <MyIcon /> accepts a className prop:
const MyIcon = ({ className }) => {
return <div className={className} />
};
const Navbar = ({ NavIcon }) => {
return (
<Navigation>
<NavIcon className="AddCustomStylesAlwaysHere" />
</Navigation>
);
};
<Navbar NavIcon={MyIcon} />
Passing an element instance
This way, you take care of instantiating the component and the parent just renders it. In this case, you have to use React utilities to modify existing elements (https://reactjs.org/docs/react-api.html#cloneelement):
const MyIcon = ({ className }) => {
return <div className={className} />
};
const Navbar = ({ NavIcon }) => {
return (
<Navigation>
{React.cloneElement(NavIcon, { className: 'AddCustomStylesAlwaysHere' })}
</Navigation>
);
};
<Navbar NavIcon={<MyIcon />} />
You can use React.Children.map in combination with React.cloneElement:
{
React.Children.map(children, ( child, idx ) => {
return React.cloneElement(child, { className: 'additional-classnames' })
})
}

export Hooks in React for Nested Components?

I'm exporting hooks with nested components so that the parent can toggle state of a child. How can I make this toggle work with hooks instead of classic classes or old school functions?
Child Component
export let visible;
export let setVisible = () => {};
export const ToggleSwitch = () => {
const [visible, setVisibile] = useState(false);
return visible && (
<MyComponent />
)
}
Parent
import * as ToggleSwitch from "ToggleSwitch";
export const Parent: React.FC<props> = (props) => {
return (
<div>
<button onClick={() => ToggleSwitch.setVisible(true)} />
</div>
)
}
Error: Linter says [setVisible] is unused variable in the child... (but required in the parent)
You can move visible state to parent like this:
const Child = ({ visible }) => {
return visible && <h2>Child</h2>;
};
const Parent = () => {
const [visible, setVisible] = React.useState(false);
return (
<div>
<h1>Parent</h1>
<Child visible={visible} />
<button onClick={() => setVisible(visible => !visible)}>
Toggle
</button>
</div>
);
};
If you have many child-components you should make more complex logic in setVisible. Put object to useState where properties of that object will be all names(Ids) of child-components
as you know React is one-way data binding so if you wanna pass any props or state you have only one way to do that by passing it from parent to child component and if the logic becomes bigger you have to make it as a global state by using state management library or context API with react hooks use reducer and use effect.

React: Mapping children of a parent component

So I want to add certain styles to any child that's appended to a component. Let's say the parent component is called Section and children are called Cardin this case. in Section.js I am trying this: -
renderChildren = () =>{
return React.Children.map(this.props.children, (child, i)=>{
let el = React.cloneElement(child,{
style: {opacity:0.5}
})
return el
})
}
render(){
<ScrollView>
{this.renderChildren()}
</ScrollView>
}
The above approach doesn't work for me. And I would like to know why. Also is there a way where I could map across the children and wrap them in a new component? Something like this;
this.props.children.map(Child => <Wrapper> <Child/> </Wrapper> )
To wrap your children into a wrapper just put the call to React.Children.map into the wrapper component:
const OpaqueWrapper = ({ children }) => (
// check that children is defined
// if you do not want your wrapper to be rendered empty
children && (
<Wrapper>
{React.Children.map(children, child => (
React.cloneElement(child, {style: {...child.props.style, opacity: 0.5}})
))}
</Wrapper>
)
);
Also note that you have to merge the styles provided to the original child with the styles injected or you will lose the ability to style the children at all.
See this codesandbox for a working example.
As to why it did not work in your code: Are you sure that your <Card> component does handle the style prop correctly, i.e. applying it to it's children?
EDIT:
The sloution wraps all children components in a single wrapper, but I
would like to wrap each child with the applied wrapper , as shown in
my question.
The just move the wrapper into React.Children.map:
const OpaqueWrapper = ({ children }) => (
React.Children.map(children, child => (
<Wrapper>
{React.cloneElement(child, {style: {...child.props.style, opacity: 0.5}})}
</Wrapper>
)))
);
I think this solution is the simplest for wrap every child. When the children are rendered, you receive an instance of the component, not the component function. And you just need to wrap the instance into the wrapper component as shown below.
this.props.children.map(child => <Wrapper>{child}</Wrapper> )
For TypeScript:
React.Children.map(props.children, child => {
return <Wrapper>{child}</Wrapper>
})
And here the Typescript version when you write properties:
const mapped = Children.map(children, (child, index) => {
if(React.isValidElement(child)) {
return React.cloneElement(child, {
...child.props,
isFirst: index === 0,
isLast: !Array.isArray(children) || index === children.length - 1,
})
}
return null
})
Another variant for TypeScript which I think is clean:
const ChildrenWithProps = Children.map(children, child =>
cloneElement(child as JSX.Element, props),
)
used like:
return (
<div>
{ChildrenWithProps}
</div>
);
Of course, you need to know beforehand that what is passed as children definitely will be a valid child element, or you need to check it with isValidElement as previous answers suggested.

react - onclick using event without using arrow function

I have a link to return home and within that there is a button to remove an item from an array. To prevent the link to redirect to the home screen and to just remove the item from the array I am need to use ev.preventDefault().
Is it possible to pass an ev to a react method without using an arrow function in the render method? From my research and specifically the answer here it appears that the following is the only way to do so.
I am concerned that the arrow function causes a re-render every time, since new function is created on each render.
removeItem(label, e) {
e.preventDefault();
this.props.removeItem(label.id);
}
render () {
const { label } = this.props;
return (
<Link to'/'>
<span>Go Home</span>
<span onClick={(e) => this.removeItem(label, e) }> Click me <span>
</Link>
);
}
You could just remove the label parameter and get label from this.props directly. Refactor your app like this:
removeItem(ev) {
ev.preventDefault();
this.props.removeItem(this.props.label.id);
}
render () {
const { label } = this.props;
return (
<Link to'/'>
<span>Go Home</span>
<span onClick={this.removeItem}> Click me <span>
</Link>
);
}
This way, React will automatically pass the click event to your removeItem method.
Although, it should be said, that creating a new function on every render, probably isn't all that expensive.
In the light of avoiding to re-create the function you could extract the parameters into the class props. Here are some examples
For example this will create a new function all the time.
var List = React.createClass({
render() {
return (
<ul>
{this.props.items.map(item =>
<li key={item.id} onClick={this.props.onItemClick.bind(null, item.id)}>
...
</li>
)}
</ul>
);
}
});
Now it will not re-create the functions on re-rendering.
var List = React.createClass({
render() {
return (
<ul>
{this.props.items.map(item =>
<ListItem key={item.id} item={item} onItemClick={this.props.onItemClick} />
)}
</ul>
);
}
});
var ListItem = React.createClass({
render() {
return (
<li onClick={this._onClick}>
...
</li>
);
},
_onClick() {
this.props.onItemClick(this.props.item.id);
}
});
Here is some SO explanation React js onClick can't pass value to method

Categories