I'm trying to chain a React Components to Object by passing my imported Components to them as Props.
const [showComponent, setShowComponent] = useState([
{ cId: 1, componentName: <ContactDialogAddresses />, title: 'Adresse', render: true },
{ cId: 2, componentName: <ContactDialogPhone />, title: 'Rufnummer', render: true },
]);
return(
{showComponent.map((component, index) => {
if (component.render) {
return (
<>
<span>{index}</span>
{component.componentName}
</>
);
}
})}
)
How can I reimplement props like this?
// I need to pass the props during the mapping because I need a unique identifier for each rendert component later on.
<ContactDialogAddresses
key={component.cId}
onShowComponent={handleShowComponent}
cIndex={index}
/>
One of the options would be to do component creators/renderers (basically functions) in your state, rather than directly components.
Something like this:
const [showComponent, setShowComponent] = useState([
{ cId: 1, componentRenderer: (props) => { return (<ContactDialogAddresses {...props} />)}, title: 'Adresse', render: true },
{ cId: 2, componentRenderer: (props) => { return (<ContactDialogPhone {...props} />) }, title: 'Rufnummer', render: true },
]);
return(
{showComponent.map((component, index) => {
if (component.render) {
return (
<>
<span>{index}</span>
{component.componentRenderer({someProp:'someValue'})}
</>
);
}
})}
)
Another option could be using variables - important they should be capitalized (and do not do < /> in componentName in your state):
const [showComponent, setShowComponent] = useState([
{ cId: 1, componentName: ContactDialogAddresses, title: 'Adresse', render: true },
{ cId: 2, componentName: ContactDialogPhone, title: 'Rufnummer', render: true },
]);
return(
{showComponent.map((component, index) => {
if (component.render) {
const TargetComponent = component.componentName
return (
<>
<span>{index}</span>
<TargetComponent x="y"/>
</>
);
}
})}
)
Otherwise you could use createElement - https://reactjs.org/docs/react-api.html#createelement
Actually there is good thread here - React / JSX Dynamic Component Name with some good discussions there, so kudos goes there
Related
I am coding a recursive component.
If I pass props like this: <RecursiveComponent itemProps={data} />, it won't work. I have to pass like this: <RecursiveComponent {...data} />. Why is that?
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
import * as React from 'react'
const data = {
name: 'Level 1',
children: [
{
name: 'Level 2',
children: [
{
name: 'Level 3'
}
]
},
{
name: 'Level 2.2',
children: [
{
name: 'Level 3.2'
}
]
}
]
}
const RecursiveComponent = (itemsProp) => {
const hasData = itemsProp.children && itemsProp.children.length;
return (
<>
{itemsProp.name}
{hasData && itemsProp.children.map((child) => {
console.log(child);
return <RecursiveComponent {...child} key={child.name} />
})}
</>
)
}
function App() {
return (
<div className="App">
<RecursiveComponent {...data} />
</div>
);
}
export default App;
React components take an object as first argument: the props. When you use jsx, the attributes you pass are converted to an object.
<Component name="test" id={42} ok={true} /> // props = { name: "test", id: 42, ok: true }
Then your component receive the props like this:
const Component = (props) => {
console.log(props.name) // => "test"
// the rest of your component...
}
// Most people will use destructuration to use directly the attribute name
const Component = ({ name, id, ok }) => {
console.log(name) // => "test"
// the rest of your component...
}
The spread operator allow you to fill attributes for every properties of an object
const data = { name: "test", id: 42, ok: true }
<Component {...data} />
// is the same as :
<Component name={data.name} id={data.id} ok={data.ok} />
In your case when you pass itemsProp={data} you actually have the first argument of your component like this
const RecursiveComponent = (itemsProp) => {
// here itemsProp = { itemsProp : { name : "Level 1", children : [...] }}
// so you have to use it this way
const hasData = itemsProp.itemsProp.children && itemsProp.itemsProp.children.length;
}
you can use it with passing props <RecursiveComponent itemProps={data} /> also, see below , notice the {} brace on props while getting props in function component.
Different way 1:-
const RecursiveComponent = (props) => {
const hasData = props.itemProps.children && props.itemProps.children.length
}
you can use like above as well.
Solution 2:-
import React from "react";
const data = {
name: "Level 1",
children: [
{
name: "Level 2",
children: [
{
name: "Level 3"
}
]
},
{
name: "Level 2.2",
children: [
{
name: "Level 3.2"
}
]
}
]
};
const RecursiveComponent = ({itemProps}) => {
console.log("itemsprops", itemProps);
const hasData = itemProps.children && itemProps.children.length;
return (
<>
{itemProps.name}
{hasData && itemProps.children.map((child) => {
console.log(child);
return <RecursiveComponent itemProps={child} key={child.name} />
})}
</>
);
};
function App() {
return (
<div className="App">
<RecursiveComponent itemProps={data} />
</div>
);
}
export default App;
I am creating a multistep form in React which uses a switch case to render a component based on its ID:
App.js
function App() {
const steps = [
{ id: 'location' },
{ id: 'questions' },
{ id: 'appointment' },
{ id: 'inputData' },
{ id: 'summary' },
];
return (
<div className="container">
<ApptHeader steps={steps} />
<Step steps={steps} />
</div>
);
}
Steps.js
const Step = ({ steps }) => {
const { step, navigation } = useStep({ initialStep: 0, steps });
const { id } = step;
const props = {
navigation,
};
console.log('StepSummary', steps);
switch (id) {
case 'location':
return <StepLocation {...props} steps={steps} />;
case 'questions':
return <StepQuestions {...props} />;
case 'appointment':
return <StepDate {...props} />;
case 'inputData':
return <StepUserInputData {...props} />;
case 'summary':
return <StepSummary {...props} />;
default:
return null;
}
};
In my <ApptHeader /> component in App.js, I want to change the Title and subtitle of the string in the header based on the component rendered in the switch case.
const SectionTitle = ({ step }) => {
console.log('step', step);
return (
<div className="sectionWrapper">
<div className="titleWrapper">
<div className="title">Title</div>
<div className="nextStep">
SubTitle
</div>
</div>
<ProgressBar styles={{ height: 50 }} />
</div>
);
};
export default SectionTitle;
How can I accomplish this? I feel like I might be writing redundant code if I have to make a switch case again for each title/subtitle. Thanks in advance.
You can use a pattern like this
const steps = {
location: { id: 'location', title: 'Location', component: StepLocation },
questions: { id: 'questions', title: 'Questions', component: StepQuestions },
appointment: { id: 'appointment', title: 'Appointment', component: StepDate },
inputData: { id : 'inputData', title: 'InputData', component: StepUserInputData },
summary: { id: 'summary', title: 'Summary', component: StepSummary },
};
Then while using it inside your Steps.js will become
const Step = ({ steps }) => {
const { step, navigation } = useStep({ initialStep: 0, steps });
const { id } = step;
const props = { navigation };
const Component = steps[id].component;
return <Component {...props} steps={steps} />;
};
Your SectionTitle.js will become like this
const SectionTitle = ({ step }) => {
console.log('step', step);
return (
<div className="sectionWrapper">
<div className="titleWrapper">
<div className="title">Title</div>
<div className="nextStep">{step.title}</div>
</div>
<ProgressBar styles={{ height: 50 }} />
</div>
);
};
export default SectionTitle;
This way you can avoid code redundancy.
Be Sure to update your other parts of code like useStep to accept and Object instead of and Array
I rendered a list of buttons using Array.map method in a function component. When I tried to pass state to each mapped array items, the rendered results changed all array items at once, instead of one by one.
Here is my code. Am I doing something wrong? Sorry if the question has been solved in other thread or I used the wrong method. This is my first React project and I am still learning. It would be very appreciated if someone could advise. Thank you!
import React, { useState } from "react"
export default function Comp() {
const [isActive, setActive] = useState(false)
const clickHandler = () => {
setActive(!isActive)
console.log(isActive)
}
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => {
return items.map(item => (
<li key={item.id}>
<button onClick={clickHandler}>
{item.name} {isActive ? "active" : "not active"}
</button>
</li>
))
}
return (
<ul>{renderList(data)}</ul>
)
}
Put the individual item into a different component so that each has its own active state:
export default function Comp() {
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => (
items.map(item => <Item key={item.id} name={item.name} />)
);
return (
<ul>{renderList(data)}</ul>
)
}
const Item = ({ name }) => {
const [isActive, setActive] = useState(false);
const clickHandler = () => {
setActive(!isActive);
};
return (
<li>
<button onClick={clickHandler}>
{name} {isActive ? "active" : "not active"}
</button>
</li>
);
};
You need to set the active-id in handling the click-event. That will in-turn render active/non-active conditionally:
Notice the flow (1) > (2) > (3)
function Comp() {
const [activeId, setActiveId] = React.useState(null);
const clickHandler = (item) => {
setActiveId(item.id) // (2) click-handler will set the active id
}
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => {
return items.map(item => (
<li key={item.id}>
<button onClick={() => clickHandler(item)}> // (1) passing the clicked-item so that we can set the active-id
{item.name} {item.id === activeId ?
"active" : "not active" // (3) conditionally render
}
</button>
</li>
))
}
return (
<ul>{renderList(data)}</ul>
)
}
Good Luck...
I have come across an issue in which there is a hash which contains a label, index, visible and I need to order the component according to the index value, it should be visible if visible is true.
These are my components:
render(){
return(
<GroupFeaturedCarousel {...this.props}/>
<GroupAssignmentCarousel {...this.props}/>
<GroupSharedCarousel {...this.props}/>
<GroupChannelCarousel {...this.props}/>
)
}
and array of hashes is this:
myArray = [
{default_label: "Featured", index: 0, visible: true},
{default_label: "Assigned", index: 1, visible: true},
{default_label: "Shared", index: 2, visible: true},
{default_label: "Channels", index: 3, visible: true}]
All I can think of solving this is by using _.sortBy(myArray, 'index') but then how can I implement this?
Need to consider sort and visible both.
What do you mean by 'hash'? Did you mean an array?
I would something like this if i understand you correctly:
class X extends React.Component{
myArray = [{
component: GroupFeaturedCarousel,
index: 0,
visible: true
}, {
component: GroupAssignmentCarousel,
index: 1,
visible: true
}
]
render() {
return this.myArray
.filter(x => x.visible == true)
.sort((a,b) => a.index - b.index)
.map(({component: Component, index}) => ({
<Component key={index} {...this.props} />
});
}
}
Extend your array item to look like this:
{
default_label: 'foo',
index: 0,
visible: true,
component: FooComponent
}
Then render like this:
render() {
return <React.Fragment>
{ myArray
.filter(item => item.visible)
.sort((a,b) => a.index - b.index))
.map(item => {
const Component = item.component
return <Component {...this.props} key={item.index} />
}) }
</React.Fragment>
}
This is how I get an array of Dropdown.Item elements returned in my react component.
render () {
const { data } = this.props
if (data) {
return data.map((item, index) => {
return <Dropdown.Item
text={item.title}
key={index}
/>
})
}
return null
}
Now I'm trying to test for the returned result, but my test fails with recieved.length: 0.
I think the issue is returning a map... How do I have to test for that?
it('should render dropdown items', () => {
wrapper = shallow(<Component data={data} />)
expect(wrapper.find(Dropdown.Item)).toHaveLength(2)
})
My data looks like:
[ { _id: '1', title: 'Item 1' }, { _id: '2', title: 'Item 2' } ]
Update
This is working for me:
expect(wrapper.at(0).find(Dropdown.Item)).toHaveLength(2)
But why do I have to use at(0)?
I think you could do something like this..
class Component extends React.Component {
...
render() {
const { data } = this.props
return (data.map((item, index) => <Dropdown text={item.title} key={index}/>));
}
}
class Dropdown extends React.Component {
...
render(){
const {text, key} = this.props;
return(
<div>
<li className='test'>
{text}
</li>
</div>
)
}
}
describe('<MyComponent />', () => {
it('renders two <Dropdown /> components', () => {
const wrapper = shallow(<Component data={[{ _id: '1', title: 'Item 1' }, { _id: '2', title: 'Item 2' }]} />);
expect(wrapper.find('Dropdown').to.have.length(2));
});
});
use to.have.length instead of toHaveLength , check enzyme docs
ref: http://airbnb.io/enzyme/docs/api/ReactWrapper/find.html
see also Shallow Rendering example from enzyme docs:
describe('<MyComponent />', () => {
it('renders three <Foo /> components', () => {
const wrapper = shallow(<MyComponent />);
expect(wrapper.find(Foo)).to.have.length(3);
});
ref: https://github.com/airbnb/enzyme/blob/HEAD/docs/api/shallow.md