setState not updating array - javascript

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

Related

Todo won't toggle as completed/not completed in React app

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.

React Component not updating even after duplication of state in Redux reducer

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

React setState not updating checkbox checked state

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.

React component does not re-render when updating state from Context

I am trying to rerender a component in React.
The setup:
I am using React Context to fetch some data from a Firestore database. So the fetching is Async.
My component is then fetching the data using: static contextType = MyContext and accessing via this.context
I store this context data on the components state to try to trigger a rerender whenever this data is changed.
I pass the data to a child component where it renders a list based on this data.
The problem:
I manage to update the state and even when debugging I can see my state updating to the correct data BUT the component does not rerender either the childcomponent or the list.
The expected list shows as soon as I click anything on the page so my guess is that the data is trapped in some kind of middle stage.
What I've tried:
I tried using the componentDidUpdate to make a check if the context is different than the current state and trigger a function that sets the state (I have even tried with setState function directly after the check) => Still state updates but no rerender is triggered (I can see the new data on state)
I tried using the getDerivedStateFromProps on the child component to do a check if the Props have changed and also tried storing the props in the child components own state => Still same result as before.
I am not sure what else to try, I thought that React triggers a rerender everytime state chages but probably I am doing something really wrong.
Here is my parent:
import React, { Component } from 'react';
import styles from './artistdropdown.module.css';
import { returnCollection } from '../../utils/Firebase.js';
import MyContext from '../../utils/MyContext.js';
import ArtistSelected from './ArtistSelected.js';
import ArtistsList from './ArtistsList';
export class ArtistDropdown extends Component {
static contextType = MyContext;
constructor(props) {
super(props);
this.state = {
artists: [],
currentArtist: {
id: null,
name: null
}
};
this.fetchArtist = (aId, artists) => {
const state = {
id: null,
name: null,
};
artists && artists.forEach((a) => {
if (a.id === aId) {
state = {
...state,
id: a.id,
name: a.name,
}
}
})
return state;
}
this.loadToState = (state) => {
this.setState({
...this.state,
...state,
})
}
this.click = (id) => {
this.context.handleArtistSelection(id);
this.props.handleBandDropdown();
}
}
componentDidMount() {
const aId = this.context.state.user.last_artist;
const artists = this.context.state.user.artists;
const currentArtist = this.fetchArtist(aId, artists);
const state = {
artists: artists,
currentArtist: currentArtist,
}
this.loadToState(state);
}
componentDidUpdate(props, state) {
if (this.state.artists !== this.context.state.user.artists) {
const aId = this.context.state.user.last_artist;
const artists = this.context.state.user.artists;
const currentArtist = this.fetchArtist(aId, artists);
const state = {
artists: artists,
currentArtist: currentArtist,
}
this.loadToState(state);
}
}
render() {
const bandDropdown = this.props.bandDropdown;
return (
<>
<ArtistSelected
currentBand={this.state.currentArtist.name}
handleDropdown={this.props.handleBandDropdown}
expanded={bandDropdown}
/>
<ul className={bandDropdown ? styles.band_options + ' ' + styles.expanded : styles.band_options}>
<ArtistsList artists={this.state.artists} />
</ul>
</>
)
}
}
export default ArtistDropdown
and here is my child:
import React, { Component } from 'react';
import MyContext from '../../utils/MyContext.js';
import ArtistItem from './ArtistItem.js';
export class ArtistsList extends Component {
static contextType = MyContext;
constructor(props) {
super(props);
this.state = {
artists: [],
};
this.loadToState = (state) => {
this.setState({
...this.state,
...state,
}, () => { console.log(this.state) })
}
}
componentDidMount() {
const artists = this.props.artists;
const state = {
artists: artists,
}
this.loadToState(state);
}
componentDidUpdate(props, state) {
if (state.artists !== this.state.artists) {
this.loadToState(state);
}
}
static getDerivedStateFromProps(props, state) {
if (props.artists !== state.artists) {
return {
artists: props.artists,
}
} else {
return null;
}
}
render() {
// const artistList = this.state.artists;
const artistList = this.state.artists;
const list = artistList && artistList.map((a) => {
return (<ArtistItem key={a.id} onClick={() => this.click(a.id)} name={a.name} />)
})
return (
<>
{list}
</>
)
}
}
export default ArtistsList

how to save react js state into localstorage

I have no idea How to store the react js state into localstorage.
import React, { Component } from 'react'
import './App.css';
import { auth,createUserProfileDocument } from './firebase/firebase.utils'
import { TodoForm } from './components/TodoForm/TodoForm.component'
import {TodoList} from './components/TodoList/TodoList.component'
import {Footer} from './components/footer/footer.component'
import Header from '../src/components/header/header.component'
import {Redirect} from 'react-router-dom'
import {connect} from 'react-redux'
import {setCurrentUser} from './redux/user/user.actions'
export class App extends Component {
constructor(props) {
super(props)
this.input=React.createRef()
this.state = {
todos:[
{id:0, content:'Welcome Sir!',isCompleted:null},
]
}
}
todoDelete = (id) =>{
const todos = this.state.todos.filter(todo => {
return todo.id !== id
})
this.setState({
todos
})
}
toDoComplete = (id,isCompleted) =>{
console.log(isCompleted)
var todos = [...this.state.todos];
var index = todos.findIndex(obj => obj.id === id);
todos[index].isCompleted = !isCompleted;
this.setState({todos});
console.log(isCompleted)
}
addTODO = (todo) =>{
todo.id = Math.random()
todo.isCompleted = true
let todos = [...this.state.todos, todo]
this.setState({
todos
})
}
unsubscribeFromAuth = null;
componentDidMount() {
const { setCurrentUser } = this.props;
this.unsubscribeFromAuth = auth.onAuthStateChanged(async userAuth => {
if (userAuth) {
const userRef = await createUserProfileDocument(userAuth);
userRef.onSnapshot(snapShot => {
setCurrentUser({
id: snapShot.id,
...snapShot.data()
});
});
}
setCurrentUser(userAuth);
});
}
componentWillUnmount() {
this.unsubscribeFromAuth();
}
render() {
return (
<div className='App'>
<Header />
<TodoForm addTODO={this.addTODO} />
<TodoList
todos={this.state.todos}
todoDelete={ this.todoDelete}
toDoComplete={ this.toDoComplete}
/>
<Footer/>
</div>
)
}
}
const mapStateToProps = ({ user }) => ({
currentUser: user.currentUser
});
const mapDispatchToProps = dispatch => ({
setCurrentUser: user => dispatch(setCurrentUser(user))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
in my input Form
import './TodoForm.style.css'
export class TodoForm extends Component {
constructor(props) {
super(props)
this.state = {
content : ''
}
}
handleChange = (e) =>{
this.setState({
content: e.target.value
})
}
handleSubmit =(e) =>{
e.preventDefault();
this.props.addTODO(this.state);
this.setState({
content: ''
})
}
render() {
return (
<div className='inputTask'>
<form onSubmit={ this.handleSubmit}>
<input
className="textBox"
type='text'
onChange={ this.handleChange}
value={this.state.content}
placeholder='what you want to do ...'
/>
</form>
</div>
)
}
}
export default TodoForm
I have no idea How to store the react js state into localstorage.
i searched on internet but unable to find the exact solution all the codes that i think is necessary post.
You can use reactLocalStorage to save any data in local storage
import {reactLocalStorage} from 'reactjs-localstorage';
reactLocalStorage.set('var', true);
reactLocalStorage.get('var', true);
reactLocalStorage.setObject('var', {'test': 'test'});
reactLocalStorage.getObject('var');
reactLocalStorage.remove('var');
reactLocalStorage.clear();
Read out the localStorage item in the componentDidMount callback. Simply read the item you want to get, check if it exists and parse it to a usable object, array or datatype that need. Then set the state with the results gotten from the storage.
And to store it, simply handle it in an event handler or helper method to update both the state and the localStorage item.
class ExampleComponent extends Component {
constructor() {
super();
this.state = {
something: {
foo: 'bar'
}
}
}
componentDidMount() {
const storedState = localStorage.getItem('state');
if (storedState !== null) {
const parsedState = JSON.parse(storedState);
this.setState({ something: parsedState });
}
}
clickHandler = (event) => {
const value = event.target.value;
const stringifiedValue = JSON.stringify(value);
localStorage.setItem('state', stringifiedValue);
this.setState({ something: value });
}
render() {
return (
<button onClick={clickHandler} value={this.state.something}>Click me</button>
);
}
}
Set data in localStorage
key-value pair :
localStorage.setItem('key_name',"value");
object
localStorage.setItem('key_name', JSON.stringify(object));
Remove data from localStorage
localStorage.removeItem('key_name');
Get data from localStorage
let data = localStorage.getItem('key_name');
object :
let data = JSON.parse(localStorage.getItem('key_name'));
clear localStorage (delete all data)
localStorage.clear();

Categories

Resources