I've got the following handleChange function in my App component that is supposed to toggle todo items as completed or not completed. As a side note, I intend to update the code to use React Hooks in the future.
import React from "react"
import TodoItem from "./TodoItem"
import todosData from "./todosData"
import './styles.css'
class App extends React.Component {
constructor() {
super()
this.state = {
todos: todosData
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed;
}
return todo
})
return {
todos: updatedTodos
}
})
}
render() {
const todoItems = this.state.todos.map(item => <TodoItem key={item.id} item={item} handleChange={this.handleChange}/>)
return (
<div className="todo-list">
{todoItems}
</div>
)
}
}
The following line as it currently is doesn't update the completed status of the todo.
if (todo.id === id) {
todo.completed = !todo.completed;
}
However if I change it to something like the following, then it updates!?
if (todo.id === id) {
todo.completed = false;
}
However, I want it to toggle the completed status whether it's true or false and I can't see why the example I've used won't work as googling the problem seems to suggest it should.
Does anybody know why my code doesn't work currently? Any help is appreciated.
Related
I'm creating a todo app with reactjs and i'm trying to update my list of todo items when the user clicks on the input. When the user clicks I can change the item but the item does not get updated in state. I can't figure out why state is not updating. I think it has something to do with .map() and returning the todo object to the new array but I can't quite figure out what is going wrong. Any ideas?
App.js
import React from 'react'
import TodoItem from './components/TodoItem'
import TodosData from './TodosData'
import './App.scss'
class App extends React.Component {
constructor() {
super()
this.state = {
todoItems: TodosData
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
this.setState( prevState => {
const updatedTodos = prevState.todoItems.map( todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
return {
todoItems: updatedTodos
}
})
}
render() {
const todosComponents = this.state.todoItems.map( item => {
return <TodoItem key={item.id} todo={item} handleChange={this.handleChange}/>
})
return (
<div className="TodoList">
{todosComponents}
</div>
)
}
}
export default App;
TodosItem.js
import React from 'react'
function TodoItem(props) {
return (
<div className="TodoItem">
<input
type="checkbox"
checked={props.todo.completed}
onChange={ () => props.handleChange(props.todo.id) }
/>
<p>{props.todo.text}</p>
</div>
)
}
export default TodoItem
Issue
You are mutating your state objects. Since the nested object reference is the same React bails on rendering anything that may've updated. In other words, since the object reference is the same as the previous render React assumes nothing changed.
handleChange(id) {
this.setState( prevState => {
const updatedTodos = prevState.todoItems.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed // <-- object mutation!!
}
return todo
})
return {
todoItems: updatedTodos
}
})
}
Solution
Along with shallow copying the array you also need to shallow copy any nested state that you are updating.
handleChange(id) {
this.setState( prevState => {
const updatedTodos = prevState.todoItems.map(todo => {
if (todo.id === id) {
return {
...todo // <-- copy todo
completed: !todo.completed, // <-- update propety
}
}
return todo
})
return {
todoItems: updatedTodos
}
})
}
Context
The goal is to have a component with a key name being react-rendered in App.js when I press a specific key, registered in another component. The information is being passed thorugh a redux managed state.
The problem
It's simple :
I'm updating my state in my redux reducer but even when duplicating it (I can see it thanks to the redux dev tool that allows me to watch my prevState and my nextState being different)
And the question is as simple :
Why my App.js component won't re-render even after connecting to and
duplicating my state ?
I think I made sure that my state was duplicated with the spreading operation and my redux dev tool display me a good state update without having my prevState and nextState duplicated. I looked through a lot of posts and found only people that forgot to duplicate their state in their reducers, which I did not.
So what's the problem here ??
DevTool Sample
Code
Here is the code, quite simple. The interesting piece is playSound and playedKeys:
App.js :
import React from 'react'
import './App.css';
import { connect } from 'react-redux';
import KeyComponent from './Components/Key'
import SoundPlayer from './Components/Sounds'
const mapStateToProps = (state) => ({
...state.soundReducer
})
class App extends React.Component {
constructor(props) {
super(props);
}
render(){
return (
<div>
{console.log(this.props)}
{
this.props.playedKeys.map(key =>{
<KeyComponent keyCode={key}> </KeyComponent>
})
}
<SoundPlayer></SoundPlayer>
</div>
);
}
}
export default connect(mapStateToProps)(App);
Reducer
export default (state = {allSounds:{},playedKeys:[]}, action) => {
switch (action.type) {
case 'ADD_SOUND':
return reduce_addSound({...state},action)
case 'PLAY_SOUND':
return reduce_playSound({...state,playedKeys : [...state.playedKeys]},action)
default:
return state
}
}
function reduce_addSound (state,action){
let i = 0
state.allSounds[action.payload.key] = { players : new Array(5).fill('').map(()=>(new Audio())) , reader : new FileReader()}
//load audioFile in audio player
state.allSounds[action.payload.key].reader.onload = function(e) {
state.allSounds[action.payload.key].players.forEach(player =>{
player.setAttribute('src', e.target.result);
player.load();
player.id = 'test'+e.target.result+ i++
})
}
state.allSounds[action.payload.key].reader.readAsDataURL(action.payload.input.files[0]);
return state
}
function reduce_playSound(state,action){
state.playedKey = action.payload.key;
if(!state.playedKeys.includes(state.playedKey))
state.playedKeys.push(action.payload.key);
return state
}
Action
export const addSound = (key, input,player) => (dispatch,getState) => {
dispatch({
type: 'ADD_SOUND',
payload: {key : key, input : input}
})
}
export const playSound = (key) => (dispatch,getState) => {
dispatch({
type: 'PLAY_SOUND',
payload: {key : key}
})
}
The component registering the keypresses
import React from 'react'
import { connect } from 'react-redux';
import { playSound } from '../../Actions/soundActions';
const mapStateToProps = (state) => ({
...state.soundReducer
})
const mapDispatchToProps = dispatch => ({
playSound: (keyCode) => dispatch(playSound(keyCode))
})
class SoundPlayer extends React.Component {
constructor(props) {
super(props);
}
componentDidMount () {
this.playSoundComponent = this.playSoundComponent.bind(this)
document.body.addEventListener('keypress', this.playSoundComponent);
}
keyCodePlayingIndex = {};
playSoundComponent(key){
if(this.props.allSounds.hasOwnProperty(key.code)){
if(!this.keyCodePlayingIndex.hasOwnProperty(key.code))
this.keyCodePlayingIndex[key.code] = 0
this.props.allSounds[key.code].players[this.keyCodePlayingIndex[key.code]].play()
this.keyCodePlayingIndex[key.code] = this.keyCodePlayingIndex[key.code] + 1 >= this.props.allSounds[key.code].players.length ? 0 : this.keyCodePlayingIndex[key.code] + 1
console.log(this.keyCodePlayingIndex[key.code])
}
this.props.playSound(key.code);
}
render(){
return <div>
<h1 >Played : {this.props.playedKey}</h1>
{Object.keys(this.keyCodePlayingIndex).map(key =>{
return <p>{key} : {this.keyCodePlayingIndex[key]}</p>
})}
</div>
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SoundPlayer);
Issue
You are mutating your state object.
state.allSounds[action.payload.key] = ...
state.playedKey = action.payload.key;
Solution
Update your reducer functions to return new state objects, remembering to correctly shallow copy each level of depth that is being updated.
export default (state = { allSounds: {}, playedKeys: [] }, action) => {
switch (action.type) {
case 'ADD_SOUND':
return reduce_addSound({ ...state },action);
case 'PLAY_SOUND':
return reduce_playSound({ ...state, playedKeys: [...state.playedKeys] }, action);
default:
return state
}
}
function reduce_addSound (state, action) {
const newState = {
...state, // shallow copy existing state
allSounds: {
...state.allSounds, // shallow copy existing allSounds
[action.payload.key]: {
players: new Array(5).fill('').map(()=>(new Audio())),
reader: new FileReader(),
},
}
};
// load audioFile in audio player
newState.allSounds[action.payload.key].reader.onload = function(e) {
newState.allSounds[action.payload.key].players.forEach((player, i) => {
player.setAttribute('src', e.target.result);
player.load();
player.id = 'test' + e.target.result + i // <-- use index from forEach loop
})
}
newState.allSounds[action.payload.key]
.reader
.readAsDataURL(action.payload.input.files[0]);
return newState;
}
function reduce_playSound (state, action) {
const newState = {
...state,
playedKey: action.payload.key,
};
if(!newState.playedKeys.includes(newState.playedKey))
newState.playedKeys = [...newState.playedKeys, action.payload.key];
return newState
}
Okay I've got it, it's always the simplest stupidest thing that we don't check huh.
Clarification
So my state was properly duplicated with reduce_addSound({ ...state },action) and reduce_playSound({ ...state, playedKeys: [...state.playedKeys] and like I wrote in my question, that wasn't the issue !
Issue
As old as it can get, I wasn't returning a component in my render function.. :
in App.js :
render(){
return (
<div>
{
this.props.soundReducer.playedKeys.map(key =>{
<KeyComponent keyCode={key}> </KeyComponent> //<-- NO return or parenthesis !!
})
}
<SoundPlayer></SoundPlayer>
</div>
);
}
Answer
App.js render function with parenthesis:
render(){
return (
<div>
{
this.props.soundReducer.playedKeys.map(key =>(
<KeyComponent key = {key} keyCode={key}> </KeyComponent> //<-- Here a component is returned..
))
}
<SoundPlayer></SoundPlayer>
</div>
);
}
I use MobX to control my ReactJS state/components and I also use an async call through MobX in order to retrieve the data, this is typically called in my header throuhg componentDidMount().
(I already know my code isn't the cleanest and most likely has errors, cut me some slack as I'm learning/coding this completely on my own at this point with no educational background in programming)
import React from 'react';
import { Layout, Row, Col, Menu, Avatar, Tag } from 'antd';
import { inject, observer } from 'mobx-react';
import Icon from '#ant-design/icons';
const { Header } = Layout;
const { SubMenu } = Menu;
#inject('store')
#observer
class PageHeader extends React.Component {
componentDidMount() {
this.props.store.getOrders();
}
Say for instance I was not in the Application, but I was still logged in through my LocalStorage data, and I went to a page "http://localhost/orders/123456". 123456 would be my order ID and this page would display it's details. Now considering I was not on the page, the DOM wasn't rendered, right? Right... But I'm still logged in through my LocalStorage, so when I visit the page - it's rendering blank because MobX has to wait for the API call to retrieve the data. I need to be able to pull this data and make sure it's rendered on the page, so I some how need the API to be retrieve before rendering to ensure it's pull the data out of MobX with the specified OrderID, in this case 123456.
Below is two ways I've made my componentDidMount
#inject('store')
#observer
class LoadPage extends React.Component {
state = {
visible: false,
ordernum: this.props.match.params.id,
orderkey: null,
}
componentDidMount() {
document.title = this.props.match.params.id;
route_ordernum = this.props.match.params.id;
if (this.props.store.orders.length === 0) {
fetch('http://localhost:5000')
.then(res1 => res1.json())
.then(data => this.props.store.setOrders(data))
.then(this.setState({
orderkey: this.props.store.orders.filter(order => order._id.includes(route_ordernum)).map((data, key) => { return data.key })
}))
}
if (this.props.store.orders.length > 0) {
this.setState({
orderkey: this.props.store.orders.filter(order => order._id.includes(route_ordernum)).map((data, key) => { return data.key })
})
}
console.log(this.state.orderkey)
}
render() {
Example #2
componentDidMount() {
document.title = this.props.match.params.id;
route_ordernum = this.props.match.params.id;
if (this.props.store.orders.length === 0) {
this.props.store.getOrders().then(dataloads => {
this.setState({
orderkey: this.props.store.orders.filter(order => order._id.includes(route_ordernum)).map((data, key) => { return data.key })
})
})
}
if (this.props.store.orders.length > 0) {
this.setState({
orderkey: this.props.store.orders.filter(order => order._id.includes(route_ordernum)).map((data, key) => { return data.key })
})
}
console.log(this.state.orderkey)
}
I'm just passing through my MobX and using two separate classes to create my state now.
#inject('store')
#observer
class LoadMain extends React.Component {
render() {
return(
this.props.store.orders.length === 0 ? <Content style={{ backgroundColor: "#ffffff" }}><center><Spinner /></center></Content> : <OrderPage ordernum={this.props.match.params.id} />
);
}
}
I'm following a React beginners tutorial making a todo app as an example.
in the App.js, there is a handleChange method that will update the state whether the checkbox is checked or not, and passes it into the TodoItem component
class App extends React.Component {
constructor() {
super()
this.state = {
todos: todosData
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
console.log(id)
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
return {
todos: updatedTodos
}
})
}
render() {
const todoItems = this.state.todos.map(item => <TodoItem key={item.id} item={item} handleChange={this.handleChange}/>)
return (
<div className="todo-list">
{todoItems}
</div>
)
}
}
export default App
TodoItem.js
function TodoItem(props) {
return (
<div className="todo-item">
<input
type="checkbox"
checked={props.item.completed}
onChange={() => props.handleChange(props.item.id)}
/>
<p>{props.item.text}</p>
</div>
)
}
export default TodoItem
it successfully displays the list and the console log correctly displays the checkbox clicked, however, the checkbox does not change. Can anyone tell me the problem?
I think you are running into a state mutation problem which is causing some unexpected behavior. The reason for this is because inside your if statement within map you are not returning your modified array item and you are actually modifying your state array and your new array.
How to fix: return your modified array item inside the if statement
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed
}
}
return todo
})
return {
todos: updatedTodos
}
})
Or use a one liner with conditional (ternary) operator:
this.setState(prevState => ({
...prevState,
todos: prevState.todos.map(todo => todo.id === id ? { ...todo, ...{ completed: !todo.completed } } : todo)
}))
Look at this example I created in playground to get a better understanding:
I defined two arrays, one array uses map() without the return and the other array is using map() with the return.
Our goal is to keep our two arrays exactly the same and using map() to create a new modified array. Look at the log results and notice how our initial array gets modified aswell. Our second todo item in this array should have a completed value of true but it gets changed to false after our map() which we would want to avoid. By returning our array item in the correct way we avoid this.
To get a better understanding of what state mutation is and how to avoid check this.
import React, { Component } from 'react';
import {withProvider} from './TProvider'
import ThreeCardMap from './ThreeCardMap';
class Threecard extends Component {
constructor() {
super();
this.state = {
newlist: []
}
}
componentDidMount(){
this.props.getList()
this.setState({newlist: [this.props.list]})
}
// componentDidUpdate() {
// console.log(this.state.newlist);
// }
render() {
const MappedTarot = (this.state.newlist.map((list, i) => <ThreeCardMap key={i} name={list.name} meaningup={list.meaning_up} meaningdown={list.meaning_rev}/>);
return (
<div>
<h1>Three Card Reading</h1>
<div>{ MappedTarot }</div>
</div>
)
}
}
export default withProvider(Threecard);
Hi, I'm trying to create a page that takes data from a tarot card API (https://rws-cards-api.herokuapp.com/api/v1/cards/search?type=major). Unfortunately by the time the data comes in, my map function has already fired. I'm asking to see if there is a way to have the map function wait until the data hits before it fires. Thanks!
Edit: getList function in the Context:
getList = () => {
console.log('fired')
axios.get('https://vschool-cors.herokuapp.com?url=https://rws-cards-api.herokuapp.com/api/v1/cards/search?type=major').then(response =>{
this.setState({
list: response.data
})
}).catch(error => {
console.log(error);
})
}
this.props.getList() is an async function. You are setting the list right after that call which is not correct.
You need to set it in the getList promise then() block.
getList() is an async function and update data for the parent component. So, my solution is just watching the list from the parent component if they updated or not, through getDerivedStateFromProps
class Threecard extends Component {
constructor() {
super();
this.state = {
newlist: []
}
}
// Set props.list to this.state.newList and watch the change to update
static getDerivedStateFromProps(nextProps, prevState) {
return {
newlist: nextProps.list
}
}
componentDidMount(){
this.props.getList()
// Removed this.setState() from here.
}
render() {
const MappedTarot = (this.state.newlist.map((list, i) => <ThreeCardMap key={i} name={list.name} meaningup={list.meaning_up} meaningdown={list.meaning_rev}/>);
return (
<div>
<h1>Three Card Reading</h1>
<div>{ MappedTarot }</div>
</div>
)
}
}
export default withProvider(Threecard);