I have been working with React Hooks for a while, but the biggest problem for me is working with arrays.
I am making a register form for teams. Teams have players (array of strings).
The user should be able to add a team, and for each team, an input is shown with the current members in the team displayed above the input.
My question: How do I set the state of a nested array with React Hooks?
On the button click, it should (for now) add a string to the players array of the current team.
My Code:
interface ITeam {
id: string;
players: Array<string>;
}
export default function Team() {
const [teams, setTeams] = useState<Array<ITeam>>([{id: '1', players: ['a', 'b']}]);
return (
<div>
{teams.map((team, teamIndex) => {
return (
<div key={teamIndex}>
<h2>Team {teamIndex + 1}</h2>
<ul>
{team.players.map((player, playerIndex) => {
return (
<div key={playerIndex}>
{player}
</div>
);
})}
</ul>
<button onClick={() => setTeams([...teams, team.players.concat('c')])}>Add player</button>
</div>
);
})}
</div>
);
}
You need to make use of team index and update that particular teams value using spread syntax and slice like
function addPlayer(index) {
setTeams(prevTeams => {
return [ ...prevTeams.slice(0, index), {...prevTeams[index], players: [...prevTeams[index].players, "c"] }, ...prevTeams.slice(index+1)];
});
}
or better you can just use map to update
function addPlayer(index) {
setTeams(prevTeams => {
return prevTeam.map((team, idx) => {
if(index === idx) {
return {...prevTeams[index], players: [...prevTeams[index].players, "c"]}
} else {
return team;
}
})
});
}
const { useState } = React;
function Team() {
const [teams, setTeams] = useState([{ id: "1", players: ["a", "b"] }]);
function addPlayer(index) {
setTeams(prevTeams => {
return [ ...prevTeams.slice(0, index), {...prevTeams[index], players: [...prevTeams[index].players, "c"] }, ...prevTeams.slice(index+1)];
});
}
return (
<div>
{teams.map((team, teamIndex) => {
return (
<div key={teamIndex}>
<h2>Team {teamIndex + 1}</h2>
<ul>
{team.players.map((player, playerIndex) => {
return <div key={playerIndex}>{player}</div>;
})}
</ul>
<button onClick={() => addPlayer(teamIndex)}>Add player</button>
</div>
);
})}
</div>
);
}
ReactDOM.render(<Team />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Related
I'm using React Hook Form to build a basic page builder application and it's been brilliant so far, I've been using the useFieldArray hook to create lists that contain items, however, I haven't found a way to move items between lists.
I know I can use the move() function to reorder items within the same list, however, since each list has its own nested useFieldArray I can't move the item from one list component to another list component.
If anyone knows of a way around this it would be much appreciated!
Here is a very simplified example of my current setup:
export const App = () => {
const methods = useForm({
defaultValues: {
lists: [
{
list_id: 1,
items: [
{
item_id: 1,
name: 'Apple'
},
{
item_id: 2,
name: 'Orange'
}
]
},
{
list_id: 2,
items: [
{
item_id: 3,
name: 'Banana'
},
{
item_id: 4,
name: 'Lemon'
}
]
}
]
}
});
return (
<FormProvider {...methods}>
<Page/>
</FormProvider>
)
}
export const Page = () => {
const { control } = useFormContext();
const { fields } = useFieldArray({
control,
name: 'lists'
})
return (
<ul>
{fields?.map((field, index) => (
<List listIdx={index} />
))}
</ul>
)
}
export const List = ({ listIdx }) => {
const { control, watch } = useFormContext();
const { fields, move } = useFieldArray({
control,
name: `lists[${sectionIdx}].items`
})
const handleMove = (prevIdx, nextIdx) => {
// this allows me to move within lists but not between them
move(prevIdx, nextIdx);
}
return (
<li>
<p>ID: {watch(lists[${listIdx}].list_id)}</p>
<ul>
{fields?.map((field, index) => (
<Item listIdx={index} itemIdx={index} handleMove={handleMove}/>
))}
</ul>
</li>
)
}
export const Item = ({ listIdx, itemIdx, handleMove }) => {
const { control, register } = useFormContext();
return (
<li>
<p>ID: {watch(lists[${listIdx}].items[${itemIdx}].item_id)}</p>
<label
Name:
<input { ...register('lists[${listIdx}].items[${itemIdx}]) }/>
/>
<button onClick={() => handleMove(itemIdx, itemIdx - 1)}>Up</button>
<button onClick={() => handleMove(itemIdx, itemIdx + 1)}>Down</button>
</div>
)
}
Thanks in advance!
If you'd not like to alter your default values (your data structure), I think the best way to handle this is using update method returning from useFieldArray. You have the data of both inputs that are going to be moved around, knowing their list index and item index, you could easily update their current positions with each other's data.
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,
};
});
}
import React, { PropTypes } from 'react';
import { Link, browserHistory } from 'react-router';
import * as DataConnectionAction from '../../actions/dataconnectionAction.jsx';
import DataConnectionStore from '../../store/dataconnectionstore.jsx';
class DataSource extends React.Component {
constructor(props) {
super(props);
this.state = {
datasourcelist: [],
};
this._dataconnectionStoreChange = this._dataconnectionStoreChange.bind(this);
}
componentWillMount() {
DataConnectionStore.on('change', this._dataconnectionStoreChange);
}
componentWillUnmount() {
DataConnectionStore.removeListener('change', this._dataconnectionStoreChange);
}
componentDidMount() {
DataConnectionAction._getDataSourcesList();
}
_dataconnectionStoreChange(type) {
if (type == 'DataSourcesList') {
let datasourcelist = DataConnectionStore._getDataSourceList() || {};
this.setState({ datasourcelist: datasourcelist.dataconnections });
}
}
DataSourceView(el) {
let data = {
id: el.dataConnectionName
}
}
_handleSearchChange(e) {
let value = e.target.value;
let lowercasedValue = value.toLowerCase();
let datasourcedata = this.state.datasourcelist;
let datasourcelist = datasourcedata && datasourcedata.filter(el => el.dataConnectionName.toLowerCase().includes(lowercasedValue));
this.setState({ datasourcelist });
}
DataSourcesCardUI() {
let datasourcedata = this.state.datasourcelist;
return (
datasourcedata && datasourcedata.map((el) =>
<div key={el.key}>
<div className="col-md-3 topadjust">
<div className="panel panel-default datasource_panel ">
<div className="panel-heading">
<h5 className="panel_title"><i className="fa fa-database"></i> {el.dataConnectionName}</h5>
</div>
<Link className="panel-body" onClick={this.DataSourceView.bind(this, el)}>
<div className="datasource_txt text-center">
<h6>{el.databaseHost}</h6>
<h6>{el.dataConnectionType} </h6>
<p>{el.createdDate}</p>
</div>
</Link>
</div>
</div>
</div>
)
);
}
render() {
return (
<div>
<section className="content_block">
<div className="container-fluid">
<div className="row dashboard_list">
{this.DataSourcesCardUI()}
</div>
</div>
</section>
</div>
);
}
}
export default DataSource;
Here I am getting one issue, that is I can able to filter based on the dataConnectionName, but when I am trying to filter with change of name it is filtering from the first filter array data.
But, I need to filter based on data array if i remove and type again.
Example:
when I tried search with Cu I am getting properly. but again when i remove Cu and search for User It is not searching from data array It is searching from filter array data. Instead of that when i remove and search with other key it should get filtered from data array.
Please Guide me what i am doing wrong.
Instead of overwriting the data in your state, you could keep a separate array in which you put all the elements that match the search.
Example
let data = [
{
dataConnectionName: "Customer_Details",
dataConnectionType: "NO_SQL",
databaseHost: "17.8.10.26",
pluginName: "AGT1_Customer_Details",
createdDate: "2018-09-23",
createBy: "Admin"
},
{
dataConnectionName: "User_Details",
dataConnectionType: "NO_SQL",
databaseHost: "17.8.10.26",
pluginName: "AGT1_Customer_Details",
createdDate: "2018-09-24",
createBy: "Admin"
},
{
dataConnectionName: "Manager_Details",
dataConnectionType: "NO_SQL",
databaseHost: "17.8.10.26",
pluginName: "AGT1_Customer_Details",
createdDate: "2018-09-25",
createBy: "Admin"
},
{
dataConnectionName: "Director_Details",
dataConnectionType: "NO_SQL",
databaseHost: "17.8.10.26",
pluginName: "AGT1_Customer_Details",
createdDate: "2018-09-26",
createBy: "Admin"
}
];
// Give each element a unique id that is used as key
data.forEach(el => el.id = Math.random());
class App extends React.Component {
state = {
data,
filteredData: data
};
_handleSearchChange = e => {
const { value } = e.target;
const lowercasedValue = value.toLowerCase();
this.setState(prevState => {
const filteredData = prevState.data.filter(el =>
el.dataConnectionName.toLowerCase().includes(lowercasedValue)
);
return { filteredData };
});
};
render() {
const { filteredData } = this.state;
return (
<div>
<input onChange={this._handleSearchChange} placeholder="Search"/>
{filteredData.map(el => (
<div key={el.key}>
<div>
{el.dataConnectionName} - {el.pluginName} - {el.createdDate} - {el.createBy}
</div>
</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>
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 object's array of users and i'm using map to show them, each user have a option buttons that is 'edit' and 'remove' options each option have a onlclick function that set a state to show another view so the code explain itselft
class App extends React.Component {
state = {
edit: false,
remove: false
}
handleEdit = () => {
this.setState({ edit: true })
}
handleRemove = () => {
this.setState({ remove: true })
}
cancelEdit = () => {
this.setState({ edit: false })
}
cancelRemove = () => {
this.setState({ remove: false })
}
renderEditItem = () => {
const {
state: {
edit,
remove
},
cancelEdit,
cancelRemove,
handleEdit,
handleRemove
} = this
if (edit) {
return (
<div>
<span>Edit view</span>
<br/>
<button onClick={cancelEdit}>Cancel</button>
</div>
)
}
if (remove) {
return (
<div>
<span>Remove view</span>
<br/>
<button onClick={cancelRemove}>Cancel</button>
</div>
)
}
return (
<div>
<button onClick={handleEdit}>Edit</button>
<br/>
<button onClick={handleRemove}>Remove</button>
</div>
)
}
renderUsers = () => {
const {
renderEditItem
} = this
const users = [
{
id: 1,
name: 'User1'
},
{
id: 2,
name: 'User-2'
},
{
id: 3,
name: 'User-3'
}
]
return users.map((user) => {
return (
<ul key={user.id}>
<li>
<div>
<span ref='span'>{user.name}</span>
<br/>
{renderEditItem()}
</div>
</li>
</ul>
)
})
}
render () {
return (
<div>
{this.renderUsers()}
</div>
)
}
}
React.render(
<App />,
document.getElementById('app')
);
JSfiddle: Here
The issue is how can you see is, when i click on the button to set the state for edit or remove option, this will show the view for all the items,
and should be only the view that is clicked, i know the state change to true and is the same for all the items but i don't know how to set the state only for one entry any idea?
Thank you in advance.
Your problem is that the edit/remove state is singular and for the entire list. Each item in the list receives the same state here:
if (edit) {
return (
<div>
<span>Edit view</span>
<br/>
<button onClick={cancelEdit}>Cancel</button>
</div>
)
}
The single edit variable from the state is applied to each list item. If you want to individually set the edit state for each item, it will need to be kept track of with that item.
EX:
const users = [
{
id: 1,
name: 'User1',
edit: true
}]
This way each individual item will be able to tell what state it is in individually. User1 item will have an edit mode that is independent of the other users.
Then you can render something like this:
return users.map((user) => {
return (
<ul key={user.id}>
<li>
<div>
<span ref='span'>{user.name}</span>
<br/>
{user.edit ? 'EDIT MODE' : 'NOT EDIT MODE'}
</div>
</li>
</ul>
)
})