This is quite peculiar because in my reducers, I made sure I was not mutating state; A common issue with this particular problem. However, I still keep getting this issue. On the initial load of the application (using npm start). In the photo below you can see that I console.log every component right before the return statement as a test to see if the components render. But despite state being updated, the component never re-renders.... (I'm confident the containers are set up properly and the components are called.)
AllGiftsDisplay
import OneGiftDisplay from './OneGiftDisplay.jsx';
const AllGiftsDisplay = (props) => {
console.log("LOADING");
let individualGifts = [];
for(let i = 0; i < props.giftList.length; i++) {
individualGifts.push(
<OneGiftDisplay
addGift = {props.addGift}
updatedGiftMessage = {props.updateGiftMessage}
setNewMessage = {props.setNewMessage}
totalVotes = {props.totalVotes}
/>
)
}
// let list = [<OneGiftDisplay/>, <OneGiftDisplay/>]
return (
<div className = "displayAllGifts">
{/* {console.log("~~~giftlist length", props.giftList.length)} */}
{individualGifts}
{/* {list} */}
</div>
)
};
export default AllGiftsDisplay;
Gift Reducers
import * as types from '../constants/actionTypes.js';
const initialState = {
giftList: [],
lastGiftId: 10000,
totalVotes: 0,
newMessage: ''
};
const giftReducer = (state=initialState, action) => {
// let giftList;
// let setMessage;
switch(action.type) {
case types.ADD_GIFT:
let stateCopy = { ...state }; // shallow copy
// create the new gift object structure.
const giftStructure = {
// lastGiftId: stateCopy.lastGiftId,
newMessage: stateCopy.newMessage,
totalVotes: 0
};
// push the new gift onto the list.
stateCopy.giftList.push(giftStructure);
// console.log("giftList: ", stateCopy.giftList);s
// return updated state
return {
...stateCopy,
newMessage: ''
}
case types.SET_MESSAGE:
return {
...state, newMessage: action.payload,
}
case types.ADD_VOTE:
case types.DELETE_GIFT:
default:
return state;
}
};
export default giftReducer;
ListContainer
import React, { Component } from 'react';
import { connect } from 'react-redux';
// import actions from action creators file
import * as actions from '../Actions/actions';
import AllGiftsDisplay from '../Components/AllGiftsDisplay.jsx';
import GiftCreator from '../Components/GiftCreator';
const mapStateToProps = (state) => ({
lastGiftId: state.gifts.lastGiftId,
giftList : state.gifts.giftList,
totalVotes: state.gifts.totalVotes,
setNewMessage: state.gifts.setNewMessage
});
//pass in text into update
const mapDispatchToProps = dispatch => ({
updateGiftMessage: (e) => {
console.log(e.target.value);
dispatch(actions.setMessage(e.target.value));
},
addGift: (e) => {
e.preventDefault();
console.log("actions: ", actions.addGift);
dispatch(actions.addGift());
}
});
class ListContainer extends Component {
constructor(props) {
super(props);
}
render() {
return(
<div className="All-Lists">
<h1>LIST CONTAINER!</h1>
<AllGiftsDisplay giftList = {this.props.giftList} addGift={this.props.addGift} setNewMessage={this.props.setNewMessage} totalVotes = {this.props.totalVotes} lastGiftId = {this.props.lastGiftId}/>
<GiftCreator setNewMessage={this.props.setNewMessage} updateGiftMessage={this.props.updateGiftMessage} addGift={this.props.addGift}/>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ListContainer);
The secret is to avoid mutating giftList.
const giftReducer = (state=initialState, action) => {
// let giftList;
// let setMessage;
switch(action.type) {
case types.ADD_GIFT:
// create the new gift object structure.
const giftStructure = {
// lastGiftId: stateCopy.lastGiftId,
newMessage: stateCopy.newMessage,
totalVotes: 0
};
return {
...state,
giftList: [...state.giftList, giftStructure],
newMessage: ''
}
case types.SET_MESSAGE:
return {
...state, newMessage: action.payload,
}
case types.ADD_VOTE:
case types.DELETE_GIFT:
default:
return state;
}
};
To better understand why it's necessary not to mutate the array, consider this example:
const arr = [1, 2, 3];
const b = { a: arr };
const c = { ...b };
c.a.push(4);
console.log(arr === c.a); // outputs true
Related
UI:
I have a dropdown list with four types of currencies. Every time I pick a new currency, the currency representations on the screen should also change. When I choose the first time, let's say Euro, all my currency marks go blank. Then, if I pick Pound, all my currency marks a change to the Euro sign. Why my state change is delayed?
components/currency.js
import React, { useState,useContext,useEffect } from 'react';import { AppContext } from '../context/AppContext';
const Budget = () => {const [currency, setCurrency] = useState('');const { dispatch } = useContext(AppContext);
const handleChange = (event) => {
setCurrency(event.target.value);
changeCurrency()
};
const changeCurrency = () => {
dispatch({
type: 'CHG_CURRENCY',
payload: currency,
});
}
useEffect(() => {console.log(currency);}, [currency] )
return (
<div className="alert alert-secondary">
<select id="inputGroupSelect02" onChange={handleChange}>
<option defaultValue>Currency</option>
<option value="$" name="dollar">$ Dollar</option>
<option value="£" name="pound">£ Pound</option>
<option value="€" name="euro">€ Euro</option>
<option value="₹" name="ruppee">₹ Ruppee</option>
</select>
</div>
);
export default Budget;
context/AppContext.js
import React, { createContext, useReducer } from 'react';
// 5. The reducer - this is used to update the state, based on the action
export const AppReducer = (state, action) => {
let budget = 0;
switch (action.type) {
case 'ADD_EXPENSE':
let total_budget = 0;
total_budget = state.expenses.reduce(
(previousExp, currentExp) => {
return previousExp + currentExp.cost
},0
);
total_budget = total_budget + action.payload.cost;
action.type = "DONE";
if(total_budget <= state.budget) {
total_budget = 0;
state.expenses.map((currentExp)=> {
if(currentExp.name === action.payload.name) {
currentExp.cost = action.payload.cost + currentExp.cost;
}
return currentExp
});
return {
...state,
};
} else {
alert("Cannot increase the allocation! Out of funds");
return {
...state
}
}
case 'RED_EXPENSE':
const red_expenses = state.expenses.map((currentExp)=> {
if (currentExp.name === action.payload.name && currentExp.cost - action.payload.cost >= 0) {
currentExp.cost = currentExp.cost - action.payload.cost;
budget = state.budget + action.payload.cost
}
return currentExp
})
action.type = "DONE";
return {
...state,
expenses: [...red_expenses],
};
case 'DELETE_EXPENSE':
action.type = "DONE";
state.expenses.map((currentExp)=> {
if (currentExp.name === action.payload) {
budget = state.budget + currentExp.cost
currentExp.cost = 0;
}
return currentExp
})
action.type = "DONE";
return {
...state,
budget
};
case 'SET_BUDGET':
action.type = "DONE";
state.budget = action.payload;
return {
...state,
};
case 'CHG_CURRENCY':
action.type = "DONE";
state.currency = action.payload;
return {
...state
}
default:
return state;
}
};
// 1. Sets the initial state when the app loads
const initialState = {
budget: 2000,
expenses: [
{ id: "Marketing", name: 'Marketing', cost: 50 },
{ id: "Finance", name: 'Finance', cost: 300 },
{ id: "Sales", name: 'Sales', cost: 70 },
{ id: "Human Resource", name: 'Human Resource', cost: 40 },
{ id: "IT", name: 'IT', cost: 500 },
],
currency: '£'
};
// 2. Creates the context this is the thing our components import and use to get the state
export const AppContext = createContext();
// 3. Provider component - wraps the components we want to give access to the state
// Accepts the children, which are the nested(wrapped) components
export const AppProvider = (props) => {
// 4. Sets up the app state. takes a reducer, and an initial state
const [state, dispatch] = useReducer(AppReducer, initialState);
let remaining = 0;
if (state.expenses) {
const totalExpenses = state.expenses.reduce((total, item) => {
return (total = total + item.cost);
}, 0);
remaining = state.budget - totalExpenses;
}
return (
<AppContext.Provider
value={{
expenses: state.expenses,
budget: state.budget,
remaining: remaining,
dispatch,
currency: state.currency
}}
>
{props.children}
</AppContext.Provider>
);
};
Spent so far component
import React, { useContext } from 'react';
import { AppContext } from '../context/AppContext';
const ExpenseTotal = () => {
const { expenses,currency } = useContext(AppContext);
const totalExpenses = expenses.reduce((total, item) => {
return (total += item.cost);
}, 0);
return (
<div className='alert alert-primary'>
<span>Spent so far: {currency}{totalExpenses}</span>
</div>
);
};
export default ExpenseTotal;
Put the changeCurrency() in your useEffectHook.
This is happening because when you call setCurrency to update your state it does not update it instantly, instead it throws it in a queue which will not take effect until you current running stack gets empty. As a result you don't get the currency state updated instantly.
And I would recommend you to take look at this react documentation
Which will describe you why this is happening in details
React state as a snapshot
I'm trying to understand how make a component that can remove itself from a array of components with functional components. Here is the sample code of what I'm trying to do:
const App = () => {
<ObjState>
<ObjectCreator />
<ObjectList />
</ObjState>
}
const ObjContext = createContext();
const ObjReducer = (state, { type, payload }) => {
switch(type) {
case Types.ADD_OBJ:
return {
...state,
objects: [...state.objects, payload]
};
case Types.REMOVE_OBJ:
return {
...state,
objects: state.objects.filter(obj => obj !== payload)
};
default:
return state;
}
}
const ObjState = ({ children }) => {
const initialState = {
objects: []
}
const [state, dispatch] = useReducer(ObjRecuder, initialState);
const addObj = (obj) => {
dispatch({
type: Types.ADD_OBJ,
payload: obj
});
}
const removeObj = (obj) => {
dispatch({
type: Types.REMOVE_OBJ,
payload: obj
});
}
return (
<ObjContext.Provider value={{
objects: state.objects,
addObj,
removeObj
}}>
{children}
</ObjContext.Provider>
);
}
const ObjCreator = () => {
const { addObject } = useContext(ObjContext);
const createObj =() => {
const obj = (<ObjectTypeA key={uuid()} />);
addObject(obj);
}
return (<button onClick={createObj}>create an object!</button>)
}
const ObjectList = () => {
const { objects } = useContext(ObjContext)
return (
<fragment>
{objects}
</fragment>
)
}
const ObjectTypeA = ({ key }) => {
const { removeObj } = useContext(ObjContext);
const removeSelf = () => {
removeObj(this);
}
return (
<button onClick={removeSelf}>remove me!</button>
)
}
The problem is you can't reference this in the final Object component.
I have the unique key but I'm not sure how to pass it through correctly. I attempted to build a reducer action that took the key from the Object and removed it that way but key came back as undefined even though it is deconstructed out of the props and I'm using an arrow function to preserve it.
I feel like I'm tackling this problem in the wrong way.
Issue
I think you veer off-course when trying to store what looks to be React components in your context state, you should be storing objects instead. The objects should have unique GUIDs. This allows the reducer to identify which object element to remove from state. The ObjectList should then render derived React components from the stored state.
I attempted to build a reducer action that took the key from the
Object and removed it that way but key came back as undefined even
though it is deconstructed out of the props and I'm using an arrow
function to preserve it.
This is because React keys (and refs) are not actually props. Keys can't be accessed in children components. You can can pass the same value via just about any other named prop though. Note below in solution I pass a React key and an id prop.
Solution
ObjectCreator: Creates objects, not React components
const ObjectCreator = () => {
const { addObj } = useContext(ObjContext);
const createObj = () => {
const obj = {
id: uuid()
};
addObj(obj);
};
return <button onClick={createObj}>create an object!</button>;
};
SpecificObject: passes its id to the removeObj callback.
const MyObject = ({ id }) => {
const { removeObj } = useContext(ObjContext);
const removeSelf = () => {
removeObj(id);
};
return (
<div>
<button onClick={removeSelf}>remove {id}</button>
</div>
);
};
ObjectList: renders the context objects mapped to JSX.
const ObjectList = () => {
const { objects } = useContext(ObjContext);
return (
<>
{objects.map((el) => (
<MyObject key={el.id} id={el.id} />
))}
</>
);
};
Check the passed id payload in the remove object reducer
const ObjReducer = (state, { type, payload }) => {
switch (type) {
case Types.ADD_OBJ:
return {
...state,
objects: [...state.objects, payload]
};
case Types.REMOVE_OBJ:
return {
...state,
objects: state.objects.filter((obj) => obj.id !== payload)
};
default:
return state;
}
};
Demo
I am dynamically adding <div> elements to a component by adding them to an array. This is not a problem and works well. The issue I'm trying to solve here is removing the <div> on double click by passing the id of the <div> that was doubled clicked with props when the reducer is dispatched.
The main issue is the array filter function only works when I code hard the div id both on the div and in the filter function when I want to pass the id of e.target.id on dispatch of delDiv reducer.
Note: I can remove the div successfully by changing the addDivReducer like this:
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={11} ***************************************************** Changed
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== 11; *********************************** Changed
});
return state;
But the desired effect is to pass id as props on dispatch as seen in my code below
The reducer that adds a removes elements look like this:
import DivComponent from "../../components/AddDivComponent";
const addDivReducer = (state, action) => {
switch (action.type) {
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={Math.floor(Math.random() * 100) + 1}
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
return state;
default:
return (state = []);
}
};
export default addClipartReducer;
The actions index.js look like:
export const addDiv = (props) => {
return {
type: "ADD_DIV",
payload: props,
};
};
export const deleteDiv = (props) => {
return {
type: "DELETE_DIV",
payload: props,
};
};
The delete reducer is being dispatched when the div is double clicked on like this in AddDivComponent.js:
import { useDispatch } from "react-redux";
import { deleteDiv } from "../../store/actions";
const AddDivComponent = (props) => {
const dispatch = useDispatch();
const removeClipart = (e) => {
dispatch(deleteDiv(e.target.id));
};
return(
<div
id={props.id}
className="my-div"
onDoubleClick={removeDiv}
/>
);
};
export default DivComponent;
Finally the array of <div> elements is being shown here in Canvas.js:
import { useSelector } from "react-redux";
const Canvas = () => {
const divList = useSelector((state) => state.addDIV);
return(
<div className="canvas">
{divList}
</div>
);
};
export default Canvas;
you are mutating state at your DELETE_DIV reducer. If you need to handle state, create a copy a first:
// mutating state here to a new value, can lead to problems
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
I would suggest to return filter directly, given filter already returns the desired next state, while not mutating the original:
case "DELETE_DIV":
return state.filter((elements) => {
return elements.props.id !== action.payload;
});
I'm trying to build a simple todo-app using react-redux. Problem is when I'm trying to update data, it would not update in view. My code is given below:
actions
export const listTodo = () => { type: actionTypes.LIST_TODO }
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload, index }
Reducers
const INITIAL_STATE = {
all: [{
name: 'init',
status: false,
lastUpdate: new Date().toISOString()
}]
}
const listTodo = ( state, action ) => {...state, all: state.all }
const updateTodo = ( state, action ) => {
const listTodo = {...state, all: state.all }; // init data
// find data
let todo = listTodo.all.filter( (todo, index) => index === action.index );
// update data
todo.name = action.payload.name;
todo.status = action.payload.status;
todo.lastUpdate = new Date().toISOString();
listTodo.all[ action.index ] = todo;
// return data
return {
...state,
all: listTodo.all
}
}
export default ( state = INITIAL_STATE, action) => {
switch( action.type ) {
case LIST_TODO:
return listTodo( state, action );
case UPDATE_TODO:
return updateTodo( state, action );
default:
return state;
}
}
In below code (Components/list.js), I just fetch all todo-list, and then print all list using ShowList.
Components/list.js
import ShowList from '../container/showList';
class TodoList extends Component {
renderTodoList() {
return this.props.all.map( (todo, index) => {
return (
<ShowList key={index} index={index} todo={todo} />
);
});
}
render() {
return <ul> { this.renderTodoList() } </ul>
}
}
const mapStateToProps = ( state ) => { all: state.todo.all };
export default connect( mapStateToProps ) ( TodoList );
In below code (container/showList.js), todo list is shown using <li /> and also have a checkbox, when user click on checkbox, handleCheckbox will trigger, and will update todo-list. I believe data is updated correctly, but it is not updated on html. In browser, todo-list remain same as before.
container/showList.js
class ShowList extends Component {
handleCheckbox = ( ) => {
const { todo, index } = this.props;
todo.status = !todo.status;
todo.lastUpdate = new Date().toISOString();
this.props.onUpdateTodo( todo, index );
}
render() {
const { todo, index } = this.props;
return (
<li> <input type="checkbox" onChange={ this.handleCheckbox } checked={todo.status} /> {todo.name} # {todo.status.toString()} # { todo.lastUpdate } </li>
)
}
}
const mapDispatchToProps = ( dispatch ) => onUpdateTodo: ( todo, index ) => dispatch( actions.updateTodo( todo, index ) )
export default connect(null, mapDispatchToProps) (ShowList);
How can I solve this problem? Thanks in Advance.
Your problem was in your reducers file. Whenever you executed updateToDo() you were not actually updating the the existing todos, you would just add a new property to your state with the new changes. This created layers and layers of properties without actually updating the first-layer. And since your components were only connected to the first-layer, it would never get the updated state.
I've updated a codesandbox for your reference: https://codesandbox.io/s/lively-flower-mwh79
You can update your reducers to something like this and then your code works completely fine:
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case "LIST_TODO":
return listTodo(state, action);
case "UPDATE_TODO":
return {
...state,
all: state.all.map((todo, index) => {
if (index == action.index) {
return {
...todo,
status: todo.status,
lastUpdate: new Date().toISOString()
};
} else {
return todo;
}
})
};
default:
return state;
}
};
Your problem is in this line,
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload, index }
Redux Action will only take 2 parameters, type and payload respectively.
Here you are passing 3 parameters which is wrong. Remove your index parameter, then your action becomes like this,
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload } //payload = your updated todo list
Pass only update todo list to your action,
this.props.onUpdateTodo( todo );
Finally in your reducer only do this,
return Object.assign(state,action.todo) // This will merge your old state with updated todo list and eventually you will get a updated list.
See more obout Object.assign here
I have a weird problem with React-Redux app. One of my components doesn't re-render on props change which is updated by action 'SET_WINNING_LETTERS'. github repo: https://github.com/samandera/hanged-man
setWord.js
const initialWordState = {
word: []
};
const setWinningLetters = (wordProps) => {
let {word, pressedKey} = wordProps;
for (let i = 0; i < word.length; i++) {
if (/^[a-zA-Z]$/.test(pressedKey) && word[i].letter.toUpperCase() == pressedKey) {
word[i].visible = true;
}
}
return {word};
}
const setWord = (state = initialWordState, action) => {
switch(action.type) {
case 'SET_WORD': return Object.assign({}, state, getWord(action.word));
case 'SET_WINNING_LETTERS': return Object.assign({}, state,
updateWord(action.wordProps));
}
return state;
}
export default setWord;
In Index.js in this function the actions are triggered
handleKeyPress(pressedKey) {
store.dispatch({
lettersProps: {
word:this.props.word,
pressedKey,
missedLetters: this.props.missedLetters
},
type: 'SET_MISSED_LETTERS'
});
store.dispatch ({
wordProps: {
word:this.props.word,
pressedKey
},
type: 'SET_WINNING_LETTERS'
});
this.showEndGame(this.props.word,this.props.missedLetters);
};
componentWillMount() {
fetchWord(this.statics.maxWordLength);
window.onkeydown = () =>
{this.handleKeyPress(String.fromCharCode(event.keyCode))};
}
And in PrimaryContent.js Winning and Missing Characetrs are rendered
import React from 'react';
import {connect} from 'react-redux';
import store from '../reducers/store';
import Hangedman from './Hangedman';
import AspectRatio from './AspectRatio';
import Puzzle from './Puzzle';
import MissedCharacters from './MissedCharacters';
const mapStateToProps = (store) => {
return {
word: store.wordState.word,
missedLetters: store.missedLettersState.missedLetters
}
}
class PrimaryContent extends React.Component {
constructor() {
super();
}
renderDisabledPuzzles(amount){
return Array.from({length: amount}, (value, key) => <AspectRatio parentClass="disabled" />)
}
renderLetters(word) {
return word.map(function(letterObj, index) {
let space = (letterObj.letter==' ' ? "disabled": '')
return(
<AspectRatio parentClass={space} key={"letter" + index}>
<div id={"letter" + index}>{letterObj.visible ? letterObj.letter : ''}</div>
</AspectRatio>
)
}) ;
}
render() {
let disabledCount = this.props.puzzles - this.props.word.length;
let disabledPuzzles = this.renderDisabledPuzzles(disabledCount);
let WinningLetters = this.renderLetters(this.props.word);
return (
<div className="ratio-content primary-content">
<Hangedman/>
<MissedCharacters missedLetters={this.props.missedLetters}/>
<Puzzle>
{disabledPuzzles}
{WinningLetters}
</Puzzle>
</div>
);
}
}
export default connect(mapStateToProps)(PrimaryContent);
MissedCharacters works well while {WinningLetters} doesn't.
The action 'SET_MISSED_LETTERS' works perfect, while 'SET_WINNING_LETTERS' works only when 'SET_MISSED_LETTERS' gets updated. It means when I press one or more letter that wins they won't display until I press the letter that is missing. When I press the missing letter the component that is parent for both missing and winning letters re-renders. I was trying to pass props to PrimaryContent from it's parent but I get the same. I tried to separate {WinningLetters} in it's own component wit access to redux store but it works even worse and stops updating even when MissedCharacters updates. Can you detect where I've made a mistake?