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;
Related
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'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
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'm wiriting a kibana plugin and I have some problems with a flyout component. That's my starting code:
export const InputPipelineDebugger = ({queryParams, setQueryParams, setConnectionType, setMessage}) => {
const onChangeTest = (e) => {
setMessage(e.target.value);
}
const onTabConnectionTypeClicked = (tab) => {
setConnectionType(tab.id);
}
var tabsConnection = [
{
id: 'http',
name: 'HTTP',
content: <HttpInput onChangeTest = {onChangeTest} queryParams = {queryParams} setQueryParams={setQueryParams} />
},
{
id: 'syslog',
name: 'SYSLOG',
content: <SyslogInput onChangeTest = {onChangeTest} />
},
{
id: 'beats',
name: 'BEAT',
content: <BeatsInput onChangeTest = {onChangeTest} />
}
];
return (
<EuiFlexItem>
<h3>Input</h3>
<EuiTabbedContent
tabs={tabsConnection}
initialSelectedTab={tabsConnection[0]}
autoFocus="selected"
onTabClick={tab => {
onTabConnectionTypeClicked(tab);
}} />
</EuiFlexItem>
);
}
And what I want is to dynamically build the tabs array according to the response from a rest call. So I was trying to use the useEffect method and for that I change the tabsConnection with a state (and a default value, that works WITHOUT the useEffect method) but is not working at all. Console saids to me that the 'content' value from the tabs array is undefined, like if it's not recognizing the imports.
How can I achieve my goal? Thanks for the support
export const InputPipelineDebugger = ({queryParams, setQueryParams, setConnectionType, setMessage}) => {
//initialized with a default value
const [tabs, setTabs] = useState([{
id: 'syslog',
name: 'SYSLOG',
content: <SyslogInput onChangeTest = {onChangeTest} />
}]);
const onChangeTest = (e) => {
setMessage(e.target.value);
}
const onTabConnectionTypeClicked = (tab) => {
setConnectionType(tab.id);
}
useEffect(()=>{
//rest call here;
//some logics
var x = [{
id: 'beats',
name: 'BEATS',
content: <BeatsInput onChangeTest = {onChangeTest} />
}];
setTabs(x);
}, []);
return (
<EuiFlexItem>
<h3>Input</h3>
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[0]}
autoFocus="selected"
onTabClick={tab => {
onTabConnectionTypeClicked(tab);
}} />
</EuiFlexItem>
);
}
Errors from the console:
Uncaught TypeError: Cannot read property 'content' of undefined
at EuiTabbedContent.render
EDIT 1
Here the code of BeatsInput and SyslogInput:
import {
EuiText,
EuiTextArea,
EuiSpacer,
EuiFlexItem,
} from '#elastic/eui';
import React, { Fragment, useState } from 'react';
export const SyslogInput = ({onChangeTest}) => {
return (
<EuiFlexItem>
<EuiFlexItem >
<EuiSpacer />
<EuiText >
<EuiTextArea fullWidth={true}
style={{ height: "450px" }}
onChange={e => onChangeTest(e)}
placeholder="Scrivi l'input"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexItem>
)
}
import {
EuiText,
EuiTextArea,
EuiSpacer,
EuiFlexItem,
} from '#elastic/eui';
import React, { Fragment, useState } from 'react';
export const BeatsInput = ({onChangeTest}) => {
return (
<EuiFlexItem>
<EuiFlexItem >
<EuiSpacer />
<EuiText >
<EuiTextArea fullWidth={true}
style={{ height: "450px" }}
onChange={e => onChangeTest(e)}
placeholder="Scrivi l'input"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexItem>
)
}
Change initialSelectedTab to selectedTab [or just add it in addition to]
https://elastic.github.io/eui/#/navigation/tabs
You can also use the selectedTab and
onTabClick props to take complete control over tab selection. This can
be useful if you want to change tabs based on user interaction with
another part of the UI.
Or work around:
give tabs an empty default value
const [tabs, setTabs] = useState();
render the component conditionally around tabs
{tabs && (
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[0]}
autoFocus="selected"
onTabClick={tab => {
onTabConnectionTypeClicked(tab);
}}
/>
)}
I have a dynamic form as a functional component which is generated via a class based component. I want to make reset button which clears the input field values and sets the state to null array.
Full code is available here:
https://codesandbox.io/s/beautiful-archimedes-o1ygt
I want to make a reset button, clearing all the input values and initializing the Itemvalues array to null.
Even if I set the values to null, it doesn't clear the input field.
However, the problem I'm facing is that since, it is a dynamic form and a functional component it doesn't have a predefined state for each individual form field making it difficult to set value to null.
Can someone please help, I'm stuck on this from a long time
Here's a codesandbox to show you how to reset the items: https://codesandbox.io/s/romantic-heisenberg-93qi7
I also left a note for you on how to get this to work with your API data, see the comment inside onChangeText()
The problem is that the inputs are not controlled by state as you have deduced. We should create an updated object for each item from your API, giving it a value prop.
index.js
import React from "react";
import ReactDOM from "react-dom";
import Cart from "./Cart";
import "./styles.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
Items: [],
itemvalues: [{}]
};
this.onChangeText = this.onChangeText.bind(this);
this.getItems = this.getItems.bind(this);
this.handleReset = this.handleReset.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.findFieldIndex = this.findFieldIndex.bind(this);
this.trimText = this.trimText.bind(this);
}
getItems = () => {
/*if the data is coming from an API, store it in an array then .map() over it.
we can add a value prop to the object like:
so you can do something like:
const newItems = [...apiData].map((item) => {
return {
...item,
value: ""
}
})
this.setState({
Items: newItems
})
*/
this.setState({
Items: [
{
name: "item1",
description: "item1",
group: "groupA",
dtype: "str",
value: ""
},
{
name: "item2",
description: "item2",
group: "groupA",
dtype: "str",
value: ""
},
{
name: "item3",
description: "item3",
group: "groupB",
dtype: "str",
value: ""
},
{
name: "item4",
description: "item4",
group: "groupB",
dtype: "str",
value: ""
}
]
});
};
onChangeText = e => {
const updatedItems = [...this.state.Items].map(item => {
if (item.name === e.target.name) {
return {
...item,
value: e.target.value
};
} else {
return item;
}
});
const updatedItemValues = [...updatedItems].reduce((obj, curr) => {
if (!obj[curr.group]) {
obj[curr.group] = [];
}
obj[curr.group] = [...obj[curr.group], { [curr.name]: curr.value }];
return obj;
}, {});
this.setState({
...this.state,
Items: updatedItems,
itemvalues: updatedItemValues
});
};
findFieldIndex = (array, name) => {
return array.findIndex(item => item[name] !== undefined);
};
trimText(str) {
return str.trim();
}
handleReset = () => {
const resetedItems = [...this.state.Items].map(item => {
return {
...item,
value: ""
};
});
this.setState(
{
...this.state,
Items: resetedItems,
itemvalues: []
},
() => console.log(this.state)
);
};
handleSubmit = () => {
console.log(this.state.itemvalues);
};
render() {
return (
<div>
{
<Cart
Items={this.state.Items}
getItems={this.getItems}
handleSubmit={this.handleSubmit}
handleReset={this.handleReset}
onChangeText={this.onChangeText}
/>
}
</div>
);
}
}
Cart.js
import React, { useEffect } from "react";
import Form from "./Form";
const Cart = props => {
useEffect(() => {
props.getItems(props.Items);
}, []);
return (
<div>
<Form Items={props.Items} onChangeText={props.onChangeText} />
<button onClick={props.handleSubmit}>Submit</button>
<button onClick={props.handleReset}>Reset</button>
</div>
);
};
export default Cart;
The Cart component can remain mostly the same, we do not need to pass in props.items to useEffect() dependency.
Form.js
import React from "react";
const Form = props => {
return (
<div>
{props.Items.map(item => {
return (
<input
name={item.name}
placeholder={item.description}
data-type={item.dtype}
data-group={item.group}
onChange={e => props.onChangeText(e)}
value={item.value}
/>
);
})}
</div>
);
};
export default Form;
Now in Form component, we provide each input a value prop that is connected to the item our upper-most parent component-state.
That's pretty much all you need to reset the values.
See if that works for you:
Working example on CodeSandbox
Since you were already using hooks in part of your code, I've converted your class into a functional component using hooks (my advice: learn hooks and forget about class components).
I've added a value property to your INITIAL_STATE so it will keep the input value for each inputItem.
Full CODE:
index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import FormV2 from "./FormV2";
import "./styles.css";
function App() {
const INITIAL_STATE = [
{
name: "item1",
description: "item1",
group: "groupA",
dtype: "str",
value: "" // ADDED VALUE PROPERTY TO KEEP THE INPUT VALUE
},
{
name: "item2",
description: "item2",
group: "groupA",
dtype: "str",
value: ""
},
{
name: "item3",
description: "item3",
group: "groupB",
dtype: "str",
value: ""
},
{
name: "item4",
description: "item4",
group: "groupB",
dtype: "str",
value: ""
}
];
const [inputItems, setInputItems] = useState(INITIAL_STATE);
function handleChange(event, index) {
const newValue = event.target.value;
setInputItems(prevState => {
const aux = Array.from(prevState);
aux[index].value = newValue;
return aux;
});
}
function handleReset() {
console.log("Reseting Form to INITIAL_STATE ...");
setInputItems(INITIAL_STATE);
}
function handleSubmit() {
inputItems.forEach(item =>
console.log(
"I will submit input: " + item.name + ", which value is: " + item.value
)
);
}
return (
<FormV2
handleSubmit={handleSubmit}
handleReset={handleReset}
handleChange={handleChange}
inputItems={inputItems}
/>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
FormV2.js
import React from "react";
function FormV2(props) {
const formInputItems = props.inputItems.map((item, index) => (
<div key={item.name}>
{item.name + ": "}
<input
type="text"
data-type={item.dtype}
data-group={item.group}
placeholder={item.description}
value={item.value}
onChange={event => props.handleChange(event, index)}
/>
</div>
));
return (
<React.Fragment>
<form>{formInputItems}</form>
<button onClick={props.handleSubmit}>Submit</button>
<button onClick={props.handleReset}>Reset</button>
<div>State: {JSON.stringify(props.inputItems)}</div>
</React.Fragment>
);
}
export default FormV2;
In order to control the values of the child components (Items) which I presume are input fields you need to be passing down their values from their parent component. So each of your items will have an item.value which is stored in the parent component's state.
That means that in the parent component you will be able to define a method which clears all of the item values it is storing in its state.
That will probably look something like
resetInputs = () => {
this.setState({
inputFields: this.state.inputFields.map(inputField => {
...inputField,
value: ''
}
})
}
Also you'll need to write what kind of tag you want for your code to work, like input.
So what you'll end up with for the code of the child component you shared is something like:
const Form = (props) => {
return (
<div>
{props.Items.map(item => (
<input
name={item.name}
value={item.value}
placeholder={item.description}
onChange={e => props.onChangeText(e)}
/>
)
)}
</div>
);
}
export default Form
You want to manage the state of unknown number N of items, one way to achieve it is by managing a single object which contains all states, for example, setValuesManager manages N inputs and clicking the button reset its state:
function TextAreaManager() {
const [valuesManager, setValuesManager] = useState([...items]);
return (
<Flexbox>
{valuesManager.map((value, i) => (
<TextBoxItem
key={i}
value={value}
onChange={e => {
valuesManager[i] = e.target.value;
setValuesManager([...valuesManager]);
}}
/>
))}
<PinkButton
onClick={() =>
setValuesManager([...Array(valuesManager.length).fill('')])
}
>
Reset All
</PinkButton>
</Flexbox>
);
}
Demo: