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;
}
}
Related
The object of this app is to allow input text and URLs to be saved to localStorage. It is working properly, however, there is a lot of repeat code.
For example, localStoredValues and URLStoredVAlues both getItem from localStorage. localStoredValues gets previous input values from localStorage whereas URLStoredVAlues gets previous URLs from localStorage.
updateLocalArray and updateURLArray use spread operator to iterate of previous values and store new values.
I would like to make the code more "DRY" and wanted suggestions.
/*global chrome*/
import {useState} from 'react';
import List from './components/List'
import { SaveBtn, DeleteBtn, DisplayBtn, TabBtn} from "./components/Buttons"
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: "",
});
//these items are used for the state of localStorage
const [display, setDisplay] = useState(false);
const localStoredValues = JSON.parse(
localStorage.getItem("localValue") || "[]"
)
let updateLocalArray = [...localStoredValues, leadValue.inputVal]
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = JSON.parse(localStorage.getItem("URLValue") || "[]")
const tabBtn = () => {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
const url = tabs[0].url;
setMyLeads((prev) => [...prev, url]);
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
localStorage.setItem("URLValue", JSON.stringify(updateURLArray));
});
setDisplay(false)
};
//handles change of input value
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const saveBtn = () => {
setMyLeads((prev) => [...prev, leadValue.inputVal]);
setDisplay(false);
// update state of localStorage
localStorage.setItem("localValue", JSON.stringify(updateLocalArray))
};
const displayBtn = () => {
setDisplay(true);
};
const deleteBtn = () => {
window.localStorage.clear();
setMyLeads([]);
};
const listItem = myLeads.map((led) => {
return <List key={led} val={led} />;
});
//interates through localStorage items returns each as undordered list item
const displayLocalItems = localStoredValues.map((item) => {
return <List key={item} val={item} />;
});
const displayTabUrls = URLStoredVAlues.map((url) => {
return <List key={url} val={url} />;
});
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<TabBtn tabBtn={tabBtn} />
<DisplayBtn displayBtn={displayBtn} />
<DeleteBtn deleteBtn={deleteBtn} />
<ul>{listItem}</ul>
{/* displays === true show localstorage items in unordered list
else hide localstorage items */}
{display && (
<ul>
{displayLocalItems}
{displayTabUrls}
</ul>
)}
</main>
);
}
export default App
Those keys could be declared as const and reused, instead of passing strings around:
const LOCAL_VALUE = "localValue";
const URL_VALUE = "URLValue";
You could create a utility function that retrieves from local storage, returns the default array if missing, and parses the JSON:
function getLocalValue(key) {
return JSON.parse(localStorage.getItem(key) || "[]")
};
And then would use it instead of repeating the logic when retrieving "localValue" and "URLValue":
const localStoredValues = getLocalValue(LOCAL_VALUE)
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = getLocalValue(URL_VALUE)
Similarly, with the setter logic:
function setLocalValue(key, value) {
localStorage.setItem(key, JSON.stringify(value))
}
and then use it:
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
setLocalValue(URL_VALUE, updateURLArray);
// update state of localStorage
setLocalValue(LOCAL_VALUE, updateLocalArray)
When I click DisplayBtn() it should sets the display state to true and display myLeads Array from localStorage. localStorage contains MyLeads Array and I've used a map() in an attempt to fetch items and place them in an unordered list. I've done this before on arrays and it has worked but its not currently working.
Basically, I just want them items in localStorage to render in an unordered list. I've attempted several approaches to solve this issue my latest error message is 'Cannot read properties of null (reading 'map')'
import {useState} from 'react';
import List from './components/List'
import { SaveBtn } from './components/Buttons';
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: ""
})
const [display, setDisplay] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const [localItems, setLocalItems] = useState(
JSON.parse(localStorage.getItem("myLeads"))
);
const displayLocalItems = localItems.map((item) => {
return <List key={item} val={item}/>
})
const saveBtn = () => {
setMyLeads(prev => [...prev, leadValue.inputVal]);
localStorage.setItem("myLeads", JSON.stringify(myLeads))
setLocalItems((prevItems) => [...prevItems, leadValue.inputVal]);
setDisplay(false);
};
const displayBtn = () => {
setDisplay(true)
};
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<button onClick={displayBtn}>Display Leads</button>
{display && (
{displayLocalItems}
)
}
</main>
);
}
export default App;
You can do this:
const [localItems, setLocalItems] = useState(JSON.parse(localStorage.getItem("myLeads")) || []);
So if the local storage is empty you initialize your state to an empty array, which can be safely mapped.
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 pretty new to React and React hooks in general,
I'm building a react app for my final project and I wanted to make some component (Advanced search in this example) as generalized as possible which means I want to pass "dataFields" and the component should be updated with a unique state value that originated from those data fields.
I know that I can use a general state and store changes in it with an array but I read that it's bad practice.
this is what I have now:
const [title,updateTitle] = useState({"enable":false,"value": "" });
const [tags,updateTags] = useState({"enable":false,"value": "" });
const [owner,updateOwner] = useState({"enable":false,"value": "" });
const [desc,updateDesc] = useState({"enable":false,"value": "" });
And I try to use this to achieve the same thing:
if(props?.dataFields) {
Object.entries(props.dataFields).forEach ( ([key,value]) => {
// declare state fields
const [key,value] = useState(value)
});
}
what is the proper way of doing it? is there is one?
Do 4 lines of useState or useReducer (local)
I would suggest someting like this for the initial state
const setItem = (enable = false, value = '') => ({ enable, value });
const [title, updateTitle] = useState(setItem());
const [tags, updateTags] = useState(setItem());
const [owner, updateOwner] = useState(setItem());
const [desc, updateDesc] = useState(setItem());
And you also can useReducer and define the initial state.
I add an example for useReducer and case dor change title.value
import React from 'react';
import { useReducer } from 'react';
const setItem = (enable = false, value = '') => ({ enable, value });
const initialState = { title: setItem(), tags: setItem(), owner: setItem(), desc: setItem() };
function reducer(state, action) {
switch (action.type) {
case 'CHANGE_TITLE':
return { ...state, title: setItem(null, action.payload) };
default:
return state;
}
}
function MyFirstUseReducer() {
const [state, dispatch] = useReducer(reducer, initialState);
const updateTitle = ev => {
if (ev.which !== 13 || ev.target.value === '') return;
dispatch({ type: 'CHANGE_TITLE', payload: ev.target.value });
ev.target.value = '';
};
return (
<>
<h2>Using Reducer</h2>
<input type="text" onKeyUp={updateTitle} placeholder="Change Title" />
<div>
<span>The State Title is: <strong>{state.title.value}</strong></span>
</div>
</>
);
}
export default MyFirstUseReducer;
I am making a rock, paper, scissors game. In the code below, I have a context file that is used to store the global state. I also am showing my choices component. When a user clicks on a button in the choices component, the setChoices method is called which should set the user choice and cpu choice variables in the global state. Then, the cpuScore() method is ran afterwards to increment the cpu score (just to illustrate the problem). The cpu score updates as expected, but the choice variables are not updated. If I do not run the cpuScore method, the choice variables update as expected, but obviously not the score.
//context file
import React, { createContext, useState } from 'react';
const GameContext = createContext();
const GameContextProvider = props => {
const [gameItems, setGameItems] = useState(
{userChoice: null, userScore: 0, cpuChoice: null, cpuScore: 0}
);
const setChoices = (userChoice, cpuChoice) => {
setGameItems({...gameItems, userChoice: userChoice, cpuChoice: cpuChoice})
}
const cpuScore = () => {
setGameItems({...gameItems, cpuScore: gameItems.cpuScore + 1})
}
return (
<GameContext.Provider value={{gameItems, setChoices, cpuScore}}>
{ props.children }
</GameContext.Provider>
)
}
export default GameContextProvider;
//choices component
import React, { useContext } from 'react';
import { GameContext } from '../contexts/GameContext';
const Choices = (props) => {
const { setChoices, cpuScore } = useContext(GameContext);
const getCpuChoice = () => {
const cpuChoices = ['r', 'p', 's'];
const randomIndex = Math.floor((Math.random() * 3));
const cpuDecision = cpuChoices[randomIndex];
return cpuDecision
}
const playGame = (e) => {
const userChoice = e.target.id;
const cpuChoice = getCpuChoice();
setChoices(userChoice, cpuChoice);
cpuScore();
}
return (
<div>
<h1>Make Your Selection</h1>
<div className="choices">
<i className="choice fas fa-hand-paper fa-10x" id="p" onClick={playGame}></i>
<i className="choice fas fa-hand-rock fa-10x" id="r" onClick={playGame}></i>
<i className="choice fas fa-hand-scissors fa-10x" id='s' onClick={playGame}></i>
</div>
</div>
)
What do I need to change to set the state for both choice and score?
Since calls to setState are asynchronous, your two calls to setState are interfering with each other, the later one is overwriting the earlier one.
You have a few options.
Separate your state so that the values don't affect each other:
const [choices, setChoices] = useState({ user: null, cpu: null });
const [scores, setScores] = useState({ user: 0, cpu: 0);
Or go even further and set each of the two choices and two scores as their own state value.
Keep all state in one object, but update it all at once:
const setChoices = (userChoice, cpuChoice) => {
const cpuScore = gameItems.cpuScore + 1;
setGameItems({
...gameItems,
userChoice,
cpuChoice,
cpuScore
});
}
Use useReducer:
const initialState = {
userChoice: null,
userScore: 0,
cpuChoice: null,
cpuScore: 0
}
const [gameItems, dispatch] = useReducer((state, action) => {
switch (action.type) {
case "UPDATE_CHOICES":
return {
...state,
userChoice: action.userChoice,
cpuChoice: action.cpuChoice
};
case 'UPDATE_CPU_SCORE':
return {
...state,
cpuScore: state.cpuScore + 1
}
default:
return state;
}
}, initialState);
const setChoices = (userChoice, cpuChoice) => {
dispatch({ type: 'UPDATE_CHOICES', userChoice, cpuChoice });
};
const cpuScore = () => {
dispatch({ type: 'UPDATE_CPU_SCORE'})
};
Basically, React doesn't immediately updates after you call setState.
If you call cpuScore() right after setChoices(), the cpuScore function you are calling is still the function from the current render, not after setChoices() update. Because of that, cpuScore() will set the state again, using the spread value of gameItems (which is still hasn't changed because the update from setChoices hasn't kicked in) which cause the changes by setChoices() to be overridden.
If React always immediately updates after every setState call, the performance would be atrocious. What React does is it will batch multiple setState calls into one update, so it doesn't update the DOM repeatedly.
const cpuScore = () => {
setGameItems({...gameItems, cpuScore: gameItems.cpuScore + 1})
}
My suggestion would be either to separate this two states, so they don't get overwritten by each other, or create a new function that handle all this updates in one place.