React. Delete specific row from the table - javascript

I have a component that is a table.
Each row of this table is also component.
class FormulaBuilder extends Component {
constructor(props) {
super(props);
this.state = {
rows: [{}]
}
}
handleAddRow = () => {
const item = {};
this.setState({
rows: [...this.state.rows, item]
});
};
handleRemoveSpecificRow = (idx) => {
const rows = [...this.state.rows]
rows.splice(idx, 1)
this.setState({ rows })
}
render() {
return (
{
this.state.rows.map((item, idx) => {
return (
<React.Fragment key={idx}>
<ConcoctionRow
removeSpecificRow={(idx) =>this.handleRemoveSpecificRow(idx)}
id={idx} />
</React.Fragment>);
})
});
}
}
In the child component there is a button. When clicked, the event from the parent component is called:
class ConcoctionRow extends Component {
constructor(props) {
super(props);
}
handleRemoveSpecificRow = () => {
this.props.removeSpecificRow(this.props.id);
}
}
The properties passed the index of the array. But only the last line is always deleted not specific.
Where is my bad? P.S. I am new in JS.

A couple of things, you want to avoid using .splice() to update your arrays in components. Often times this actually ends up mutating your original state instead of creating a new one. A direct violation of React concepts.
Likewise lets try some stuff out on the console:
const arr = [1, 2, 3] <-- this is your state
const newArr = arr <-- you created a reference of your state. This does not actually create a new copy.
Now if you splice the newArr
newArr.splice(0, 1) <-- now newArr = [2, 3]
Well guess what, you also mutated your original state.
arr <-- is now also [2, 3]
A common misconception in JavaScript is that when you create a new variable that equals an existing variable, you expect that it actually creates a new copy.
let cat = {id: 1, name: "bunny"}
let myCat = cat
This is not actually the case, instead of explicitly creating a new copy, your new variable points to the same reference of the original object it is derived from. If I did something like:
myCat.age = 2 <-- Now myCat has a new property of age.
myCat <-- {id: 2, name: "bunny", age: 2}
BUT, because these two variables point to the same reference. You also mutate the original cat object as well
cat <-- {id: 2, name: "bunny", age: 2}
Use array.filter() instead to create a completely new array.
Here's an example with your code as well as a sandbox for reference: https://codesandbox.io/s/heuristic-nobel-6ece5
import React from "react";
import ConcoctionRow from "./ConcoctionRow";
class FormulaBuilder extends React.Component {
constructor(props) {
super(props);
this.state = {
rows: [{}, {}, {}]
};
}
handleAddRow = () => {
const item = {};
this.setState({
rows: [...this.state.rows, item]
});
};
handleRemoveSpecificRow = idx => {
const { rows } = this.state;
const updatedRows = rows.filter((row, index) => {
return index !== idx;
});
this.setState({
rows: updatedRows
});
};
render() {
return (
<div>
{this.state.rows.map((item, idx) => {
return (
<React.Fragment key={idx}>
<ConcoctionRow
removeSpecificRow={this.handleRemoveSpecificRow}
id={idx}
/>
</React.Fragment>
);
})}
</div>
);
}
}
export default FormulaBuilder;

I show the pattern I would use for this case. I recommend to use id instead of array index for items.
filter array function is immutable (it creates a new array, not mutates the previous one), so ok to use in set state. The functional form of setState is also a good stuff.
const Row = ({ onClick, children, id }) => (
<li>{children} <button onClick={() => onClick(id)}>Delete</button></li>
)
class App extends React.Component {
state = {
list: [
{id: 1, label: 'foo' },
{id: 2, label: 'bar' }
]
}
handleDelete = id => {
this.setState(prevState => ({
list: prevState.list.filter(row => (
row.id !== id
))
}))
}
render(){
const { list } = this.state;
return (
<ul>
{list.map(({ id, label }) => (
<Row id={id} onClick={this.handleDelete}>{label}</Row>
))}
</ul>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<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>
<div id="root"></div>

Related

How to hide a specific element inside .map function in React?

I am looking for a way to hide a div once the button thats in it is clicked and continue to show all other div's.
I've tried using the setState method, however when setting it to false with onClick() all of my objects disappear.
class App extends React.PureComponent {
state: {
notHidden: false,
}
constructor(props: any) {
super(props);
this.state = {search: '', notHidden: true};
this.hideObject = this.hideObject.bind(this)
}
hideThisDiv() {
this.setState({notHidden: false})
}
async componentDidMount() {
this.setState({
objects: await api.getObjects()
});
}
render = (objects: Object[]) => {
return ({Object.map((object) =>
<div key={index} className='class'>
<button className='hide' type='button' onClick={() => hideThisDiv()}>Hide</button>
<p>object.text</p>
</div>}
render() {
const {objects} = this.state;
return (<main>
<h1>Objects List</h1>
<header>
<input type="search" onChange={(e) => this.onSearch(e.target.value)}/>
</header>
{objects ? this.render(objects) : null}
</main>)
}
);
The data is a data.json file filled with many of these objects in the array
{
"uuid": "dsfgkj24-sfg34-1r134ef"
"text": "Some Text"
}
Edit: Sorry for the badly asked question, I am new to react.
Not tested, just a blueprint... is it what you want to do?
And yes I didn't hide, I removed but again, just an idea on how you can hide button separately, by keeping in state which ones are concerned.
function MagicList() {
const [hidden, hiddenSet] = useState([]);
const items = [{ id:1, text:'hello'}, { id:2, text:'from'}, { id:3, text:'the other sided'}]
const hideMe = id => hiddenSet([...hidden, id]);
return {
items.filter( item => {
return hidden.indexOf(item.id) !== -1;
})
.map( item => (
<button key={item.id} onClick={hideMe.bind(this, item.id)}>{item.text}</button>
))
};
}
Edition
const hideMe = id => hiddenSet([...hidden, id]);
It is just a fancy way to write:
function hideMe(id) {
const newArray = hidden.concat(id);
hiddenSet(newArray);
}
I suggest using a Set, Map, or object, to track the element ids you want hidden upon click of button. This provides O(1) lookups for what needs to be hidden. Be sure to render your actual text and not a string literal, i.e. <p>{object.text}</p> versus <p>object.text</p>.
class MyComponent extends React.PureComponent {
state = {
hidden: {}, // <-- store ids to hide
objects: [],
search: "",
};
// Curried function to take id and return click handler function
hideThisDiv = id => () => {
this.setState(prevState => ({
hidden: {
...prevState.hidden, // <-- copy existing hidden state
[id]: id // <-- add new id
}
}));
}
...
render() {
const { hidden, objects } = this.state;
return (
<main>
...
{objects
.filter((el) => !hidden[el.uuid]) // <-- check id if not hidden
.map(({ uuid, text }) => (
<div key={uuid}>
<button
type="button"
onClick={this.hideThisDiv(uuid)} // <-- attach handler
>
Hide
</button>
<p>{text}</p>
</div>
))}
</main>
);
}
}

React state returns only one element

I'm trying to modify state and take the new state to render.
When I click and modified(added {isClicked: true} to array), console.log(this.state.listOfQuotes) inside onClicked function returns modified the full array of state(which I want to use)
but after render, console.log(this.state.listOfQuotes) returns only one clicked element and not even modified one...
Any help/hint much appreciated!
Here is my code
import React from "react";
export class Quotes extends React.Component {
constructor(props) {
super(props);
this.state = { listOfQuotes: [] };
this.vote = this.vote.bind(this);
this.onClicked = this.onClicked.bind(this);
}
componentDidMount() {
const url = "https://programming-quotes-api.herokuapp.com/quotes";
fetch(url)
.then(res => res.json())
.then(quote => {
this.setState({
listOfQuotes: quote
});
});
}
onClicked(id) {
const quotes = [...this.state.listOfQuotes];
const clickedQuote = quotes.findIndex(quote => quote.id === id);
console.log("onclicked", clickedQuote);
const newArray = { ...quotes[clickedQuote], isClicked: true };
console.log(newArray);
this.setState(prevState => ({
listOfQuotes: [
...prevState.listOfQuotes.splice(clickedQuote, 1, newArray)
]
}));
console.log(this.state.listOfQuotes); ----------> this one returns what i want
}
render() {
console.log(this.state.listOfQuotes); -----------> i want to have same result as above state
return (
<div className="quotes">
<div>
{this.state.listOfQuotes.map((quote, idx) => (
<div key={idx}>
<div onClick={() => this.onClicked(quote.id)}>
{!quote.isClicked ? (
<div className="before-clicked">{quote.en}</div>
) : (
<div className="after-clicked">{quote.en}</div>
)}
</div>
<div>By {quote.author}</div>
<div>Rating {quote.rating}</div>
<div className="vote">
<span>{quote.numberOfVotes}</span>
</div>
</div>
))}
</div>
</div>
);
}
}
There is a problem with your onClicked method.
It is not modifying the array correctly.
In my opinion, this is how it could have done.
onClicked(id) {
let quotes = [...this.state.listOfQuotes];
const clickedQuoteIndex = quotes.findIndex(quote => quote.id === id);
// Modify the object on the found index and assign true to "isClicked"
quotes[clickedQuoteIndex].isClicked = true;
// And then setState with the modified array
// Since setState is async, so the console shouldn't be called immediately
// but rather in the callback
this.setState({ listOfQuotes: quotes }, () => {
console.log(this.state.listOfQuotes);
});
}

Array.splice works on both state sub-arrays

This is my simple react component. I set rooms firstly in state in componentWillReceiveProps and then on submit I do set rooms to data in state as well.
Also on submit I do an api call by passing single object from the rooms and when response come then I do slice that object from the data (but not from rooms) until the data length is equal to 0.
Now problem is when I do slice from the data then it slices the rooms elements as well.
class EditRoom extends Component {
constructor() {
super()
this.state = {
rooms: [],
data: []
}
}
componentWillMount() {
const { fetchRooms } = this.props
fetchRooms()
}
componentWillReceiveProps(np) {
const { rooms, setNick, setNickName } = np
if (this.props.rooms !== rooms) {
console.log('ppppppppppppp')
this.setState({ rooms })
}
if (setNick) {
const data = this.state.data
data.splice(0, 1)
this.setState({ data }, () => {
if (data.length === 0) {
console.log('pppp542545455453864')
} else {
const room = _.first(this.state.data)
setNickName(room)
}
})
}
}
handleInputChange(e, i) {
const { rooms } = this.state
rooms[i].nickname = e.target.value
this.setState({ rooms })
}
onSaveClick() {
const { setNickName } = this.props
this.setState({ data: this.state.rooms }, () => {
const room = _.first(this.state.data)
setNickName(room)
})
}
render() {
const { rooms } = this.state
return (
<div>
<main id="EditRoom">
{rooms &&
rooms.map((room, i) => {
return (
<div className="barcode-box" key={i} style={{ backgroundColor: this.getRandomColor(i) }}>
<div className="edit-room-name">
<input
type="text"
className="form-control"
style={{ color: '#ffffff' }}
name="cardNumber"
placeholder="Nickname"
value={_.get(room, 'nickname') || ''}
onChange={e => this.handleInputChange(e, i)}
/>
</div>
</div>
)
})}
</main>
</div>
)
}
}
What I am missing here?
Thank you !!!
You should not modify this.state directly, e.g. using array mutating methods like splice. Instead of this, make a copy from this.state.data sub-array, modify and pass it to setState().
Something like this:
const data = this.state.data.slice() // make a copy of data
data.splice(0, 1) // modify a copy
this.setState({ data }, ...) // pass to setState
[Update] Explanation of why changing one sub-array of state affects on another:
Arrays (as all objects) in JS are passed by reference. So if you do a simple assignment like arr2 = arr1, splice method will mutate the original array too. That's also true for nested arrays (objects) like in your case. data sub-array is stored with rooms together in state. So mutating data will affect rooms sub-array too.

Pass this.state not working ReactJS

I have a table that gets data and dynamically displays it,
Passing the "example" the table shows the data without problems,
but when i try to pass
< ResponseTable data={this.state.sprints} ,
which is also contains the same structure as example ( Array of objects )
it doesn't work
var example = [
{id: 1, sequence: 2, name: "Sprint 2018", state: "CLOSED", linkedPagesCount: 0},
{id: 2, sequence: 2, name: "Sprint 2018-1", state: "OPEN", linkedPagesCount: 0}
];
class Table extends React.Component{
constructor(props){
super(props);
this.state = {}
}
fetchData() {
fetch('http://localhost:8000/sprints/23')
.then(function(response) {
return response.json();
})
.then((myJson) => this.setState(myJson));
}
componentDidMount(){
this.fetchData();
}
render(){
console.log(this.state.sprints)
console.log(example)
return(
<div>
<ResponseTable data={example} />
</div>
);
}
}
TypeError: Cannot read property '0' of undefined
const props = Object.keys(data[0]);
columnsBuilder (data) {
> 12 | const props = Object.keys(data[0]);
13 | const columns = props.map( (item, index) => ({
14 | Header : item,
15 | accessor : item,
ReactTable ->
export default class ResponseTable extends React.Component {
constructor() {
super();
this.columnsBuilder = this.columnsBuilder.bind(this);
}
columnsBuilder (data) {
const props = Object.keys(data[0]);
const columns = props.map( (item, index) => ({
Header : item,
accessor : item,
Cell : propss => propss.original[item].length === 0 ? '[]' : propss.original[item].toString(),
}));
const built = [
{
Header : 'Response',
columns,
},
];
return built;
}
render() {
return (
<div>
<ReactTable
data={this.props.data}
columns={this.columnsBuilder(this.props.data)}
defaultPageSize={10}
className="-striped -highlight"
/>
<br />
</div>
);
}
}
You are making your API call on componentDidMount and sending the data to your component on render.
But render comes before componentDidMount in the component lifecycle.
That's why you need to verify if you have the data before passing it down. Like this:
...
render() {
return this.state.sprints ? (
<div>
<ResponseTable data={example} />
</div>
) : (
<div>
Loading ...
</div>
);
}
When you have this.state.sprints loaded, your component will update and a re-render will be triggered, showing the table properly.
export default class ResponseTable extends React.Component {
constructor(props) { // Use Props
super(props); // Use Props
this.columnsBuilder = this.columnsBuilder.bind(this);
}
columnsBuilder () { //Remove data
const props = Object.keys(this.props.data[0]); //Use Props
const columns = props.map( (item, index) => ({
Header : item,
accessor : item,
Cell : propss => propss.original[item].length === 0 ? '[]' : propss.original[item].toString(),
}));
const built = [
{
Header : 'Response',
columns,
},
];
return built;
}
render() {
return (
<div>
<ReactTable
data={this.props.data}
columns={this.columnsBuilder()} // Remove Props
defaultPageSize={10}
className="-striped -highlight"
/>
<br />
</div>
);
}
}
Please Do check propss use have used for Cell.
That's it easy!!!
You'll need to initialize the sprints state with a value if you want to use it in the render method, otherwise it's looking for a key that doesn't exist.
Your constructor could look something like this:
constructor(props){
super(props);
this.state = {
sprints: {}
};
}
Try set property name:
setState({sprints: myJson})
as it is missing in this line:
.then((myJson) => this.setState(myJson));
You are overriding the global state with
.then((myJson) => this.setState(myJson)) basicaly you are overriding this.state with new object which is not allowed you always need to only extend this.state as you are reffering in your code to this.state.sprints correct call would be
.then((sprints) => this.setState({sprints))
which leverages of using destructuring of objects so it will be same as {sprints: sprints React will then merge this.state (which is {} at that time from constructor) and merge it with {sprints: sprints} which will result to the correct state
this.state = {
sprints: data
}
also your code is asynchronous you need to add check inside render it can be easily done with returning null
if (!this.state.sprints) return null
which will guard the render method from errors
The issue that until your request is done - your state is empty thus there is no sprints property.
You should add it to your constructor in Table component.
Table component.
constructor(props){
super(props);
this.state = {
sprints: []
}
}
Also, you should check if there is items inside of your data in columnsBuilder method cause it can also produce an error.
ResponseTable component.
columnsBuilder (data) {
const props = data.length ? Object.keys(data[0]) : data;
const columns = props.map( (item, index) => ({
Header : item,
accessor : item,
Cell : propss => propss.original[item].length === 0 ? '[]' : propss.original[item].toString(),
}));
const built = [
{
Header : 'Response',
columns,
},
];
return built;
}
Another approach is to check in render method if sprints is exists in the state and use loader as mentioned Tiago Alves.

React nested object array inner object is somehow linked to all object elements

I've got a nested object that somehow retains a linkage to a profile object. Every time I call myMethod and makes changes to userObj, these changes are reflected in all elements of the nested object. For example, allProfiles['a'] and allProfiles['b'] have the same values for allProfiles[][].userObj properties. This only happens with the userObj data, everything is works as expected. The following snippet duplicates the issue.
import React from 'react';
import { ReactDOM, render } from 'react-dom';
export const userProfile = {
address1: { label: "Local", data: null },
address2: { label: "World", data: null },
};
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
profiles: {}
};
this.addProfile = this.addProfile.bind(this);
}
addProfile() {
const { counter, profiles } = this.state;
let new_user = Object.assign({}, userProfile);
new_user.address1.data = counter * 5;
const profile_id = 1;
const user_id = counter;
const new_profile = {};
const show = true;
new_profile[profile_id] = { show };
new_profile[profile_id] = new_user;
profiles[user_id] = new_profile;
this.setState({ counter: user_id + 1, profiles });
}
render() {
const profile_id = 1;
const ctr = this.state.counter;
return (
<div>
<div><button onClick={this.addProfile}>add profile</button></div>
<div>profile 0 data:
{ctr > 0 ? this.state.profiles[0][profile_id].address1.data : null}</div>
<div>profile {ctr} data:
{ctr > 0 ? this.state.profiles[ctr - 1][profile_id].address1.data : null}</div>
</div>
);
}
}
export default (Toggle);
render(<Toggle />, document.getElementById('root'));
Object.assign({}, userProfile) only creates a shallow copy of userProfile, meaning new_user.address1 and userProfile.address1 are still referencing the same object.
To properly clone the address1 and address2 objects, you need to do
const new_user = Object.entries(userProfile).reduce((acc, [key, value]) => {
acc[key] = { ...value }; // create a shallow copy of the nested object
return acc;
}, {});
This should fix your problem but there's a lot of weird things going on in your code, like assigning new_profile[profile_id] consecutively.

Categories

Resources