Updating state twice in a complex react logic - reactjs

So, I have a component like this which has quite complex logic in updateCT which I have simpler for the question
import { useState } from "react";
const App = () => {
const setFinalPerson = (key,value) => {
let updatedData = key
? {
[key]: value
}
: value;
setPerson((person) => ({
...person,
...updatedData
}));
};
const updateCT = (key, value) => {
const { info } = person;
const newInfo = {
...info,
[key]: value
};
setFinalPerson('info', newInfo);
};
const onClick = () => {
updateCT("age", "23");
updateCT("name", "Henry");
};
const [person, setPerson] = useState({
info: {
name: "Max",
age: "22"
},
bank: {
account: "22345333455",
balance: "7000"
}
});
return (
<div className="App">
<h1>{`Name ${person.info.name}`}</h1>
<h1>{`Age ${person.info.age}`}</h1>
<h1>{`Account Number ${person.bank.account}`}</h1>
<h1>{`Balance ${person.bank.balance}`}</h1>
<button onClick={onClick}>Click</button>
</div>
);
};
export default App;
So, when i click on the button, i want to change age and name of the person in info.
I know that first it is updating state for age, but when it updates state for name, it gets the older state with old value of age. Therefore, ultimately the age is not getting updated.
According to React Docs, I have to use functional way to update state, but the state object is too complex in real and I just cant use spread operators to that much nesting of object.
Is there any way, I can solve this problem?

If your true state is actually much more complex than you are showing, a potential solution is the useReducer hook instead. But in this case I fail to see why you would need that, you are essentially wanting to update name and age on the info key at the same time?
but the state object is too complex in real and I just cant use spread operators to that much nesting of object.
There's not really a limit to how much nesting you can have. Perhaps you can re-architect the data structure but it is pretty typical to use a good amount of Object spread to redefine state values. In conclusion you want all of your state changing to happen one time otherwise render will run when you change the state so you will re-overwrite it with the past info and you'll never get to change state twice.
Without seeing your true usecase I fail to see why useReducer would be necessary, but that's an option to consider and useEffect is also a possibility. You can set useEffect to change some state if that's dependent on a different state change. Such as: when the user's name changes, find their age:
const nameByAgeLookup = {
john: 23,
jack: 41,
jill: 12
}
const [name, setName] = useState('')
const [age, setAge] = useState(Infinity)
useEffect(() => {
setAge(nameByAgeLookup[name])
}, [name])
const { useState } = React;
const App = () => {
const [person, setPerson] = useState({
info: {
name: "Max",
age: "22"
},
bank: {
account: "22345333455",
balance: "7000"
}
});
const setFinalPerson = (key,value) => {
let updatedData = key
? {
[key]: value
}
: value;
setPerson((person) => ({
...person,
...updatedData
}));
};
const updateCT = (newInfo) => {
const { info } = person;
const merged = {
...info,
...newInfo
};
setFinalPerson('info', merged);
};
const onClick = () => {
updateCT({age: "23", name: "Henry" });
};
return (
<div className="App">
<h1>{`Name ${person.info.name}`}</h1>
<h1>{`Age ${person.info.age}`}</h1>
<h1>{`Account Number ${person.bank.account}`}</h1>
<h1>{`Balance ${person.bank.balance}`}</h1>
<button onClick={onClick}>Click</button>
</div>
);
};
ReactDOM.render(
<App />,
window.root
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id='root'></div>

Related

Invalid custom hook call

I'm just a react beginner. I'm trying to create a custom hook, which will be triggered once an onClick event is triggered. By what I see, I need to use the useRef hook, to take into account if the component is rendered by first time, or if it's being re-rendered.
My code approach is the next:
const Clear = (value) => {
const useClearHook = () => {
const stateRef = useRef(value.value.state);
console.log(stateRef);
useEffect(() => {
console.log("useEffect: ");
stateRef.current = value.value.state;
stateRef.current.result = [""];
stateRef.current.secondNumber = [""];
stateRef.current.mathOp = "";
console.log(stateRef.current);
value.value.setState({
...stateRef.current,
result: value.value.state.result,
secondNumber: value.value.state.secondNumber,
mathOp: value.value.state.mathOp,
});
}, [stateRef.current]);
console.log(value.value.state);
};
return <button onClick={useClearHook}>Clear</button>;
};
Any suggestion? Maybe I might not call ...stateRef.current in setState. I'm not sure about my mistake.
Any help will be appreciated.
Thanks!
Your problem is useClearHook is not a component (the component always goes with the first capitalized letter like UseClearHook), so that's why when you call useRef in a non-component, it will throw that error. Similarly, for useEffect, you need to put it under a proper component.
The way you're using state is also not correct, you need to call useState instead
Here is a possible fix for you
const Clear = (value) => {
const [clearState, setClearState] = useState()
const useClearHook = () => {
setClearState((prevState) => ({
...prevState,
result: [""],
secondNumber: [""],
mathOp: "",
}));
};
return <button onClick={useClearHook}>Clear</button>;
};
If your states on the upper component (outside of Clear). You can try this way too
const Clear = ({value, setValue}) => {
const useClearHook = () => {
setValue((prevState) => ({
...prevState,
result: [""],
secondNumber: [""],
mathOp: "",
}));
};
return <button onClick={useClearHook}>Clear</button>;
};
Here is how we pass it
<Clear value={value} setValue={setValue} />
The declaration for setValue and value can be like this in the upper component
const [value, setValue] = useState()

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

React why the state is not updating when calling a function to initialize it?

Playing with React those days. I know that calling setState in async. But setting an initial value like that :
const [data, setData] = useState(mapData(props.data))
should'nt it be updated directly ?
Bellow a codesandbox to illustrate my current issue and here the code :
import React, { useState } from "react";
const data = [{ id: "LION", label: "Lion" }, { id: "MOUSE", label: "Mouse" }];
const mapData = updatedData => {
const mappedData = {};
updatedData.forEach(element => (mappedData[element.id] = element));
return mappedData;
};
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
export default function App() {
const [loadedData, setLoadedData] = useState(data);
const [filter, setFilter] = useState("");
const filterData = () => {
return loadedData.filter(element =>
filter ? element.id === filter : true
);
};
//loaded comes from a useEffect http call but for easier understanding I removed it
return (
<div className="App">
<button onClick={() => setFilter("LION")}>change filter state</button>
<ChildComponent dataProp={filterData()} />
</div>
);
}
So in my understanding, when I click on the button I call setFilter so App should rerender and so ChildComponent with the new filtered data.
I could see it is re-rendering and mapData(updatedData) returns the correct filtered data BUT ChildComponent keeps the old state data.
Why is that ? Also for some reason it's rerendering two times ?
I know that I could make use of useEffect(() => setMappedData(mapData(dataProp)), [dataProp]) but I would like to understand what's happening here.
EDIT: I simplified a lot the code, but mappedData in ChildComponent must be in the state because it is updated at some point by users actions in my real use case
https://codesandbox.io/s/beautiful-mestorf-kpe8c?file=/src/App.js
The useState hook gets its argument on the very first initialization. So when the function is called again, the hook yields always the original set.
By the way, you do not need a state there:
const ChildComponent = ({ dataProp }) => {
//const [mappedData, setMappedData] = useState(mapData(dataProp));
const mappedData = mapData(dataProp);
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
EDIT: this is a modified version in order to keep the useState you said to need. I don't like this code so much, though! :(
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
let actualMappedData = mappedData;
useMemo(() => {
actualMappedData =mapData(dataProp);
},
[dataProp]
)
console.log("** Render Child Component **");
return Object.values(actualMappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
Your child component is storing the mappedData in state but it never get changed.
you could just use a regular variable instead of using state here:
const ChildComponent = ({ dataProp }) => {
const mappedData = mapData(dataProp);
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};

useEffect is not running for every change in dependency array

When I update an expense, it doesn't trigger the useEffect to store the the data in local storage.
codesandbox
import React, { useState, useEffect } from 'react';
export const ExpenseContext = React.createContext();
const ExpenseState = (props) => {
const [state, setState] = useState(
() => JSON.parse(localStorage.getItem('expenses')) || []
);
useEffect(() => {
console.log('didUpdate', state)
state.length !== 0 && localStorage.setItem('expenses', JSON.stringify(state));
}, [state])
function addExpense({id, name, category, value, note, date}){
const expense = state.find(exp => exp.id === id);
if(!expense){
setState(state => ([...state, {id, name, category, value, note, date}]))
}else{
const updatedExpense = state.map(exp => {
if(exp.id === expense.id){
return {
...exp,
...{id, name, category, value, note, date}
}
}else{
return exp
}
})
setState(updatedExpense)
}
console.log(state)
}
return (
<ExpenseContext.Provider
value={{
expenses: state,
addExpense: addExpense,
}}
>
{props.children}
</ExpenseContext.Provider>
)
}
I am calling the addExpense for every elements of the array. Assume we have 4 expenses stored in storage. Now we have updated one expense and then running the below code.
for(const expense of expenses){
expenseContext.addExpense(expense);
}
Now the use effect only get triggered for the last element and the expense also not getting updated in context.
You code is working, I'm not sure if you just missed some stuff?
1. export default ExpenseState is missing?
2. Wrap your App with ExpenseState context
Assuming you have a default CRA setup:
// index.js
import React from "react"
import ReactDOM from "react-dom"
import ExpenseState from "./AppContext" // or where your Context
import App from "./App"
const rootElement = document.getElementById("root")
ReactDOM.render(
<ExpenseState>
<App />
</ExpenseState>,
rootElement
)
And your App.js
// App.js
import React, { useContext } from "react"
import { ExpenseContext } from "./AppContext"
export default () => {
const { expenses, addExpense } = useContext(ExpenseContext)
const handleExpense = () => {
addExpense({
id: 1, // Change ID to add another object
name: "some name",
category: "some category", // keep the id and change some values for update
value: "some value",
note: "some note",
date: "some date"
})
}
return (
<div className="App">
<button onClick={handleExpense}>Add Expenses</button>
<pre>
<code>{JSON.stringify(expenses, null, 2)}</code>
</pre>
</div>
)
}
Working codeSandBox example.
The problem is that useState is working differently than you think it does. Inside the consumer you're calling api.addExpense(...) three times in a row synchronously with the expectation that calling setState will update state in-place - it doesn't, the state is only updated on the next render call and not at line 40 as your console.log there suggests you're expecting it to.
So instead of the three complete state updates you're expecting (one for each expense) you only get the state from the last update for {id: "d6109eb5-9d7b-4cd3-8daa-ee485b08361b", name: "book", category: "personal care", value: 200}, which has no changes and is still based on the original state not one where the first item's value has been set to 800.
I'd suggest modifying your ExpenseState api to take an array of expenses so it can handle multiple internal updates appropriately.
function addOrUpdateExpense(state, { id, name, category, value }) {
const expense = state.find(exp => exp.id === id);
if (!expense) {
return [...state, { id, name, category, value }];
} else {
const updatedExpense = state.map(exp => {
if (exp.id === expense.id) {
return {
...exp,
...{ id, name, category, value }
};
} else {
return exp;
}
});
return updatedExpense;
}
}
function addExpenses(expenses) {
let newState = [...state];
for (const expense of expenses) {
newState = addOrUpdateExpense(newState, expense);
}
setState(newState);
}
// ...usage
function doIt() {
api.addExpenses(expenses);
}
This is a common misconception, just remember that the next state is only available asynchronously in the next render phase, treat state from useState as immutable within a render pass.

React hooks, declaring multiple state values from prop

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;

Resources