Array.splice works on both state sub-arrays - javascript

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.

Related

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);
});
}

Filtering an Array within an Array in React

import React, { Component } from "react"
import {
StaticQuery,
grahpql,
Link
} from "gatsby"
import {
StyledFilter,
StyledLine
} from "./styled"
class Filter extends Component {
render() {
const { data } = this.props
const categories = data.allPrismicProjectCategory.edges.map((cat, index) => {
return (
<a
key={index}
onClick={() => this.props.setFilterValue(cat.node.uid)}
>
{cat.node.data.category.text}
</a>
)
})
return (
<StyledFilter>
<div>
Filter by<StyledLine />
<a
// onClick={() => {this.props.filterProjects("all")}}
>
All
</a>
{categories}
</div>
<a onClick={this.props.changeGridStyle}>{this.props.gridStyleText}</a>
</StyledFilter>
)
}
}
export default props => (
<StaticQuery
query={graphql`
query {
allPrismicProjectCategory {
edges {
node {
uid
data {
category {
text
}
}
}
}
}
}
`}
render={data => <Filter data={data} {...props} />}
/>
)
<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>
I am working on a React App with Gatsby and Prismic that has a project page. By default it lists all projects but at the page's top appears a filter to select by category (just a bunch of <a> tags).
My Page consists of a <Filter /> component as well as several <GridItem /> components I am mapping over and load some props from the CMS.
The part I am struggling with is the filtering by category.
When my page component mounts it adds all projects into my filteredItems state.
When a user is clicking on a filter at the top it set's my default filterValue state from "all" to the according value.
After that I'll first need to map over the array of projects and within that array I'll need to map over the categories (each project can belong to multiple categories).
My idea is basically if a value (the uid) matches my new this.state.filterValue it returns the object and add's it to my filteredItems state (and of course delete the one's not matching this criteria).
This is what my page component looks like (cleaned up for better readability, full code in the snippet at the bottom):
class WorkPage extends Component {
constructor(props) {
super(props)
this.state = {
filterValue: "all",
filteredItems: []
}
this.filterProjects = this.filterProjects.bind(this)
}
filterProjects = (filterValue) => {
this.setState({ filterValue: filterValue }, () =>
console.log(this.state.filterValue)
)
// see a few of my approaches below
}
componentDidMount() {
this.setState({
filteredItems: this.props.data.prismicWork.data.projects
})
}
render() {
const projectItems = this.props.data.prismicWork.data.projects && this.props.data.prismicWork.data.projects.map((node, index) => {
const item = node.project_item.document["0"].data
const categories = node.project_item.document["0"].data.categories.map(cat => {
return cat.category_tag.document["0"].uid
})
return (
<GridItem
key={index}
categories={categories}
moreContentProps={moreContentProps}
/>
)
})
return (
<LayoutDefault>
<Filter
filterProjects={this.filterProjects}
/>
{projectItems}
</LayoutDefault>
)
}
}
I tried so many things, I can't list all of them, but here are some examples:
This approach always returns an array of 10 objects (I have 10 projects), sometimes the one's that don't match the this.state.filterValue are empty objects, sometimes they still return their whole data.
let result = this.state.filteredItems.map(item => {
return item.project_item.document["0"].data.categories.filter(cat => cat.category_tag.document["0"].uid === this.state.filterValue)
})
console.log(result)
After that I tried to filter directly on the parent item (if that makes sense) and make use of indexOf, but this always console logged an empty array...
let result = this.state.filteredItems.filter(item => {
return (item.project_item.document["0"].data.categories.indexOf(this.state.filterValue) >= 0)
})
console.log(result)
Another approach was this (naive) way to map over first the projects and then the categories to find a matching value. This returns an array of undefined objects.
let result = this.state.filteredItems.map(item => {
item = item.project_item.document["0"].data.categories.map(attachedCat => {
if (attachedCat.category_tag.document["0"].uid === this.state.filterValue) {
console.log(item)
}
})
})
console.log(result)
Other than that I am not even sure if my approach (having a filteredItems state that updates based on if a filter matches the according category) is a good or "right" React way.
Pretty stuck to be honest, any hints or help really appreciated.
import React, { Component } from "react"
import { graphql } from "gatsby"
import LayoutDefault from "../layouts/default"
import { ThemeProvider } from "styled-components"
import Hero from "../components/hero/index"
import GridWork from "../components/grid-work/index"
import GridItem from "../components/grid-item/index"
import Filter from "../components/filter/index"
class WorkPage extends Component {
constructor(props) {
super(props)
this.state = {
filterValue: "all",
filteredItems: [],
isOnWorkPage: true,
showAsEqualGrid: false
}
this.filterProjects = this.filterProjects.bind(this)
this.changeGridStyle = this.changeGridStyle.bind(this)
}
changeGridStyle = (showAsEqualGrid) => {
this.setState(prevState => ({
showAsEqualGrid: !prevState.showAsEqualGrid,
isOnWorkPage: !prevState.isOnWorkPage
}))
}
filterProjects = (filterValue) => {
this.setState({ filterValue: filterValue }, () =>
console.log(this.state.filterValue)
)
let result = this.state.filteredItems.filter(item => {
return (item.project_item.document["0"].data.categories.toString().indexOf(this.state.filterValue) >= 0)
})
console.log(result)
}
componentDidMount() {
this.setState({
filteredItems: this.props.data.prismicWork.data.projects
})
}
render() {
const projectItems = this.props.data.prismicWork.data.projects && this.props.data.prismicWork.data.projects.map((node, index) => {
const item = node.project_item.document["0"].data
const categories = node.project_item.document["0"].data.categories.map(cat => {
return cat.category_tag.document["0"].uid
})
return (
<GridItem
key={index}
isSelected="false"
isOnWorkPage={this.state.isOnWorkPage}
isEqualGrid={this.state.showAsEqualGrid}
projectURL={`/work/${node.project_item.uid}`}
client={item.client.text}
tagline={item.teaser_tagline.text}
categories={categories}
imageURL={item.teaser_image.squarelarge.url}
imageAlt={item.teaser_image.alt}
/>
)
})
return (
<ThemeProvider theme={{ mode: "light" }}>
<LayoutDefault>
<Hero
introline="Projects"
headline="Art direction results in strong brand narratives and compelling content."
/>
{/* {filteredResult} */}
<Filter
filterProjects={this.filterProjects}
changeGridStyle={this.changeGridStyle}
gridStyleText={this.state.showAsEqualGrid ? "Show Flow" : "Show Grid"}
/>
<GridWork>
{projectItems}
</GridWork>
</LayoutDefault>
</ThemeProvider>
)
}
}
export default WorkPage
export const workQuery = graphql`
query Work {
prismicWork {
data {
page_title {
text
}
# All linked projects
projects {
project_item {
uid
# Linked Content
document {
type
data {
client {
text
}
teaser_tagline {
text
}
teaser_image {
url
alt
xlarge {
url
}
large {
url
}
medium {
url
}
squarelarge {
url
}
squaremedium {
url
}
squaresmall {
url
}
}
categories {
category_tag {
document {
uid
data {
category {
text
}
}
}
}
}
}
}
}
}
}
}
}
`
<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>
So there are at least two things.
In your filterProjects() you're first setting state.filterValue and then you use it in filteredItems.filter(). That might not work, because React does not execute setState() immediately always, to optimize performance. So you're probably filtering against the previous value of state.filterValue. Instead just use filterValue, which you pass into filterProjects().
setFilterValue = (filterValue) => {
this.setState({filterValue}) // if key and variable are named identically, you can just pass it into setState like that
}
// arrow function without curly braces returns without return statement
filterProjects = (projects, filterValue) =>
projects.filter(item => item.project_item.document[0].data.categories.toString().includes(filterValue))
You should return the result from filterProjects(), because you need to render based on the filteredItems then, of course. But actually it's not necessary to put the filter result into state. You can apply the filterProjects() on the props directly, right within the render(). That's why you should return them. Also separate setState into another function which you can pass into your <Filter/> component.
And a recommendation: Use destructuring to make your code more readable. For you and anyone else working with it.
render() {
const { projects } = this.props.data.prismicWork.data // this is
const { filterValue } = this.state // destructuring
if (projects != undefined) {
this.filterProjects(projects, filterValue).map((node, index) => {
// ...
// Filter component
<Filter filterProjects={this.setFilterValue} />
That way you trigger a rerender by setting the filterValue, because it
resides in this.state, and the render function depends on
this.state.filterValue.
Please try that out and tell me if there is another problem.

React. Delete specific row from the table

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>

How to delete a specific element from an component array?

When my code executes to remove an element, the last array element gets deleted, not the specific index I am referring to in the code.
Any assistance in reviewing this would be helpful.
constructor(props) {
super(props);
this.state = {
rows: []
}
this.addHandler = this.addHandler.bind(this);
this.removeHandler = this.removeHandler.bind(this);
}
addHandler(event){
const rows = this.state.rows.concat(<Component>);
this.setState({
rows
})
}
removeHandler(id){
const index = id;
this.setState({
row: this.state.row.filter( (x, i) => i !== index)
})
}
render () {
const rows = this.state.rows.map((Element, index) => {
return <Element key={index} id={index} index={index} func1=
{this.addHandler} func2={this.removeHandler} />
});
// func2 gets called by child
return (
<div className="rows">
<button onClick={this.addHandler} >+</button>
{rows}
</div>
);
}
Don't use index as a key for mapping out an array. Check out this package, easy to use and get the job done uuid

How to search data efficiently with React Native ListView

I am trying to implement a filter and search function that would allow user to type in keyword and return result(array) and re-render the row
This is the event arrays that being passed in into the createDataSource function
The problem I am having now is my search function can't perform filter and will return the entire parent object although I specifically return the indexed object.
Here's what I got so far
class Search extends Component {
state = { isRefreshing: false, searchText: '' }
componentWillMount() {
this.createDataSource(this.props);
}
componentWillReceiveProps(nextProps) {
this.createDataSource(nextProps);
if (nextProps) {
this.setState({ isRefreshing: false })
}
}
createDataSource({ events }) {
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.dataSource = ds.cloneWithRows(events);
}
//return arrays of event from events
renderRow(event) {
return <EventItem event={event} />;
}
onRefresh = () => {
this.setState({ isRefreshing: true });
this.props.pullEventData()
}
setSearchText(event) {
let searchText = event.nativeEvent.text;
this.setState({ searchText })
var eventLength = this.props.events.length
var events = this.props.events
const filteredEvents = this.props.events.filter(search)
console.log(filteredEvents);
function search() {
for (var i = 0; i < eventLength; i++) {
if (events[i].title === searchText) {
console.log(events[i].title)
return events[i];
}
}
}
}
render() {
const { skeleton, centerEverything, container, listViewContainer, makeItTop,
textContainer, titleContainer, descContainer, title, desc, listContainer } = styles;
return(
<View style={[container, centerEverything]}>
<TextInput
style={styles.searchBar}
value={this.state.searchText}
onChange={this.setSearchText.bind(this)}
placeholder="Search" />
<ListView
contentContainerStyle={styles.listViewContainer}
enableEmptySections
dataSource={this.dataSource}
renderRow={this.renderRow}
refreshControl={
<RefreshControl
refreshing={this.state.isRefreshing}
onRefresh={this.onRefresh}
title="Loading data..."
progressBackgroundColor="#ffff00"
/>
}
/>
</View>
)
}
}
As you can see from the image above, my code requires me to type in the full query text to display the result. And it displays all the seven array objects? why's that?
The syntax of Array.prototype.filter is wrong... it should take a callback that will be the item being evaluated for filtering.. if you return true it will keep it.
function search(event) {
return ~event.title.indexOf(searchText)
}
You could even make the inline like..
const filteredEvents = this.props.events.filter(event => ~event.title.indexOf(searchText))
For understanding my use of ~, read The Great Mystery of the Tilde.
Since filter returns a new array, you should be able to clone your dataSource with it. If you didn't use filter, you would have to call events.slice() to return a new array. Otherwise, the ListView doesn't pickup the changes.

Categories

Resources