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;
});
Related
I am new to using "#reduxjs/toolkit" (version "^1.5.1").
I am trying to remove an object from within the state's array (roundScore). This is usually something that is very simple to do using filter(). For some reason this isn't working and I can't figure out why. Here's my code:
Reducer slice:
import { createSlice } from "#reduxjs/toolkit";
export const roundScoreSlice = createSlice({
name: "roundScore",
initialState: {
roundScore: [],
},
reducers: {
deleteArrow: (state, action) => {
console.log(`action.payload = ${action.payload}`); // returns correct id
state.roundScore.filter((arrow) => arrow.id !== action.payload);
},
},
});
export const { deleteArrow } = roundScoreSlice.actions;
export default roundScoreSlice.reducer;
React component:
import React from "react";
import styled from "styled-components";
import { motion } from "framer-motion";
import { useDispatch } from "react-redux";
import { deleteArrow } from "../../redux-reducers/trackSession/roundScoreSlice";
export default function InputtedScore({
arrowScore,
id,
initial,
animate,
variants,
}) {
const dispatch = useDispatch();
const applyStyling = () => {
switch (arrowScore) {
case 0:
return "miss";
case 1:
return "white";
case 2:
return "white";
case 3:
return "black";
case 4:
return "black";
case 5:
return "blue";
case 6:
return "blue";
case 7:
return "red";
case 8:
return "red";
case 9:
return "gold";
case 10:
return "gold";
default:
return null;
}
};
return (
<ParentStyled
id={id}
initial={initial}
animate={animate}
variants={variants}
onClick={() => dispatch(deleteArrow(id))}
>
<Circle className={applyStyling()}>
{arrowScore}
<IconStyled>
<IoIosClose />
</IconStyled>
<IoIosClose className="redCross" />
</Circle>
</ParentStyled>
);
}
The state after adding 2 arrows would look like this:
roundScore: [
{
id:"e0f225ba-19c2-4fd4-b2bf-1e0aef6ab4e0"
arrowScore:7
},
{
id:"2218385f-b37a-4f2c-a8db-4e7e65846171"
arrowScore:5
}
]
I've tried a combination of things.
Using e.target.id within dispatch
Using e.currentTarget.id within dispatch
Using ({id}) instead of just (id) within dispatch
Wrapping the reducer function with or without braces e.g. within (state, action) => { /* code */ }
What is it I'm missing? I know this is going to be a simple fix but for some reason it's eluding me.
Any help is much appreciated.
Okay, it looks like the issue is in the way how filter method works, it returns a new array, and an initial array is not mutated (that's why we have been using filter before to manipulate redux state), also in the code you've shown value of the filtering operation not assigned to any property of your state
You need to assign the value or mutate array, so the code below should work for you
state.roundScore = state.roundScore.filter((arrow) => arrow.id !== action.payload);
Mutate your existing array:
state.roundScore.splice(state.roundScore.findIndex((arrow) => arrow.id === action.payload), 1);
We can think outside of the box and look at it from another way. the React component above is just actually a child of a certain parent component. And for the purpose of my answer i assume that in your parent component you have some form of array.map .
So from that code, each array item will already have an array index. and you can pass that index as a prop to the above react component like so:
const InputtedScore = ({ ...all your props, id, inputIndex }) => {
const dispatch = useDispatch();
// Having access to your index from the props, you can even
// find the item corresponding to that index
const inputAtIndex_ = useSelector(state => state.input[inputIndex])
const applyStyling = () => {
switch (arrowScore) {
...your style logic
}
};
return (
// you can send that index as a payload to the reducer function
<ParentStyled id={id} onClick={() => dispatch(deleteArrow(inputIndex))}
...the rest of your properties >
<Circle className={applyStyling()}>
{arrowScore}
<IconStyled>
<IoIosClose />
</IconStyled>
<IoIosClose className="redCross" />
</Circle>
</ParentStyled>
);
}
After dispaching the delete action by sending as payload the item's index already, you do not need to find the item in the reducer anymore:
deleteMeal: (state, action) => {
// you receive you inputIndex from the payload
let { inputIndex } = action.payload;
// and you use it to splice the desired item off the array
state.meals.splice(inputIndex, 1);
...your other logic if any
},
You need to mutate your existing array
state.roundScore.splice(state.roundScore.findIndex((arrow) => arrow.id === action.payload), 1);
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
so I am making a budget tracking app where the user can add their income sources to an incomes list and expenses to an expenses list, and I got it working, but I wanted to see if I could use useReducer instead of using useState so many times. This is where I am stuck since I am not sure what to return in the reducer.
I am using 2 state objects, incomes and expenses. Basically for now I want to use a reducer to allow the user to add an income source to the incomes object. I want to see if I could set the incomes object inside the reducer, and when dispatch is called with the action set to ADD_INCOME_ITEM, budgetObj.type will be set to + and setIncomes(incomes.concat(budgetObj)) will be called (the income source will be added to the incomes list). I hope I made this clear!
App.js:
import React, { useState, useReducer } from 'react';
import './App.css';
import BudgetInput from './components/input/BudgetInput';
import BudgetOutput from './components/output/BudgetOutput';
import IncomeOutputList from './components/output/IncomeOutputList';
import ExpenseOutputList from './components/output/ExpenseOutputList';
// custom hook
const useSemiPersistentState = (key, initialState) => {
const [value, setValue] = React.useState(
localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key)) : initialState
);
React.useEffect(()=>{
localStorage.setItem(key, JSON.stringify(value));
}, [value, key])
return [value, setValue];
};
const App = () => {
// want to replace these 5 lines with useReducer
const [incomes, setIncomes] = useSemiPersistentState('income',[{}]);
const [expenses, setExpenses] = useSemiPersistentState('expense',[{}]);
const [description, setDescription] = useState('');
const [type, setType] = useState('+');
const [value, setValue] = useState('');
const budgetObj = {
desc: description,
budgetType: type,
incomeValue: value
}
const initialbudget = {
desc: '',
budgetType: '+',
incomeValue: ''
}
const budgetReducer = (state, action) => {
switch(action.type) {
case 'ADD_INCOME_ITEM': //want to set the incomes object here
return setIncomes(incomes.concat(budgetObj)); // not sure if this is correct??
// also set state here???
}
//will add more cases here
}
const [budget, dispatchBudget] = useReducer( //reducer, initial state
budgetReducer,
initialbudget
);
const handleBudgetObjArray = () => {
if(budgetObj.budgetType === '+') {
setIncomes(incomes.concat(budgetObj)); //want to move this to reducer
}
else if(budgetObj.budgetType === '-') {
setExpenses(expenses.concat(budgetObj)); //want to move this to reducer
}
}
const handleChange = (event) => {
setDescription(event.target.value);
}
const handleSelectChange = (event) => {
setType(event.target.value);
}
const handleValueChange = (event) => {
setValue(event.target.value);
console.log(budgetObj)
}
const removeInc = (index) => {
let items = JSON.parse(localStorage.getItem("income"));
items.splice(index, 1);
setIncomes(items);
}
const removeExp = (index) => {
let items = JSON.parse(localStorage.getItem("expense"));
items.splice(index, 1);
setExpenses(items);
}
return (
<div className="App">
<link href="http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css"></link>
<div className="top">
<BudgetOutput />
</div>
<div className="bottom">
<BudgetInput
descValue={description}
onDescChange={handleChange}
onSelectChange={handleSelectChange}
type={type}
onBudgetSubmit={handleBudgetObjArray}
budgetValue={value}
onValChange={handleValueChange}
/>
<div className="container clearfix">
<IncomeOutputList
list={incomes}
removeIncome={(index)=>removeInc(index)}
/>
<ExpenseOutputList
list={expenses}
removeExpense={(index)=>removeExp(index)}
/>
</div>
</div>
</div>
)
};
export default App;
This file is where budgetObj is set:
import React from 'react';
import IncomeOutput from './IncomeOutput';
// list will be list of income objects
const IncomeOutputList = ({ list, removeIncome }) => {
return (
<div className="income__list">
<div className="income__list--title">INCOME</div>
{list.map((item, index, arr) => <IncomeOutput
id={item.id}
value={item.incomeValue}
type={item.budgetType}
desc={item.desc}
// handleButton={handler(index)}
handleButton={()=>removeIncome(index)}
/>
)}
</div>
)
}
export default IncomeOutputList;
The useReducer replaces useState. It is your state. So this right here makes no sense.
case 'ADD_INCOME_ITEM': //want to set the incomes object here
return setIncomes(incomes.concat(budgetObj)); // not sure if this is correct??
Those five useState lines of your code which include incomes and setIncomes are going to be totally deleted, so you cannot be using them in your reducer.
It looks like the initialState for your reducer is just one budget object. It needs to be an object that represents the entire component state. Something like this:
const initialBudget = {
description: '',
type: '+',
value: '',
};
const initialState = {
incomes: [{}],
expenses: [{}],
budgetObj: initialBudget,
};
I am defining the initialBudget separately so that we can use it to reset the budgetObj easily.
Your reducer handles actions by taking the state and the action and returning the next state, like this:
const budgetReducer = (state, action) => {
switch(action.type) {
case 'SUBMIT_BUDGET':
// I am using spread to clone the object to be safe, might not be 100% neccessary
const budget = {...state.budget};
// figure out where to add the current budget object
const isIncome = budget.budgetType === '+';
return {
...state, // not actually necessary in this case since we are updating every property
incomes: isIncome ? state.incomes.concat(budget) : state.incomes, // maybe add to incomes
expenses: isIncome ? state.expenses : state.expenses.concat(budget), // maybe add to expenses
budgetObj: initialBudget, // reset budget object
}
default:
return state;
}
}
I am trying to pass the state after onClick to the component but it does not display because it is undefined. I want to display the state as the remaining number of cookies for sale
class CookieContainer extends Component {
render(){
return (
<div className="donutShop" >
//below I want to pass the updated state but {this.props.numOfCookies} is undefined
<h2>Cookies available for Sale: {this.props.numOfCookies}</h2>
<button onClick={this.props.buyCookie}>Buy cookie</button>
</div>
);
};
}
numOfCookies: is undefined when passed as props to CookieContainer
const mapStateToProps = state => ({numOfCookies: state.numOfCookies})
// dispatch
const mapDispatchToProps = dispatch => {
return {
buyCookie: () => dispatch(buyCookie())
};
};
let ConnectedCookie = connect(mapStateToProps, mapDispatchToProps)(CookieContainer)
export default ConnectedCookie
// action
export const buyCookie = () => {
return {
type: BUY_COOKIE,
content: 1
};
};
// state
const initialCookieState = {
numOfCookies: 100
}
const BUY_COOKIE = "BUY_COOKIE";
//reducer
const cookieReducer = (state = initialCookieState, action) => {
switch (action.type) {
case BUY_COOKIE:
return {
...state,
numOfCookies: state.numOfCookies - action.content
};
default:
return state;
}
};
I have created a stackblitz for the code that you provided here
and it is working for me.
The code itself seems fine. The problem might be in what you have not shown in your question. Make sure that you have created your store
const store = createStore(cookieReducer);
and passed your store properly by wrapping your app with provider
<Provider store={store}>
<App />
</Provider>
Also when declaring action types(BUY_COOKIE, etc.), declare the action types in a separate file and you can import them in the reducer. You can declare action creators and action types in the same file if you want.
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