I have two classes. One holds the array, the other holds the array props. These are my classes:
//PARENT CLASS:
constructor() {
super()
this.state = {
items: []
}
this.addItem = this.addItem.bind(this)
}
componentDidMount(){
this.setState({
items: [{
name: 'Sebastian',
num: '001'
},{
name: 'Josh',
num: '002'
}]
})
}
addItem() {
??????
}
render() {
return(
<div>
<MethodA items={this.state.items} addItem={this.addItem}/>
</div>
)
}
//CHILD CLASS:
function MethodA(props) {
return(
<div>
{props.items.map((item, i) =>{
return(<div key={i}>
<span>{item.name}</span>
<span>{item.num}</span>
</div>)
})}
<button onClick={() => { props.addItem() }}>ADD ITEM</button>
</div>
)
}
Current result is like this:
<div>
<span>Sebastian</span>
<span>001</span>
</div>
<div>
<span>Sebastian</span>
<span>002</span>
</div>
Then after the "ADD ITEM" button was hit, this will be the new result:
<div>
<span>Sebastian</span>
<span>001</span>
</div>
<div>
<span>Sebastian</span>
<span>002</span>
</div>
<div>
<span>New Name</span>
<span>New Num</span>
</div>
I'm not sure whether what and how to use between push() or concat() or both. Any ideas?
Firstly, there's no need to set the initial state in componentDidMount, you can do it directly in constructor.
constructor(props) {
super(props);
this.state = {
items: [
{
name: "Sebastian",
num: "001"
},
{
name: "Josh",
num: "002"
}
]
};
this.addItem = this.addItem.bind(this);
}
To add an item you can use functional form of setState and you'll need to pass that item into callback from the child component.
addItem(item) {
this.setState(state => ({
items: [...state.items, item]
}));
}
// Child class
function MethodA(props) {
return(
<div>
{props.items.map((item, i) =>{
return(<div key={i}>
<span>{item.name}</span>
<span>{item.num}</span>
</div>)
})}
<button onClick={() => props.addItem(item)}>ADD ITEM</button> // Pass item to the parent's method
</div>
)
}
Here's the deal. The difference between push() and concat() is in immutability.
If you use push on an array, it will mutate the original array and add a new value to that array (wrong).
If you use concat, it will create a new array for you, leaving the old array untouched (correct).
So you might want to do something along these lines:
addItem(item)
this.setState(state => {
const items = state.items.concat(item);
return {
items,
};
});
}
Related
I have a class component that Renders a list of elements and I need to focus them when an event occurs.
Here is an example code
class page extends React.Component {
state = {
items: [array of objects]
}
renderList = () => {
return this.state.items.map(i => <button>{i.somekey}</button>)
}
focusElement = (someitem) => {
//Focus some item rendered by renderList()
}
render(){
return(
<div>
{this.renderList()}
<button onClick={() => focusElement(thatElement)}>
</div>
)
}
}
I know that I need to use refs but I tried several ways to do that and I couldn't set those refs properly.
Can someone help me?
you should use the createRefmethod of each button that you would like to focus, also you have to pass this ref to the focusElement method that you have created:
const myList = [
{ id: 0, label: "label0" },
{ id: 1, label: "label1" },
{ id: 2, label: "label2" },
{ id: 3, label: "label3" },
{ id: 4, label: "label4" },
{ id: 5, label: "label5" }
];
export default class App extends React.Component {
state = {
items: myList,
//This is the list of refs that will help you pick any item that ou want to focus
myButtonsRef: myList.map(i => React.createRef(i.label))
};
// Here you create a ref for each button
renderList = () => {
return this.state.items.map(i => (
<button key={i.id} ref={this.state.myButtonsRef[i.id]}>
{i.label}
</button>
));
};
//Here you pass the ref as an argument and just focus it
focusElement = item => {
item.current.focus();
};
render() {
return (
<div>
{this.renderList()}
<button
onClick={() => {
//Here you are able to focus any item that you want based on the ref in the state
this.focusElement(this.state.myButtonsRef[0]);
}}
>
Focus the item 0
</button>
</div>
);
}
}
Here is a sandbox if you want to play with the code
I'm building a shopping cart application and I ran into a problem where all my inputs have the same state value. Everything works fine but when I type in one input box, it's the same throughout all my other inputs.
I tried adding a name field to the input and setting my initial state to undefined and that works fine but the numbers don't go through.
How do we handle inputs to be different when they have the same state value? Or is this not possible / dumb to do?
class App extends Component {
state = {
items: {
1: {
id: 1, name: 'Yeezys', price: 300, remaining: 5
},
2: {
id: 2, name: 'Github Sweater', price: 50, remaining: 5
},
3: {
id: 3, name: 'Protein Powder', price: 30, remaining: 5
}
},
itemQuantity: 0
},
render() {
return (
<div>
<h1>Shopping Area</h1>
{Object.values(items).map(item => (
<div key={item.id}>
<h2>{item.name}</h2>
<h2>$ {item.price}</h2>
{item.remaining === 0 ? (
<p style={{ 'color': 'red' }}>Sold Out</p>
) : (
<div>
<p>Remaining: {item.remaining}</p>
<input
type="number"
value={ itemQuantity }
onChange={e => this.setState({ itemQuantity: e.target.value})}
placeholder="quantity"
min={1}
max={5}
/>
<button onClick={() => this.addItem(item)}>Add To Cart</button>
</div>
)}
</div>
))}
</div>
)
}
}
If you are using same state key for all input, All input take value from one place and update to one place. To avoid this you have to use separate state. I suppose you are trying to show input for a list of item.
To achive you can create a component for list item and keep state in list item component. As each component have their own state, state value will not conflict.
Here is an example
class CardItem extends Component {
state = {
number: 0
}
render() {
render (
<input type="text" value={this.state.number} onChange={e => this.setState({ number: e.target.value })} />
)
}
}
class Main extends Component {
render () {
const list = [0,1,2,3,4]
return (
list.map(item => <CardItem data={item} />)
)
}
}
This is a solution which the problem is loosely interpreted, but it does work without having to create another component. As you know, you needed to separate the state of each items in the cart. I did this by dynamically initializing and setting the quantity states of each item. You can see the state changes with this example:
class App extends React.Component {
constructor(props) {
super(props);
this.state = { quantities: {} }
}
componentDidMount() {
let itemIDs = ['1', '2', '3', 'XX']; //use your own list of items
itemIDs.forEach(id => {
this.setState({quantities: Object.assign(this.state.quantities, {[id]: 0})});
})
}
render() {
let list = Object.keys(this.state.quantities).map(id => {
return (
<div>
<label for={id}>Item {id}</label>
<input
id={id}
key={id}
type="number"
value={this.state.quantities[id]}
onChange={e => {
this.setState({quantities: Object.assign(this.state.quantities, {[id]: e.target.value})})
}}
/>
</div>
);
})
return (
<div>
{list}
<div>STATE: {JSON.stringify(this.state)}</div>
</div>
);
}
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='root'></div>
You can modify the state structure to your liking.
Here is how I usually handle this scenario. You say that you get an array of items? Each item object should contain a key to store the value (count in my example). You can use a generic onChange handler to update an individual item in the array. So now, your state is managing the list of items instead of each individual input value. This makes your component much more flexible and it will be able to handle any amount of items with no code changes:
const itemData = [
{ id: 0, count: 0, label: 'Number 1' },
{ id: 1, count: 0, label: 'Number 2' },
{ id: 2, count: 0, label: 'Number 3' },
{ id: 3, count: 0, label: 'Number 4' }
];
class App extends React.Component {
state = {
items: itemData
}
handleCountChange = (itemId, e) => {
// Get value from input
const count = e.target.value;
this.setState( prevState => ({
items: prevState.items.map( item => {
// Find matching item by id
if(item.id === itemId) {
// Update item count based on input value
item.count = count;
}
return item;
})
}))
};
renderItems = () => {
// Map through all items and render inputs
return this.state.items.map( item => (
<label key={item.label}>
{item.label}:
<input
type="number"
value={item.count}
onChange={this.handleCountChange.bind(this, item.id)}
/>
</label>
));
};
render() {
return (
<div>
{this.renderItems()}
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
label {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
You can't use the same state for the both inputs. Try to use a different state for each one like that:
class App extends Component {
state = {
number: ""
}
render() {
return (
<div>
<input
type="number"
value={this.state.number}
onChange={e => this.setState({ number: e.target.value })}
/>
<input
type="number"
value={this.state.number2}
onChange={e => this.setState({ number2: e.target.value })}
/>
</div>
)
}
}
I am trying to recursively render JSON data to nested list using React. Right now I am using simple data object like this:
[{"id": "1",
"name": "Luke"
},
{"id": "2",
"name": "Jim",
"childNodes":[{
"id": "3",
"name": "Lola"
}]
}]
using this class:
export default class NestedList extends Component {
constructor(props) {
super(props);
this.state = {
visible: true
};
}
toggle = () => {
this.setState({ visible: !this.state.visible });
};
renderChild = (child) => {
if (child.childNodes) {
return (
<ul>
{child.myData.map(item => {
return this.renderChild(item);
})}
</ul>
);
}
else if (child.name) {
return <input type="checkbox"><Child name={child.name}/></input>;
}
return null;
}
render() {
return (
<aside>
<div>
<h4>Data Sets</h4>
<ul>
{this.renderChild(this.props.myData)}
</ul>
</div>
</aside>
);
}
}
which calls a Child class that creates list element:
export default class Child extends Component {
render() {
let {name}=this.props;
return (
<li>{name}</li>
);
}
}
but it doesn't print anything. I have tried removing attribute childNodes altogether and tried to print the list but it doesn't work still. I don't understand where I am doing wrong. I would appreciate some help regarding how to fix this.
You need to map through myData first so the rendering process begins:
<ul>
{this.props.myData.map(data => this.renderChild(data))}
</ul>
Also, on childNodes you need to loop through child.childNodes:
if (child.childNodes) {
return (
<ul>
{child.childNodes.map(node => this.renderChild(node))}
</ul>
);
}
there were couple of issues here:
You passed myData to renderChild which doesn't hold childNodes
property nor name property. Hence none of the conditions were met
(null was returned).
So maybe you should loop through myData and
pass each member of the array to renderChild.
Even if we will pass a valid "child" to the renderChild method,
inside this condition:
if (child.childNodes) {
Again you are using a wrong property:
<ul>
{child.myData.map(item => {
return this.renderChild(item);
})}
</ul>
this should be:
{child.childNodes.map(item => {...
Last thing, You can't nest child elements inside an input element.
so change the layout, maybe like this? :
<input type="checkbox"/>
<Child name={child.name} />
Here is a running example with your code:
const data = [
{
id: "1",
name: "Luke"
},
{
id: "2",
name: "Jim",
childNodes: [
{
id: "3",
name: "Lola"
}
]
}
];
class NestedList extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: true
};
}
toggle = () => {
this.setState({ visible: !this.state.visible });
};
renderChild = child => {
if (child.childNodes) {
return (
<ul>
{child.childNodes.map(item => {
return this.renderChild(item);
})}
</ul>
);
} else if (child.name) {
return (
<div>
<input type="checkbox"/>
<Child name={child.name} />
</div>
);
}
return null;
};
render() {
return (
<aside>
<div>
<h4>Data Sets</h4>
<ul>{this.props.myData.map(item => this.renderChild(item))}</ul>
</div>
</aside>
);
}
}
class Child extends React.Component {
render() {
let { name } = this.props;
return <li>{name}</li>;
}
}
ReactDOM.render(<NestedList myData={data} />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I'm rendering a list of inputs and I want to bind each input's value to a link's href. My current attempt renders https://twitter.com/intent/tweet?text=undefined:
class App extends React.Component {
tweets = [
{ id: 1, link: 'example.com' },
{ id: 2, link: 'example2.com' }
];
render() {
return (
<div>
{this.tweets.map(tweet =>
<div key={tweet.id}>
<input type="text" placeholder="text" onChange={e => tweet.text = e.target.value} />
<a href={`https://twitter.com/intent/tweet?text=${tweet.text}`}>Tweet</a>
</div>
)}
</div>
);
}
}
This probably needs to involve setState but I have no idea how to achieve that when rendering a list. I've tried to do some research on this but didn't found anything helpful.
JSFiddle: https://jsfiddle.net/nunoarruda/u5c21wj9/3/
Any ideas?
You can move the tweets variable to the state to maintain consistency in that array.
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
tweets: [
{ id: 1, link: 'example.com' },
{ id: 2, link: 'example2.com' }
]
};
};
setTweets = index => e => {
const { tweets } = this.state
tweets[index].text = e.target.value
this.setState({ tweets })
}
render() {
const { tweets } = this.state
return (
<div>
{tweets.map((tweet, index) =>
<div key={tweet.id}>
<input type="text" placeholder="text" onChange={this.setTweets(index)} />
<a href={`https://twitter.com/intent/tweet?text=${tweet.text}`}>Tweet</a>
</div>
)}
</div>
);
}
}
Updated Jsfiddle: https://jsfiddle.net/u5c21wj9/6/
You can reach the desired result using state.
return (
<div>
{tweets.map(({ id, link }) =>
<div key={id}>
<input type="text" placeholder="text" onChange={({ target }) => this.setState({ [id]: target.value })} />
<a href={`https://twitter.com/intent/tweet?text=${this.state[id] || link}`}>Tweet</a>
</div>
)}
</div>
);
Note: I would move tweets outside the component and implement few ES6 features.
Updated Jsfiddle: https://jsfiddle.net/u5c21wj9/7/
You really should use a state here and make your tweets variable be part of it. To do that, add a constructor:
constructor() {
super();
this.state = {
tweets: [
{ id: 1, link: 'example.com' },
{ id: 2, link: 'example2.com' }
]
};
}
Then you need to mutate each linkwhenever you type in one of the inputs. There are a few pitfalls here, so let me go through them one-by-one:
changeTweet = (id, e) => {
let arr = this.state.tweets.slice();
let index = arr.findIndex(i => i.id === id);
let obj = Object.assign({}, arr[index]);
obj.link = e.target.value;
arr[index] = obj;
this.setState({tweets: arr});
}
First, you need to create a copy of your state variable. This gives you something to work with, without mutating the state directly which is anti-pattern. This can be done with slice().
Since you are sending in the id of the object to modify, we need to find it in our array (in case the items are unordered). This is done with findIndex(). You might want to handle the scenario in which such index is not found (I have not done that).
Now we know where in the array the object with the given id key is. Now, create a copy of that item (which is an object). This is also to prevent mutating the state directly. Do this with Object.assign().
Now change the link to the input value we typed in. Replace the old item object with the new one (obj) and replace the old tweets array with the new one (arr).
Here's the full example:
class App extends React.Component {
constructor() {
super();
this.state = {
tweets: [
{ id: 1, link: 'example.com' },
{ id: 2, link: 'example2.com' }
]
};
}
changeTweet = (id, e) => {
let arr = this.state.tweets.slice();
let index = arr.findIndex(i => i.id === id);
let obj = Object.assign({}, arr[index]);
obj.link = e.target.value;
arr[index] = obj;
this.setState({tweets: arr});
}
render() {
return (
<div>
{this.state.tweets.map(tweet =>
<div key={tweet.id}>
<input type="text" placeholder="text" onChange={(e) => this.changeTweet(tweet.id, e)} />
<a href={`https://twitter.com/intent/tweet?text=${tweet.link}`}>Tweet</a>
</div>
)}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
You need to save the text from the input in the state (using setState), not in the tweets array. Then you can render it getting the text from the state.
class App extends React.Component {
tweets = [
{ id: 1, link: 'example.com' },
{ id: 2, link: 'example2.com' }
];
state = {
tweetsText :{}
}
handleTextChange = (event, tweetId) => {
const tweetsTextCopy = Object.assign({}, this.state.tweetsText)
tweetsTextCopy[tweetId] = event.target.value
this.setState({tweetsText: tweetsTextCopy})
}
render() {
return (
<div>
{this.tweets.map(tweet =>
<div key={tweet.id}>
<input type="text" placeholder="text" onChange={e => this.handleTextChange(e, tweet.id)} />
<a href={`https://twitter.com/intent/tweet?text=${this.state.tweetsText[tweet.id]}`}>Tweet</a>
</div>
)}
</div>
);
}
}
Links info is in the link property of your tweets array. The property text is not defined.
So, your render function should look like this
render() {
return (
<div>
{this.tweets.map(tweet =>
<div key={tweet.id}>
<input type="text" placeholder="text" onChange={e => tweet.text= e.target.value} />
<a href={`https://twitter.com/intent/tweet?text=${tweet.link}`}>Tweet</a>
</div>
)}
</div>
);
}
I have a simple todolist structure into the state of component. This is an array with two field called 'content' and one called 'done'. I catch click of row item with a simple onClick() function passed from parent to TodoItem childs (like React tutorial suggest):
render() {
const tthis = this;
var myList = tthis.state.todos.map(function(todo,index){
var myOnCLick = function(){
var newTodo = {content: todo.content, done: !todo.done};
tthis.state.todos[index] = newTodo;
tthis.setState({});
}
return <Todo key={index} todo={todo} onClick={myOnCLick}/>
})
return (
<ul className="list">
{ myList }
</ul>
)
}
This code work without problems, but I don't like so much.
I would find some good solution to change a value of single item in an array. I found that in Immutability helper of React DOC:
{$set: any} replace the target entirely.
And in a good answer in this forum I saw an example:
this.setState({
todos: update(this.state.todos, {1: {done: {$set: true}}})
But I cannot use 1 in my case. I have index which give me the index of clicked todo in todos list.
You can flip the value of the done property on the individual todo object listed in the array in the following way:
flipDone(id) {
let index = Number(id);
this.setState({
todos: [
...this.state.todos.slice(0, index),
Object.assign({}, this.state.todos[index], {done: !this.state.todos[index].done}),
...this.state.todos.slice(index + 1)
]
});
}
Here is a demo: http://codepen.io/PiotrBerebecki/pen/jrdwzB
Full code:
class TodoList extends React.Component {
constructor() {
super();
this.flipDone = this.flipDone.bind(this);
this.state = {
todos: [
{content: 'Go shopping', done: false},
{content: 'Walk the dog', done: false},
{content: 'Wash the dishes', done: false},
{content: 'Learn React', done: false}
]
};
}
flipDone(id) {
let index = Number(id);
this.setState({
todos: [
...this.state.todos.slice(0, index),
Object.assign({}, this.state.todos[index], {done: !this.state.todos[index].done}),
...this.state.todos.slice(index + 1)
]
});
}
render() {
const myList = this.state.todos.map((todo, index) => {
return (
<Todo key={index}
clickHandler={this.flipDone}
content={todo.content}
done={todo.done}
id={index}
/>
);
})
return (
<ul className="list">
{myList}
</ul>
);
}
}
class Todo extends React.Component {
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
this.props.clickHandler(event.target.id);
}
render() {
return (
<li>
<button onClick={this.handleClick}
id={this.props.id}>
Click me
</button> ---
{String(this.props.done)} ---
{this.props.content}
</li>
);
}
}
ReactDOM.render(
<TodoList />,
document.getElementById('app')
);