React map and state confusion


#1

I’m new to React. I’m building a simple app that will reorder a list depending on the number of votes each item gets.

I’m keeping all the information about all the post in state in the Posts component:

		state =  {
			posts: [
		{
			link: "css-tricks.com",
			description: "the best css site around!",
			rating: 0
		},
		{
			link: "udacity.com",
			description: "great learning resource!",
			rating: 10
		},
                {
			link: "alistapart.com.",
			description: "A high-quality resource for web design and frontend development",
			rating: 10
		}
	]
}

Also in the Posts component I have this render function

render() {
	var sortedposts = this.state.posts.sort((a, b) => {return a.votes - b.votes})
	var posts = sortedposts.map((post) => {
		return <Post increment={this.increment} decrement={this.decrement} rating={post.rating} link={post.link} description={post.description}/>
	})
	return(
		<div>
			{posts}
		</div>
	)
}

To allow people to vote I need an increment and decrement function.

If I was defining the rating state inside each individual Post component I could do something like:

increment = () => {
	this.setState({
		rating: this.state.rating + 1
	})
}

However, these functions need to be defined inside the Posts component because they are changing state of the Posts component. How do I update the state of the right post?


#2

You can either pass in the index directly (BTW don’t forget the key prop!):

const posts = this.state.posts.map((post, index) => {
  return <Post increment={() => this.increment(index)} key={index} />
})

… or forward the index to the Post component, so that it can for example set it as a data-* attribute on a native element; thus avoiding inline function definitions inside the render method:

const posts = this.state.posts.map((post, index) => {
  return <Post increment={this.increment} key={index} index={index} />
})

On another note, you shouldn’t .sort() the posts inside the render method as this mutates the state. Instead, you should always use setState() and do so somewhere else (e.g. inside the constructor and then after having incremented a rating).


#3

Thanks for the advice @m3g4p0p

I’ve tried what you suggested. I can’t get it to work

increment = (index) => {
	this.setState({
		votes: this.state.posts[index].votes + 1,
		posts: this.state.posts.sort((a, b) => {return a.votes - b.votes})
	})
}

When I sort the posts inside the constructor I get the error message “Warning: Can’t call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to this.state directly or define a state = {}; class property with the desired state in the Posts component.” inside the browser devtools console.


#4

I think votes should be a property of the posts, not another state property next to posts… here’s how you’d do this without mutating any data (using the spread syntax):

// Create a copy of the posts
const posts = [...this.state.posts]

// Don't mutate the target post either, but replace it
// with a copy with an updated votes property
const current = posts[index]
posts[index] = { ...current, votes: current.votes + 1 }

// Now the posts can be sorted without worries
posts.sort((a, b) => a.votes - b.votes)
this.setState({ posts })

Yes, sorry… the constructor is indeed the only place where you can (and must) set the state directly.


#5

Feels slightly convoluted. As a newbie for React I thought it would make things a bit easier. That all worked though so thanks! :smile:


#6

Maybe I should explain a bit. Technically, this would work too:

const { posts } = this.state

posts[index].votes++

this.setState({
  posts: posts.sort((a, b) => a.votes - b.votes)
})

By calling setState(), the component will re-render either way; however, if you were to implement shouldComponentUpdate() (or using a pure component), you couldn’t reliably compare the current with the next state as you already mutated the current state. For example, this would always return false:

shouldComponentUpdate (nextProps, nextState) {
  return this.state.posts.length !== nextState.posts.length ||
    this.state.posts.some((post, index) => {
      return nextState.posts[index].votes !== post.votes
    })
}

To avoid such gotchas, you simply shouldn’t ever mutate the state directly… and BTW this is a good practice when writing vanilla JS as well. :-)


#7

Thanks @m3g4p0p
Where does prevState fit into all this?


#8

Why where are you using prevState?


#9

prevState seems weirdly absent from the React docs. Isn’t it another way to reference state without mutating state?


#10

No, but because the state gets updated asynchronously you can’t reliably compute the next state from the current state; that’s why there’s a callback version of setState(), which takes the previous state as its first argument:

// To use the classic example...
this.setState(prevState => ({
  counter: prevState.counter + 1
}))

In this case, you can be sure that prevState holds the state from when you called setState(), regardless of any updates that may have happened in the meanwhile. (This will not protect you against direct mutations of the state though!)