Passing data between components in React/Redux - reactjs

I'm new to React and am trying to build an app which shuffles football players into two teams and am having difficulty with passing data from one component to another.
I have redux and react-redux installed.
In my reducer.js, I take a list of players and shuffle them, adding the shuffled list to state:
const shufflePlayers = (state) => {
return {
...state,
shuffledList: [
...state.playersList.sort(() => Math.random() - 0.5)
]
}
}
Then in 'src/components/DisplayTeams.index.js', I map the 'shuffledList' array to props:
import { connect } from "react-redux";
import DisplayTeams from "./DisplayTeams";
const mapStateToProps = (state) => {
return {
shuffledList: state.shuffledList,
};
};
export default connect(mapStateToProps)(DisplayTeams);
and finally, in 'src/components/DisplayTeams.js', I attempt to render the 'shuffledList' array in a list:
import React from 'react';
import '../../App.css';
const DisplayTeams = ({ shuffledList }) => (
<div>
<ul>
{shuffledList.map((player, index) => (
<li key={index}>{player.name}</li>
))}
</ul>
</div>
);
export default DisplayTeams;
but am getting TypeError: Cannot read property 'map' of undefined, indicating that the 'shuffledList' array is empty or not set at all.
Any help will be much appreciated!!

Two things:
You should add add an initial state, you can set it directly in the reducer file
const initialState = {
// other reducer parts here
shuffledList: []
}
The reducer should check the action type, otherwise it would run at any action. Something like this:
const shufflePlayers = (state = initialState, action) => {
switch (action.type) {
case actionTypes.SHUFFLE_LIST: {
// use a new array, avoid mutating the previous state
const sortedList = [...state.playersList].sort(() => Math.random() - 0.5)
return {
...state,
shuffledList: sortedList
}
}
}

You should not copy data in state, the list and shuffledList are the same data but shuffledList is a calculated result of list.
You can use a selector to calculate shuffled list from list instead to prevent it from re calculating on renders you can use reselect (should use that anyway) and memoize shuffled result as long as list doesn't change.
const { Provider, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
};
const reducer = (state = initialState) => state;
//selectors
const selectList = (state) => state.list;
//if state.list changes then it will shuffle again
const selectShuffledList = createSelector(
[selectList],
(list) => [...list].sort(() => Math.random() - 0.5)
);
const selectTeams = createSelector(
[selectShuffledList, (_, size) => size],
(shuffledList, teamSize) => {
const teams = [];
shuffledList.forEach((item, index) => {
if (index % teamSize === 0) {
teams.push([]);
}
teams[teams.length - 1].push(item);
});
return teams;
}
);
const selectTeamsCurry = (teamSize) => (state) =>
selectTeams(state, teamSize);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (n) => (a) => n(a))
)
);
const App = () => {
//you can re render app with setCount
const [count, setCount] = React.useState(0);
//setting count has no effect on teams because
// state.list didn't change and selectShuffledList
// will use memoized shuffled result
const teams = useSelector(selectTeamsCurry(3));
return (
<div>
<button onClick={() => setCount((w) => w + 1)}>
re render {count}
</button>
<pre>{JSON.stringify(teams, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>

The above code looks fine. You can check the shuffledList in the initial state and also in the Redux store while dispatching actions.

Related

What is the proper way to avoid rerenders when selecting an array in React Redux?

Is there a way to select a derived array from an array in a Redux store without spurious renders?
My Redux store contains an array of objects.
state = {items: [{id: 1, keys...}, {id: 2, keys...}, {id: 3, keys...}, ...]}
I wrote a selector to return an array of ids.
const selectIds = (state: MyStateType) => {
const {items} = state;
let result = [];
for (let i = 0; i < items.length; i++) {
result.push(items[I].id);
}
return result;
};
I then call this selector using react-redux's useSelector hook, inside a component to render out a list of components.
const MyComponent = () => {
const ids = useSelector(selectIds);
return (
<>
{ids.map((id) => (
<IdComponent id={id} key={id} />
))}
</>
);
};
I am finding that MyComponent is being rendered every call to dispatch which breaks down performance at a higher number of array elements.
I have passed in an equality function to useSelector like so:
import {shallowEqual, useSelector } from "react-redux";
const ids = useSelector(selectIds, (a, b) => {
if (shallowEqual(a, b)) {
return true;
}
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i].id !== b[i].id) {
return false;
}
}
return true;
});
But dispatch is called enough times that checking equality becomes expensive with a large amount of array elements.
I have tried using the reselect library as well.
const selectItems = (state: MyStateType) => {
return state.items;
};
const selectIds = createSelector(
selectItems,
(items) => {
let result = [];
for (let i = 0; i < items.length; i++) {
result.push(items[i].id);
}
return result;
}
);
However, every time I modify the properties of one array element in state.items via dispatch, this changes the dependency of selectItems which causes selectIds to recalculate.
What I want is for selectIds to only recompute when the ids of state.items are modified. Is this possible?
I think the best you can do here is to combine reselect with the use of shallowEqual:
import { shallowEqual } from "react-redux";
const selectItems = (state: MyStateType) => state.items;
const selectIds = createSelector(
selectItems,
(items) => items.map(item => item.id)
);
const MyComponent = () => {
const ids = useSelector(selectIds, shallowEqual);
return (
<>
{ids.map((id) => (
<IdComponent id={id} key={id} />
))}
</>
);
};
Notes
I'm using Array.map to extract ids: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
shallowEqual can be passed directly to useSelector
With the code above:
The array of ids will be re-created only if state.items change.
The ids variable will have a new reference only if the ids changed.
If this solution is not enough (can't afford the shallowEqual) you can take a look at https://github.com/dai-shi/react-tracked it uses a more precise system to track which part of the state is used (using Proxies: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).
Another way of doing this is to memoize the ids array in the selector:
const { createSelector, defaultMemoize } = Reselect;
const selectItems = (state) => {
return state.items;
};
const selectIds = (() => {
//memoize the array
const memArray = defaultMemoize((...ids) => ids);
return createSelector(selectItems, (items) =>
memArray(...items.map(({ id }) => id))
);
})(); //IIFE
//test the code:
const state = {
items: [{ id: 1 }, { id: 2 }],
};
const result1 = selectIds(state);
const newState = {
...state,
items: state.items.map((item) => ({
...item,
newValue: 88,
})),
};
const result2 = selectIds(newState);
console.log('are they the same:', result1 === result2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>

redux calculated data re-rendering how to fix

I have a list of cards that take the state tree.
I have one selector that gets the list of jobs, then two selectors that use that selection to map and combine an object to pass into the card.
function ProductionJobs(props) {
const jobData = useSelector(getDataForProductionJobs);
const dataData = useSelector(getDataForProduction(jobData.map(x=>x.jobsessionkey)));
const matData = useSelector(getMatsForProduction(jobData.map(x=>x.jobsessionkey)));
console.count("renders");
const combined = jobData.map(x=> {
const foundData = dataData.find(y=>y.attachedJobKey===x.jobsessionkey);
const foundMaterial = matData.filter(z=>z.attachedJobkey===x.jobsessionkey);
const obj = {...x}
if(foundData) obj.foundData = foundData;
if(foundMaterial) obj.material = foundMaterial;
return obj;
});
const productionCards = combined.map(x=><ProductionJobCard key={x.jobsessionkey} props={x} />)
return <div className="ProductionJobs">{productionCards}</div>
}
The problem is - this re-renders unnecessarily. Is there a better way of combining this data on the reducer's side, instead of the component?
You can create a container for ProductionJobCard and select combined items in that one using shallowEqual as second argument when filtering matData items.
const {
Provider,
useDispatch,
useSelector,
shallowEqual,
} = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
productionJobs: [
{ jobSessionKey: 1 },
{ jobSessionKey: 2 },
{ jobSessionKey: 3 },
{ jobSessionKey: 4 },
],
data: [{ id: 1, attachedJobKey: 1 }],
mat: [
{ id: 1, attachedJobKey: 1 },
{ id: 2, attachedJobKey: 1 },
{ id: 3, attachedJobKey: 2 },
],
};
//action types
const TOGGLE_MAT_ITEM = 'TOGGLE_MAT_ITEM';
const TOGGLE_DATA_ITEM = 'TOGGLE_DATA_ITEM';
const TOGGLE_JOB = 'TOGGLE_JOB';
//action creators
const toggleMatItem = () => ({ type: TOGGLE_MAT_ITEM });
const toggleDataItem = () => ({ type: TOGGLE_DATA_ITEM });
const toggleJob = () => ({ type: TOGGLE_JOB });
const reducer = (state, { type }) => {
if (type === TOGGLE_MAT_ITEM) {
//toggles matItem with id of 3 between job 1 or 2
return {
...state,
mat: state.mat.map((matItem) =>
matItem.id === 3
? {
...matItem,
attachedJobKey:
matItem.attachedJobKey === 2 ? 1 : 2,
}
: matItem
),
};
}
if (type === TOGGLE_DATA_ITEM) {
//toggles data between job 1 or 3
const attachedJobKey =
state.data[0].attachedJobKey === 1 ? 3 : 1;
return {
...state,
data: [{ id: 1, attachedJobKey }],
};
}
if (type === TOGGLE_JOB) {
//adds or removes 4th job
const productionJobs =
state.productionJobs.length === 3
? state.productionJobs.concat({ jobSessionKey: 4 })
: state.productionJobs.slice(0, 3);
return { ...state, productionJobs };
}
return state;
};
//selectors
const selectDataForProductionJobs = (state) =>
state.productionJobs;
const selectData = (state) => state.data;
const selectMat = (state) => state.mat;
const selectDataByAttachedJobKey = (attachedJobKey) =>
createSelector([selectData], (data) =>
data.find((d) => d.attachedJobKey === attachedJobKey)
);
const selectMatByAttachedJobKey = (attachedJobKey) =>
createSelector([selectMat], (mat) =>
mat.filter((m) => m.attachedJobKey === attachedJobKey)
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const ProductionJobCard = (props) => (
<li><pre>{JSON.stringify(props, undefined, 2)}</pre></li>
);
const ProductionJobCardContainer = React.memo(
function ProductionJobCardContainer({ jobSessionKey }) {
//only one item, no need to shallow compare
const dataItem = useSelector(
selectDataByAttachedJobKey(jobSessionKey)
);
//shallow compare because filter always returns a new array
// only re render if items in the array change
const matItems = useSelector(
selectMatByAttachedJobKey(jobSessionKey),
shallowEqual
);
console.log('rendering:', jobSessionKey);
return (
<ProductionJobCard
dataItem={dataItem}
matItems={matItems}
jobSessionKey={jobSessionKey}
/>
);
}
);
const ProductionJobs = () => {
const jobData = useSelector(selectDataForProductionJobs);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(toggleMatItem())}>
toggle mat
</button>
<button onClick={() => dispatch(toggleDataItem())}>
toggle data
</button>
<button onClick={() => dispatch(toggleJob())}>
toggle job
</button>
<ul>
{jobData.map(({ jobSessionKey }) => (
<ProductionJobCardContainer
key={jobSessionKey}
jobSessionKey={jobSessionKey}
/>
))}
</ul>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<ProductionJobs />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
You should not combine the data on the reducer because you will essentially copy the data (combined data is essentially a copy of the data you already have). The combined data is a derived value and such values should not be stored in state but calculated in selectors, re calculate when needed by using memoization (not done here) but if you're interested you can see here how I use reselect for memoizing calculations.
At the moment the filter and find are run on each item but since the outcome is the same the component is not re rendered.

React replacing useState with useReducer while also using a custom hook

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

Why cant I remove element from array in Reactjs with redux

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

Increase count and decrease count when action is dispatch

I am currently developing a front end application using react-redux. But I am very new to this language.
So basically I have the following UI
What I am trying to achieve is whenever users increase or decrease the option, it will store to the store procedure, and finally make an API call to backend and calculate pricing.
Before API call, my idea is I will let users to increase/decrease the option and finally when the user is done, i will take that array of object and submit to the api endpoint.
Unfortunately, It seems like the following scenario is failed.
I increase option 1, it will save to the state as an array of object
first time with quantity and optionId [OK]
After that, I will increase the option 2, since it is the new option,
I will push the object to the existing array. [OK]
When I try to increase option 1 again, it has to check whether option
1 is already inside the array, if there is option 1, it will just
increase that option quantity. but my code does not behave that way. [FAILED]
below is my Component
import React, {Component} from 'react';
import {handleIncreaseOption} from '../actions/option';
import {Button, Card, Col, Row, Statistic} from "antd";
import {MinusOutlined, PlusOutlined} from '#ant-design/icons';
import {connect} from 'react-redux';
const {Meta} = Card;
class FlavourCard extends Component {
state = {
quantity: 0,
optionId: this.props.optionId
}
increase = () => {
let count = this.state.quantity + 1;
this.setState({
quantity: count,
optionId: this.props.optionId
}, function(){
console.log('this state before going in', this.state);
this.props.dispatch(handleIncreaseOption(this.state));
});
}
decline = () => {
let count = this.state.count - 1;
if (count < 0) {
count = 0;
}
this.setState({count: count});
console.log(this.state);
}
render() {
const {flavourImg, itemTitle} = this.props;
return (
<Card
hoverable
cover={<img alt="example" className="flavour-img" src={flavourImg}/>}
>
<Meta
title={itemTitle}
style={{textAlign: 'center'}}
description={
<Row justify="start" gutter={12}>
<Col span={10} style={{textAlign: 'right', paddingTop: '6px'}}>
<Button onClick={this.decline} size="small">
<MinusOutlined/>
</Button>
</Col>
<Col span={4}>
<Statistic value={this.state.quantity} style={{fontSize: '10px'}}/>
</Col>
<Col span={10} style={{textAlign: 'left', paddingTop: '6px'}}>
<Button onClick={this.increase} size="small">
<PlusOutlined/>
</Button>
</Col>
</Row>
}
/>
</Card>
)
}
}
function mapStateToProps(state) {
return{
loadingBar: state.loadingBar
}
}
export default connect(mapStateToProps) (FlavourCard)
This is my action class
export const RETRIEVE_OPTIONS = 'RETRIEVE_OPTIONS';
export const INCREASE_OPTIONS = 'INCREASE_OPTIONS';
export function receiveOptions( option ) {
return {
type: RETRIEVE_OPTIONS,
option
}
}
export function handleIncreaseOption ( option ) {
return {
type: INCREASE_OPTIONS,
option
}
}
This is my reducer
import {RETRIEVE_OPTIONS, INCREASE_OPTIONS} from "../actions/option";
export default function option ( state = null , action )
{
switch (action.type) {
case RETRIEVE_OPTIONS:
return {
...state,
...action
}
case INCREASE_OPTIONS:
if ( !state.hasOwnProperty('addOption') ) {
return {
...state,
addOption: [
{
quantity: action.option.quantity,
optionId: action.option.optionId
}
]
}
}
state.addOption.map((opt) => {
if(opt.optionId === action.option.optionId) {
opt.quantity = action.option.quantity;
}else {
let originalAddOption = state.addOption;
originalAddOption.push({
quantity: action.option.quantity,
optionId: action.option.optionId
})
}
return {
...state,
...action
}
})
default:
return state
}
}
I believe that my "INCREASE_OPTIONS" reducer is something wrong, because, the correct logic should be when there is a new optionId, it will add in as a new object, and if the optionId is existing one, it will just increase the entity. For my current code, whenever I make a second option to increase, it will just add in a new object with new quantity value. I have attached the console result below
How can I achieve when there is existing option, just increase/decrease the quantity and if option is newly added, make a new object and push to the array? Thanks in advance
There are a couple problems in the reducer.
The first issue is that you are trying to update the state object directly. This will not work, you have to set state to a new object.
The second issue is how you are using the map function. It looks like you are using it to update a value if it exists, or add a new entry if it does not. You might have to separate that out and first check if it exists, if so do an update, if not add a new element. Then for each opt in the array, you return an object containing the entire state and action, which I don't think is your intention.
Try out something like this in the reducer:
case INCREASE_OPTIONS: {
if ( !state.hasOwnProperty('addOption') ) {
return {
...state,
addOption: [
{
quantity: action.option.quantity,
optionId: action.option.optionId
}
]
}
}
let updated = false;
// For every element, check if we find the id to modify
// Map returns an array. Does not modify in place.
let addOptCopy = state.addOption.map((opt) => {
if(opt.optionId === action.option.optionId) {
opt.quantity = action.option.quantity;
updated = true;
}
return opt;
});
// If nothing was updated, push new element
if(!updated){
addOptCopy.push({
quantity: action.option.quantity,
optionId: action.option.optionId
})
}
// return the new state
return {
...state,
addOption: [...addOptCopy]
}
}
As one of the comments on your post suggested, it may be an over complication to be keeping two states, using the components state plus redux state and keeping them in sync. You can do the increase and decrease within the reducer, and get the state from props in the components by linking it in mapStateToProps.
Lastly, there seems to be a typo in the decrease function, you are setting count in state instead of quantity.
Here is a functional example, you only need to pass id to the increaseOption action creator:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
data: [
{
id: 1,
},
{
id: 2,
},
],
};
//action types
const INCREASE_OPTIONS = 'INCREASE_OPTIONS';
//action creators
const increaseOption = (id) => ({
type: INCREASE_OPTIONS,
payload: id,
});
const reducer = (state, { type, payload }) => {
if (type === INCREASE_OPTIONS) {
const addOption = state.addOption || [];
const exist = addOption.some(
({ optionId }) => optionId === payload
);
return {
...state,
addOption: exist
? addOption.map((option) =>
option.optionId === payload
? { ...option, quantity: option.quantity + 1 }
: option
)
: addOption.concat({
optionId: payload,
quantity: 1,
}),
};
}
return state;
};
//selectors
const selectData = (state) => state.data;
const selectOption = (state) => state.addOption || [];
const createSelectOption = (id) =>
createSelector([selectOption], (options) => {
const option = options.find(
({ optionId }) => optionId === id
);
return option ? option.quantity : 0;
});
const createSelectItem = (itemId) =>
createSelector([selectData], (data) =>
data.find(({ id }) => id === itemId)
);
const createSelectCardProp = (id) =>
createSelector(
[createSelectOption(id), createSelectItem(id)],
(option, item) => ({ option, item })
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (n) => (a) => n(a))
)
);
const FlavourCard = React.memo(function FlavourCard({
id,
}) {
const dispatch = useDispatch();
const selectProps = React.useMemo(
() => createSelectCardProp(id),
[id]
);
const props = useSelector(selectProps);
return (
<button onClick={() => dispatch(increaseOption(id))}>
id: {props.item.id} count:{props.option}
</button>
);
});
const App = () => {
const data = useSelector(selectData);
return (
<ul>
{data.map(({ id }) => (
<FlavourCard key={id} id={id} />
))}
</ul>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>

Resources