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.
Related
guys, I'm just trying to set some contact data in local storage and render on DOM but after reloading the page there is no contact data in localStorage to render in DOM!
I tried in other browsers but the result is equal.
here is my code:
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import ContactForm from "./ContactForm";
import ContactList from "./ContactList";
const ContactApp = () => {
const [contacts, setContacts] = useState([]);
const addContactHandler = (formValues) => {
if (!formValues.name || !formValues.email) {
toast.error("Name and email are required");
return;
}
setContacts([...contacts, { ...formValues, id: Date.now() }]);
};
const deleteContactHandler = (id) => {
setContacts(contacts.filter((contact) => contact.id !== id));
};
useEffect(() => {
const savedContacts = JSON.parse(localStorage.getItem("contacts"));
if (savedContacts) setContacts(savedContacts);
}, []);
useEffect(() => {
localStorage.setItem("contacts", JSON.stringify(contacts));
}, [contacts]);
return (
<div className="contact-app">
<nav>
<h1>Contact Manager</h1>
</nav>
<ContactForm onClick={addContactHandler} />
<ContactList onDelete={deleteContactHandler} contacts={contacts} />
</div>
);
};
export default ContactApp;
Here is the explanation:
When you are calling the function to get the data from localstorage, you are also setting the contacts variable to the new data.
useEffect(() => {
const savedContacts = JSON.parse(localStorage.getItem("contacts"));
if (savedContacts) setContacts(savedContacts); // Here you are changing the value of 'contacts'
}, []);
BUT, You are also calling the localStorage.setItem method whenever the value of the contacts variable changes.
useEffect(() => {
localStorage.setItem("contacts", JSON.stringify(contacts));
}, [contacts]); // Here you are calling it when the contacts change.
So basically, when the component loads in the first useEffect. You are checking to see if contacts exist and setting the contacts variable to the existing contacts.
But you are also setting the value of the localStorage WHENEVER the value of the contacts variable changes.
its like:
Fetching Data the first time it loads and storing it --> because you have stored the data and the value of contacts has changed, it again sets the value of the contacts ----> which can lead to storing undefined or [] values if the contacts don't already have some default value.
Hope this helps.
I have a FilterContext provider and a hook useFilter in filtersContext.js:
import React, { useState, useEffect, useCallback } from 'react'
const FiltersContext = React.createContext({})
function FiltersProvider({ children }) {
const [filters, setFilters] = useState({})
return (
<FiltersContext.Provider
value={{
filters,
setFilters,
}}
>
{children}
</FiltersContext.Provider>
)
}
function useFilters(setPage) {
const context = React.useContext(FiltersContext)
if (context === undefined) {
throw new Error('useFilters must be used within a FiltersProvider')
}
const {
filters,
setFilters
} = context
useEffect(() => {
return () => {
console.log('reset the filters to an empty object')
setFilters({})
}
}, [setFilters])
{... do some additional stuff with filters if needed... not relevant }
return {
...context,
filtersForQuery: {
...filters
}
}
}
export { FiltersProvider, useFilters }
The App.js utilises the Provider as:
import React from 'react'
import { FiltersProvider } from '../filtersContext'
const App = React.memo(
({ children }) => {
...
...
return (
...
<FiltersProvider>
<RightSide flex={1} flexDirection={'column'}>
<Box flex={1}>
{children}
</Box>
</RightSide>
</FiltersProvider>
...
)
}
)
export default App
that is said, everything within FiltersProvider becomes the context of filters.
Now comes the problem description: I have selected on one page (Page1) the filter, but when I have to switch to another page (Page2), I need to flush the filters. This is done in the useFilters hook in the unmount using return in useEffect.
The problem is in the new page (Page2), during the first render I'm still getting the old values of filters, and than the GraphQL request is sent just after that. Afterwards the unmount of the hook happens and the second render of the new page (Page2) happens with set to empty object filters.
If anyone had a similar problem and had solved it?
first Page1.js:
const Page1 = () => {
....
const { filtersForQuery } = useFilters()
const { loading, error, data } = useQuery(GET_THINGS, {
variables: {
filter: filtersForQuery
}
})
....
}
second Page2.js:
const Page2 = () => {
....
const { filtersForQuery } = useFilters()
console.log('page 2')
const { loading, error, data } = useQuery(GET_THINGS, {
variables: {
filter: filtersForQuery
}
})
....
}
Printout after clicking from page 1 to page 2:
1. filters {isActive: {id: true}}
2. filters {isActive: {id: true}}
3. page 2
4. reset the filters to an empty object
5. 2 reset the filters to an empty object
6. filters {}
7. page 2
As I mentioned in the comment it might be related to the cache which I would assume you are using something like GraphQL Apollo. It has an option to disable cache for queries:
fetchPolicy: "no-cache",
By the way you can also do that reset process within the Page Two component if you want to:
const PageTwo = () => {
const context = useFilters();
useEffect(() => {
context.setFilters({});
}, [context]);
For those in struggle:
import React, { useState, useEffect, useCallback, **useRef** } from 'react'
const FiltersContext = React.createContext({})
function FiltersProvider({ children }) {
const [filters, setFilters] = useState({})
return (
<FiltersContext.Provider
value={{
filters,
setFilters,
}}
>
{children}
</FiltersContext.Provider>
)
}
function useFilters(setPage) {
const isInitialRender = useRef(true)
const context = React.useContext(FiltersContext)
if (context === undefined) {
throw new Error('useFilters must be used within a FiltersProvider')
}
const {
filters,
setFilters
} = context
useEffect(() => {
**isInitialRender.current = false**
return () => {
console.log('reset the filters to an empty object')
setFilters({})
}
}, [setFilters])
{... do some additional stuff with filters if needed... not relevant }
return {
...context,
filtersForQuery: { // <---- here the filtersForQuery is another variable than just filters. This I have omitted in the question. I will modify it.
**...(isInitialRender.current ? {} : filters)**
}
}
}
export { FiltersProvider, useFilters }
What is done here: set the useRef bool varialbe and set it to true, as long as it is true return always an empty object, as the first render happens and/or the setFilters function updates, set the isInitialRender.current to false. such that we return updated (not empty) filter object with the hook.
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>
I am sending data from Node JS to React JS in array object. In React JS when I am setting response data I am getting error "Objects are not valid as a React child (found: object with keys {eventName, eventDate, merchantid}). If you meant to render a collection of children, use an array instead."
I checked one of the Stackoverflow post useState Array of Objects. I am also setting value same way, but I am getting error.
Below data I am sending from Node JS.
[
{
eventName: 'Sankranti',
eventDate: 2021-01-21T00:00:00.000Z,
merchantid: 'tp012345'
},
{
eventName: 'Sankranti 1',
eventDate: 2021-01-26T00:00:00.000Z,
merchantid: 'tp012345'
}
]
Below screen shot I can see error and response data on the console.
Below my code, I am getting error at setEventList(eventList => [...eventList, response]). Based on comments I added below code.
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import Carousel from 'react-bootstrap/Carousel'
import axios from 'axios';
import DashboardNavBar from './DashboardNavBar';
import Header from './Header';
const DashboardPage = (props) => {
const [eventList, setEventList] = useState([])
const [index, setIndex] = useState()
if (!props.profileData) {
useEffect(() => {
(async () => {
const eventsList = await axios.get(
"http://localhost:3000/api/dashboard"
);
console.log(eventsList.data)
const response = eventsList.data
setEventList(eventList => [...eventList, response])
if(!response){
setErrorMsg('Please create Event and then add User !!')
}
})();
}, []);
}
const eventListRender = eventList.length > 0 ?
eventList.map((item,index) => {
console.log('item name: ', item[index].eventName)
return <Carousel.Item>{item[index].eventName}</Carousel.Item>
}) :
<Carousel.Item>No upcoming events</Carousel.Item>
const handleSelect = (selectedIndex) => {
console.log(eventList)
console.log(selectedIndex)
setIndex(selectedIndex)
}
return (
<div>
<DashboardNavBar />
<Header />
<p >Welcome !!!!!!</p>
<Carousel
activeIndex={index}
onSelect={handleSelect}
>
{eventListRender}
</Carousel>
</div>
);
}
const mapStateToProps = (state) => ({
profileData: state.auth.profileData
})
export default connect(mapStateToProps) (DashboardPage);
After adding below code it always reading first occurrence
const eventListRender = eventList.length > 0 ?
eventList.map((item,index) => {
console.log('item name: ', item[index].eventName)
return <Carousel.Item>{item[index].eventName}</Carousel.Item>
}) :
<Carousel.Item>No upcoming events</Carousel.Item>
Please find the updated results
Issue
Ok, your codesandbox confirms what I suspected. In your sandbox you've directly placed that mock response in your state as a flat array
const [eventList, setEventList] = useState([
{
eventName: "Sankranti",
eventDate: "2021-01-21T00:00:00.000Z",
merchantid: "tp012345"
},
{
eventName: "Sankranti 1",
eventDate: "2021-01-26T00:00:00.000Z",
merchantid: "tp012345"
}
]);
This allows the render to work as you expected, simply mapping over this flat array of objects.
eventList.map((item, index) => {
return <Carousel.Item>{item.eventName}</Carousel.Item>;
})
But in your original code you are not updating your state to be a flat array. The response is an array, i.e. [object1, object2] and you append this array to the end of your state's eventList array.
setEventList(eventList => [...eventList, response])
This updates your state to something like this [[object1, object2]], so the mapping function you used only maps one element.
eventList.map((item, index) => {
return <Carousel.Item>{item[index].eventName}</Carousel.Item>;
})
The reason is because you used the array index of the outer (recall eventList is an array of length 1) to access into the inner nested array (array of length 2). In iterating the outer array the index only reaches value 0, so only the zeroth element of the inner array is rendered.
See a more accurate reproduction of your issue in this code:
const response = [
{
eventName: "Sankranti",
eventDate: "2021-01-21T00:00:00.000Z",
merchantid: "tp012345"
},
{
eventName: "Sankranti 1",
eventDate: "2021-01-26T00:00:00.000Z",
merchantid: "tp012345"
}
];
function App() {
const [eventList, setEventList] = useState([]);
useEffect(() => {
setEventList((eventList) => [...eventList, response]);
}, []);
const eventListRender =
eventList.length > 0 ? (
eventList.map((item, index) => {
return <Carousel.Item>{item[index].eventName}</Carousel.Item>;
})
) : (
<Carousel.Item>No upcoming events</Carousel.Item>
);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Carousel>{eventListRender}</Carousel>
</div>
);
}
Solution
If the response data is also an array then it seems you should spread it into your eventList state array so it remains a nice, flat array.
Additionally, as pointed out by #Ashish, your useEffect hook usage is invalid and breaks the rules of hooks by being placed in a conditional block. The effect needs to be in the body of the component, so the condition should be tested in the effect callback. Refactor the anonymous async function to be a standard named function, and invoke in a conditional check within the effect callback.
useEffect(() => {
const getEvents = async () => {
const eventsList = await axios.get("http://localhost:3000/api/dashboard");
console.log(eventsList.data);
const response = eventsList.data;
setEventList((eventList) => [
...eventList, // <-- copy previous state
...response, // <-- spread array response
]);
if (!response) {
setErrorMsg("Please create Event and then add User !!");
}
};
if (!props.profileData) { // <-- check condition for fetching data
getEvents();
}
}, []);
const eventListRender =
eventList.length > 0 ? (
eventList.map((item, index) => {
return <Carousel.Item key={index}>{item.eventName}</Carousel.Item>;
})
) : (
<Carousel.Item>No upcoming events</Carousel.Item>
);
Demo with mocked axios data fetch.
Having a state that changes after a while (after fetching content), if I want to construct a variable and build a new state from that, when the first state changes it is not propagated to the second state
How can I solve this? I wouldn't like to merge the 2 states into one since this would mix components that do different things
If you have time you can take a look at the codesandbox below
https://codesandbox.io/s/purple-sun-mx428?fontsize=14&hidenavigation=1&theme=dark
And I paste here the code
import React, { useEffect, useState } from "react";
import "./styles.css";
const wait = ms => new Promise((r, j) => setTimeout(r, ms));
const test2 = () => {
const [test, setTest] = useState("hi");
useEffect(() => {
const testf = async stillMounted => {
await wait(1000);
setTest("bye");
};
const stillMounted = { value: true };
testf(stillMounted);
return () => {
stillMounted.value = false;
};
}, []);
return test;
};
export default function App() {
const here = test2();
const stru = [{ id: "whatever", content: here }];
const [elements, setElements] = useState(stru);
console.log(stru);
console.log(elements);
return (
<div className="App">
<h1>stru: {stru[0].content}</h1>
<h2>elements: {elements[0].content}</h2>
</div>
);
}
You can write an effect that runs when the here variable changes. Add something like this to your App component.
useEffect(() => {
// This runs when 'here' changes and then updates your other state
setElements([{ id: "whatever", content: here }])
}, [here]) // This makes this effect dependent on the 'here' variable