How to pass object value as key of setState in react - javascript

I want to pass object value dynamically as key of setState.
useEffect(() => {
inputFields.forEach((item) => {
return setState({ ...state, [item.name]: "" });
});
}, [])

You can clone, assign and then setstate
useEffect(() => {
const sItem = {...state };
inputFields.forEach((item) => {
sItem[item.name] = "";
});
setState(sItem)
}, [])

Related

How to map an array of objects in React useReducer function

I have an array of objects in my React state. I want to be able to map through them, find the one I need to update and update its value field. The body of my request being sent to the server should look like:
{ name: "nameOfInput", value:"theUserSetValue" type: "typeOfInput" }
What I thought would be simple is causing me some heartache. My reducer function calls, and I hit the "I AM RUNNING" log where it then jumps over my map and simply returns my state (which is empty). Please note that I NEVER see the "I SHOULD RETURN SOMETHING BUT I DONT" log.
NOTE: I have learned that I could be simply handingling this with useState
function Form(props) {
const title = props.title;
const paragraph = props.paragraph;
const formBlocks = props.blocks.formBlocks
const submitEndpoint = props.blocks.submitEndpoint || "";
const action = props.blocks.action || "POST";
const formReducer = (state, e) => {
console.log("I AM RUNNING")
state.map((obj) => {
console.log("I SHOULD RETURN SOMETHING BUT I DONT")
if (obj.name === e.target.name) {
console.log("OBJ EXISTS", obj)
return {...obj, [e.target.name]:obj.value}
} else {
console.log("NO MATCH", obj)
return obj
}
});
return state
}
const [formData, setFormData] = useReducer(formReducer, []);
const [isSubmitting, setIsSubmitting] = useState(false);
=====================================================================
Where I am calling my reducer from:
<div className="form-block-wrapper">
{formBlocks.map((block, i) => {
return <FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={setFormData}
/>
})}
</div>
Issues
When using the useReducer hook you should dispatch actions to effect changes to the state. The reducer function should handle the different cases. From what I see of the code snippet it's not clear if you even need to use the useReducer hook.
When mapping an array not only do you need to return a value for each iterated element, but you also need to return the new array.
Solution
Using useReducer
const formReducer = (state, action) => {
switch(action.type) {
case "UPDATE":
const { name, value } = action.payload;
return state.map((obj) => obj.name === name
? { ...obj, [name]: value }
: obj
);
default:
return state;
}
};
...
const [formData, dispatch] = useReducer(formReducer, []);
...
{formBlocks.map((block, i) => {
return (
<FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={e => dispatch({
type: "UPDATE",
payload: {...e.target}
})}
/>
);
})}
Using useState
const [formData, setFormData] = useState([]);
...
const changeHandler = e => {
const { name, value } = e.target;
setFormData(data => data.map(obj => obj.name === name
? { ...obj, [name]: value }
: obj
));
};
...
{formBlocks.map((block, i) => {
return (
<FormBlock
key={block.title + i}
title={block.title}
paragraph={block.paragraph}
inputs={block.inputs}
buttons={block.buttonRow}
changeHandler={changeHandler}
/>
);
})}
I have come to understand my problem much better now and I'll update my question to reflect this.
As the user interacted with an input I needed to figure out if they had interacted with it before
If they did interact with it before, I needed to find that interaction in the state[] and update the value as required
If they didn't I needed to add an entirely new object to my forms state[]
I wrote two new functions, an AddObjectToArray function and an UpdateObjectInArray function to serve these purposes.
const handleFormInputChange = (e) => {
const { name, value, type } = e.target;
const addObjectToArray = (obj) => {
console.log("OBJECT TO BE ADDED TO ARRAY:", obj)
setFormData(currentArray => ([...currentArray, obj]))
}
const updateObjectInArray = () => {
const updatedObject = formData.map(obj => {
if (obj.name === name) {
//If the name matches, Update the value of the input
return ({...obj, value:value})
}
else {
//if no match just return the object as is
return obj
}
})
setFormData(updatedObject)
}
//Check if the user has already interacted with this input
if (formData.find(input => input.name === name)) {
updateObjectInArray()
}
else {
addObjectToArray({name, value, type})
}
}
I could get more complicated with this now and begin to write custom hooks that take a setState function as a callback and the data to be handled.

Zustand state with javascript Map is not updating

I am trying to use map to store state, while the state is updating, the components are not re-rendering with the new state
const store = (set, get) => ({
items: new Map(),
addItem: (key, item) => {
set((state) => state.items.set(key, item));
// this prints the latest state
console.log(get().items)
},
removeItem: (key) => {
set((state) => state.items.delete(key));
},
)}
const useStore = create(store);
export default useStore
useEffect doesn't prints anything
const items = useStore((state) => state.items))
useEffect(() => {
console.log(items)
}, [items]);
You should create a new instance of Map when you update like so:
const store = (set, get) => ({
items: new Map(),
addItem: (key, item) => {
set((state) => {
const updatedItems = new Map(state.items)
updatedItems.set(key, item)
return { items: updatedItems }
});
// this prints the latest state
console.log(get().items)
},
)}
Note: you'll have to do something similar when you remove items.
The Map instance is always the same. So Zustand cannot detect any changes. You can use an array:
const store = (set, get) => ({
items: [],
addItem: (key, item) => {
set((state) => ({
items: [...state.items, { key, item }]
}));
},
removeItem: (key) => {
set((state) => ({
items: state.items.filter((item) => item.key !== key)
}));
}
});
If you don't want to create a new instance of a map so that zustand registers state change then change you can use immer.
import create from 'zustand'
import { immer } from 'zustand/middleware/immer'
const store =immer( (set, get) => ({
items: new Map(),
addItem: (key, item) => {
set((state) => state.items.set(key, item));
// this prints the latest state
console.log(get().items)
},
removeItem: (key) => {
set((state) => state.items.delete(key));
}),
)}
const useStore = create(store);
export default useStore

Component keeps re-rendering after state change

I use handleMouseOver and handleMouseOut functions where I change the value of a state count. However, every time the state is changed the component re-renders instead of just the state. What am I missing here? Thanks.
function foo() {
const [state, setState] = useState({ count: 0, data: {}});
useEffect(() => {
const getData = async () => {
const response = await fetch(url);
const data = await response.json();
setState(prevState => ({
...prevState,
data: data,
}));
};
return ()=>{
getData();
}
}, []);
function handleMouseOver(e) {
setState(prevState => ({
...prevState,
count: e,
}));
};
function handleMouseLeave() {
setState(prevState => ({
...prevState,
count: null,
}));
};
const { count, data } = state;
const BlockComponent = () => {
const data = data.arr;
return (
<Wrapper >
{data.map((value, index) =>
value.map((value, index) => {
return (
<Block
key={index}
val={value}
onMouseEnter={e => handleMouseOver(value)}
onMouseOut={handleMouseLeave}
></Block>
);
})
)}
</Wrapper>
);
};
return (
<Wrapper>
<BlockComponent />
</Wrapper>
);
}
export default foo;
The Issue is with your handleMouseOver function. It is getting executed everytime there is a state Update and the same value is assigned to "count".
All you have to do is place setState inside the condition that will compare the value of event received by the function and the current value of sate.
It should be something like this.
function handleMouseOver(e) {
if (count !== e) {
setState((prevState) => ({
...prevState,
count: e,
}));
}
}
React updates component if component state is changed. That's correct behaviour.
I recommend you to learn react documentation, because component state is a basic concept.
That's one of the main points of state -> component is rerendered when you change state.

useEffect infinite loop asynchronous

const [allCases, setAllCases] = useState([])
const [myCases, setMyCases] = useState([])
const [sharedCases, setSharedCases] = useState([])
const [favoriteCases, setFavoriteCases] = useState([])
useEffect(()=> {
getData(_id).then(res => {
const favoriteIds = res.data.find(i => i._id === _id).cases.map(x => {
return x._id
})
setAllCases(res.cases.map(x => {
if (favoriteIds.includes(x._id)) {
return { ...x, isfavorite: true }
}
return { ...x, isfavorite: false }
}))
allCases && setSharedCases(allCases.filter(x => x.creator._id !== user._id))
allCases && setMyCases(allCases.filter(x => x.creator._id === user._id))
allCases && setFavoriteCases(allCases.filter(x => x.isfavorite))
})
}, [])
Hello, I am having problems with infinite loop. If I include brackets, my filtered cases don't populate. If i add another useEffect, I run into an infinite loop even though I am not changing allCases. (wondering why)
How can i resolve this asynchronous issue?
One simple way to bypass this issue would be to call your other useState setters with a local variable instead of the state. Then you can update the allCases state with the same variable, and not have to work around async functions.
useEffect(()=> {
getData(_id).then(res => {
const favoriteIds = res.data.find(i => i._id === _id).cases.map(x => {
return x._id
})
// Save it to a local variable so that we have the value immediately
const newAllCases = res.cases.map(x => {
if (favoriteIds.includes(x._id)) {
return { ...x, isfavorite: true }
}
return { ...x, isfavorite: false }
})
// Use the local variable to update state
setSharedCases(newAllCases.filter(x => x.creator._id !== user._id))
setMyCases(newAllCases.filter(x => x.creator._id === user._id))
setFavoriteCases(newAllCases.filter(x => x.isfavorite))
// Set the state for allCases
setAllCases(newAllCases)
})
}, [])
allCases within useEffect is not set before you are setting sharedCases and others. Also, with [] in useEffect it would only call the function on load of component. So your other state values would not get set.
I would suggest to have a separate useEffect with [allCases] and set all other values there. Something like below
useEffect(()=> {
getData(_id).then(res => {
const favoriteIds = res.data.find(i => i._id === _id).cases.map(x => {
return x._id
})
setAllCases(res.cases.map(x => {
if (favoriteIds.includes(x._id)) {
return { ...x, isfavorite: true }
}
return { ...x, isfavorite: false }
}))
})
}, [])
useEffect(() => {
// do filter and set the state
}, ['allCases'])

React hooks useState to set multiple states in context provider

I have a createContext component that is using useState to set multiple values returned from a fetch function. However in my code below, when a state is updated, the others states return to the original value.
For example, in getCountryCode() the state is updated for countryCode, but then iconCode in weatherInit() fetches its value and countryCode returns to the original US.
import React, { createContext, useState, useEffect } from 'react';
export const GlobalConsumer = createContext();
export const GlobalProvider = ({ children }) => {
const [state, setState] = useState({
menuPanel: false,
countryCode: 'US',
weatherLoading: true,
iconCode: '',
fahrenheit: '',
celcius: '',
showCelcius: false
});
const getCountryCode = () => {
const url = `https://ipapi.co/json/`;
fetch(url)
.then(data => data.json())
.then(data => {
const countryCode = data.country;
setState({ ...state, countryCode });
});
};
const weatherInit = () => {
const CITY_LAT = '...';
const CITY_LON = '...';
const OW_KEY = '...';
const url = `//api.openweathermap.org/data/2.5/weather?lat=${CITY_LAT}&lon=${CITY_LON}&units=imperial&appid=${OW_KEY}`;
fetch(url)
.then(data => data.json())
.then(data => {
const iconCode = data.weather[0].id;
setState({ ...state, iconCode });
const fahrenheit = Math.round(data.main.temp_max);
setState({ ...state, fahrenheit });
const celcius = Math.round((5.0 / 9.0) * (fahrenheit - 32.0));
setState({ ...state, celcius });
setTimeout(() => {
setState({ ...state, weatherLoading: false });
}, 150);
});
};
useEffect(() => {
getCountryCode();
weatherInit();
}, []);
return (
<GlobalConsumer.Provider
value={{
contextData: state,
togglemMenuPanel: () => {
setState({ ...state, menuPanel: !state.menuPanel });
},
toggleCelcius: () => {
setState({ ...state, showCelcius: !state.showCelcius });
}
}}
>
{children}
</GlobalConsumer.Provider>
);
};
I believe that this is caused because each value requires it's own useState. However, can these values be merged or is there another way to achieve this outcome, where I am only required to pass as data to the Provider context?
It's because you are using the old value of state when calling setState(). As documented here (Scroll down to the "Note"-block) you have to pass a function to your setState call:
const iconCode = data.weather[0].id;
setState(prevState => ({ ...prevState, iconCode }));
const fahrenheit = Math.round(data.main.temp_max);
setState(prevState => ({ ...prevState, fahrenheit }));
const celcius = Math.round((5.0 / 9.0) * (fahrenheit - 32.0));
setState(prevState => ({ ...prevState, celcius }));
setTimeout(() => {
setState(prevState => ({ ...prevState, weatherLoading: false }));
}, 150);
Unlike the setState method found in class components, useState does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});

Categories