ReactJS best way to update the state with asynchronous setState - javascript

I started working with reactjs recently and I know setState() is asynchronous, but if I have the following code below:
// the state is an object
const [addressState, setAddress] = useState({
adrStNumber: null,
adrStreet: null,
adrCity: null,
adrState: null,
adrZipcode: null,
adrCountry: null
})
// this function is called when I select an option in the input
function getAddressAndSaveToState(address) {
address.map(adr => {
if (adr.types.includes('street_number')) {
const adrStNumber = adr.long_name
setAddress({ ...addressState, adrStNumber: adrStNumber })
}
if (adr.types.includes('route')) {
const adrStreet = adr.long_name
setAddress({ ...addressState, adrStreet: adrStreet })
}
if (adr.types.includes('administrative_area_level_1')) {
const adrState = adr.long_name
setAddress({ ...addressState, adrState: adrState })
}
})
}
Great, after the first if, we have our state like this:
{
adrCity: null
adrCountry: null
adrStNumber: "65"
adrState: null
adrStreet: null
adrZipcode: null
}
But because setState is asynchronous, the second if, the ...addressState is still with its initial value (adrStNumber from the last iteration is null), so after the iteration the state will look like this:
{
adrCity: null
adrCountry: null
adrStNumber: null
adrState: "New York"
adrStreet: null
adrZipcode: null
}
In this example, everytime there is a if iteration, I need to add something to my state object. What is the best way to implement something like this?

You can buld your object inside your function's body and assign it to the state after all if are checked. Something like below:
// this function is called when I select an option in the input
function getAddressAndSaveToState(address) {
address.map(adr => {
let newAddress = {...addressState}
if (adr.types.includes('street_number')) {
const adrStNumber = adr.long_name
newAddress.adrStNumber = adrStNumber
}
if (adr.types.includes('route')) {
const adrStreet = adr.long_name
newAddress.adrStreet = adrStreet
}
if (adr.types.includes('administrative_area_level_1')) {
const adrState = adr.long_name
newAddress.adrState = adrState
}
setAddresss(newAddress)
})

You can try this:
function getAddressAndSaveToState(address) {
address.map(adr => {
const newAddressState = {};
if (adr.types.includes('street_number')) {
const adrStNumber = adr.long_name
newAddressState.adrStNumber = adrStNumber;
}
if (adr.types.includes('route')) {
const adrStreet = adr.long_name
newAddressState.adrStreet = adrStreet;
}
if (adr.types.includes('administrative_area_level_1')) {
const adrState = adr.long_name
newAddressState.adrState = adrState;
}
setAddress({ ...addressState, ...newAddressState });
})
}

Related

Cannot set properties of undefined when using eval but works without eval

I am trying to use eval() to dynamically update a variable that I have to access by path like myArray[0][0][1][0]... But for some reason it is giving me this error:
Uncaught TypeError: Cannot set properties of undefined (setting '$folded')
If I do it without eval like this productCategoriesTree[1].$folded = false - then it works.
I am using Vue 3 so perhaps it might be related to Vue somehow.
addNewProductCategory() function is the problem:
// useProductCategoriesTree.ts
import { v4 as uuidv4 } from 'uuid'
import TreeItem from '#/types/TreeItem'
import { try as tryCatch } from 'radash'
import { getCurrentInstance } from 'vue'
import { ProductCategory, ProductCategoryTreeInput } from '#/types/graphql/graphql'
import productCategoryApi, { FRAGMENT_PRODUCT_CATEGORY_TREE } from '#/api/productCategoryApi'
export function useProductCategoriesTree() {
let productCategoriesTree = $ref<TreeItem<ProductCategory>[]>([])
const { emit } = getCurrentInstance() as any
const getProductCategoriesTree = async () => {
const [error, productCategories] = await tryCatch(productCategoryApi.getProductCategoryList)(FRAGMENT_PRODUCT_CATEGORY_TREE)
productCategoriesTree = buildProductCategoriesTree(productCategories)
}
const buildProductCategoriesTree = (productCategories: ProductCategory[]): TreeItem<ProductCategory>[] => {
return productCategories.map(productCategory => ({
data: productCategory,
children: (productCategory.children && productCategory.children.length)
? buildProductCategoriesTree(productCategory.children)
: undefined
}))
}
const selectProductCategory = (productCategoryUuid: string) => {
emit('select', productCategoryUuid)
}
// treePath could be [0, 1, 0, 0, 1] - it is dynamic
const addNewProductCategory = (parentCategoryUuid: string, treePath: number[]) => {
const newProductCategory = {
uuid: `__NEW-${uuidv4()}`,
namePlural: 'new',
parent: { uuid: parentCategoryUuid }
}
if (!productCategoriesTree[1].children) {
productCategoriesTree[1].children = []
}
productCategoriesTree[1].children?.push({
data: newProductCategory as ProductCategory,
children: [],
})
console.log(`productCategoriesTree[${treePath.join('][')}].$folded = false`)
// productCategoriesTree[1].$folded = false
// this works
productCategoriesTree[1].$folded = false
// this does not work
eval(`productCategoriesTree[${treePath.join('][')}].$folded = false`)
}
getProductCategoriesTree()
return $$({
productCategoriesTree,
selectProductCategory,
addNewProductCategory,
})
}
I'd try any other approach that didn't use eval. For example:
let thingToUpdate = productCategoriesTree;
treePath.forEach(el => thingToUpdate = thingToUpdate[el]);
thingToUpdate.$folded = false;
Similar to this question: Javascript: Get deep value from object by passing path to it as string

setState does not update array state in React Js

I try to get value from the input and put it into object then push to new array. Then combine this array(with setState) with my state array. However, my state array value always returns empty. Could you please say where is the mistake. Thanks.
class AddExtraSeassion extends Component {
constructor(props) {
super(props);
this.checkValidity = this.checkValidity.bind(this);
this.onChangeDate = this.onChangeDate.bind(this);
this.onChangeTime = this.onChangeTime.bind(this);
this.onChangeDuration = this.onChangeDuration.bind(this);
this.state = {
checkedStudentLength: 0,
validity: false,
seassionDuration: null,
seassionDate: null,
seassionTime: null,
showSeassion: false,
seassions: []
}
}
addSeassion = () => {
this.setState({showSeassion: true});
}
onChangeDate = (event) => {
this.setState({seassionDate: event.target.value});
};
onChangeTime = (event) => {
this.setState({seassionTime: event.target.value});
};
onChangeDuration = (event) => {
this.setState({seassionDuration: event.target.value});
};
checkValidity() {
const seassionDate = this.state.seassionDate;
const seassionTime = this.state.seassionTime;
const seassionDuration = this.state.seassionDuration;
const obj = {
"Date": seassionDate,
"Time": seassionTime,
"Duration": seassionDuration
};
let SeassionList=[];
SeassionList.push(obj);
console.log(JSON.stringify(SeassionList) + " SeassionList array"); // print result
this.setState({
seassions: [...this.state.seassions, SeassionList]
})
console.log(JSON.stringify(this.state.seassions) + " state array"); // always empty
As #norbitrial mentioned setState is async operation so console.log() will not show the updated state . If you want to check if the state is updating or not you can use the callback provided by the setState method like below:
this.setState(prevState => ({
seassions: [...prevState.seassions, SeassionList[0]]
}),() => {
console.log(this.state.seassions);
})
The first thing is you should not expect to see the changes in that line of console.log() immediately after your usage of setState() because it is asynchronous action.
Also you can try using the previous state of you array and passing obj instead of SeassionList:
this.setState(prevState => ({
...prevState,
seassions: [...prevState.seassions, obj]
}))

How to properly reset Vue Composition Api's reactive values

I'm wondering how should I reset a reactive in vuejs setup? (i know if change it to the ref and using view.value will solve this problem, but there should be an answer to this for using reactive)
setup(props, context){
// states
const DataTable = reactive((null as unknown) as DataTable);
const PolicyForm = reactive((null as unknown) as PolicyForm);
let view = reactive(resetViewState());
let config = reactive(
(resetPreRegisterConfig() as unknown) as PreRegisterConfig
);
// methods:
const fetchProfilelist = (
pagination: Pagination{ page:1, size:15},
sort_label: string = ""
) => {
DataTable.fetchTablelist(api_fetchProfilelist, pagination, sort_label);
};
const pageRefresh = () => {
view = resetViewState(); // 👈
config = resetPreRegisterConfig();
fetchProfilelist();
};
return {
DataTable,
PolicyForm,
view,
config,
fetchProfilelist,
pageRefresh
}
You can use Object.assign:
setup() {
const initialState = {
name: "",
lastName: "",
email: ""
};
const form = reactive({ ...initialState });
function resetForm() {
Object.assign(form, initialState);
}
function setForm() {
Object.assign(form, {
name: "John",
lastName: "Doe",
email: "john#doe.com"
});
}
return { form, setForm, resetForm };
}
See it live on codesandbox
credits: taken from here
Object.assign didn't work for me. (Maybe because I used a shim for the Composition API in Nuxtjs 2?) For anybody that run into the same problem: I had to use a compact loop.
setup() {
const initialState = {
name: "",
lastName: "",
email: ""
};
const form = reactive({ ...initialState });
function resetForm() {
for (const [key, value] of Object.entries(initialState)) {
form[key] = value
}
}
function setForm(values = {name: "John", lastName: "Doe", email: "john#doe.com"}) {
// only loop with the supported keys of initial state
for (const key of Object.keys(initialState)) {
form[key] = values[key]
}
}
return { form, setForm, resetForm };
}
Citing from the official Vueland Discord server:
"For what I know, reactive is the old way we use to do reactivity from the Classic API, so to reset the values should be something like:"
const myData = reactive({
foo: true,
bar: ''
})
function resetValues () {
myData.foo = true
myData.bar = ''
}
Therefore, if you don't change properties you should be able to use Object.assign(). (Correct me if I'm wrong)
How about use ref instead of reactive?
const myData = ref({ xxx: 11 })
// ... After many operations on myData
// resetData
myData.value = { xxx: 11 }
The disadvantage is that you need to add .value when using it in script.
But this is the same as reactive in vue template.
If you have deep objects you can use this trick to reset the values converting them to JSON
setup(){
const form = ref({
order: '',
user:{},
});
const defaultForm = JSON.parse(JSON.stringify(form));
form.value.user = {firstname:"Joe", lastname:"Doe"};
const onSubmit = () => {
// Reset values to default
form.value = JSON.parse(JSON.stringify(defaultForm));
}
}
If you do not want to use ref and want to reset value of a reactive variable then you have to take one key variable for reference in the reactive variable, because reinitializing reactive variable directly will cause to loose reactivity in the process.
So to reset the value use extra key variable in reactive variable, see code as below
setup() {
const initialState = {
name: "",
email: ""
};
const form = reactive({ data: ...initialState });
function resetForm() {
form.data = { ...initialState };
}
function setForm() {
form.data = {
name: "Bahubali",
email: "bahubali#mahismati.com"
});
}
return { form, setForm, resetForm };
}
So basically you are taking data as key of your reactive variable and when you want to reset or set your values you just need to change the form.data and it will be reactive and refreshes the elements which uses form.data.

setState() in Reactjs updating multiple properties instead of one

When I update 'classProficienciesChoices' in my state using setState() it is updating not only that property, but also where I derived the 'classProficienciesChoices' info from in the 'classSelected' property, AND ALSO from where I derived the classSelected info from in the 'classesInfo' property.
The same function I update 'classProficienciesChoices' I also update 'classProficiencies', and it updates properly in the one property I tell it to, and not the elements where the information was derived from.
Any insight would be helpful. The Create component has other components nested and none of them have state and only use props passed. There are navigation, selection, and information display components nested.
import React, { Component } from 'react'
import Create from './Create'
class App extends Component {
constructor(props) {
super(props);
const url = 'http://www.dnd5eapi.co/api/';
fetch(url + 'classes')
.then(result => result.json())
.then(result => { this.setState({ classes: result, }, this.getInfo(result)) });
}
state = {
classes: {}, //assigned value from API call in constructor
classesInfo: [], //assigned value from API call in getInfo()
classSelected: {}, //assigned a value derived from classInfo in displayClassInfo()
classProficiencies: [], //assigned a value derived from classSelected in setStartingProficiencies()
classProficienciesChoices: [], //assigned a value derived from classSelected in setStartingProficiencies()
}
getInfo(data) {
let info = []
const url = 'http://www.dnd5eapi.co'
for (var i = 0; i < data.results.length; i++) {
fetch(url + data.results[i].url)
.then(result => result.json())
.then(result => info.push(result))
}
this.setState({ classesInfo: info, })
}
}
setStartingProficiencies(chosenClass) {
const profs = chosenClass.proficiencies.map((prof) => {
return prof;
});
const proChoice = chosenClass.proficiency_choices.map((choices) => {
return choices;
});
this.setState({ classProficiencies: profs, classProficienciesChoices: proChoice, });
}
addProficiency = (proficiencyName) => {
const { classProficienciesChoices } = this.state
// classProficienciesChoices: [
// { choose: 2, type: 'proficiencies', from: [{ name: 'someName', url: 'someUrl' }, { name: 'someName', url: 'someUrl' }] },
// ]
// different classes have more objects in the parent array
let newChoiceArray = classProficienciesChoices.map((choices) => {
return choices
})
for (var i = 0; i < newChoiceArray.length; i++) {
for (var j = 0; j < newChoiceArray[i].from.length; j++) {
if (newChoiceArray[i].from[j].name === proficiencyName) {
let newChoices = newChoiceArray[i].from.filter(function (proficiency) { return proficiency.name !== pIndex })
let newProficiency = newChoiceArray[i].from.filter(function (proficiency) { return proficiency.name === pIndex })
newChoiceArray[i].from = newChoices //I think this is the problem
this.setState(state => ({
classProficiencies: [...state.classProficiencies, newProficiency[0]],
proficienciesChoices: newChoiceArray,
}))
}
}
}
}
displayClassInfo = index => {
const { classesInfo } = this.state
for (let i = 0; i < classesInfo.length; i++) {
if (classesInfo[i].index === index) {
const classSelected = classesInfo.filter(function (cClass) { return cClass.name === classesInfo[i].name })
this.setState({ classSelected: classSelected[0], isClassSelected: true }, this.setStartingProficiencies(classSelected[0]),)
break;
}
}
}
render() {
const { classes, classesInfo, classSelected, isClassSelected, classProficiencies, classProficienciesChoices } = this.state
return (<Create classes={classes} classesInfo={classesInfo} displayClassInfo={this.displayClassInfo} classSelected={classSelected} isClassSelected={isClassSelected} category='classes' classProficiencies={classProficiencies} classProficienciesChoices={classProficienciesChoices} addProficiency={this.addProficiency} />);
}
}
export default App
You call setState four times, of course it will update the state multiple times.
SOLVED!!! Stumbled across turning the array into a string and then back. It breaks the reference and creates an entirely new array. Reference type got me.
setStartingProficiencies(chosenClass) {
const proficiencies = JSON.parse(JSON.stringify(chosenClass.proficiencies))
const proficienciesChoices = JSON.parse(JSON.stringify(chosenClass.proficiency_choices))
this.setState({ classProficiencies: proficiencies, classProficienciesChoices: proficienciesChoices, });
}

Reducer cannot read property 'photos' of undefined? What am I doing wrong?

Here is the initial state of my reducer, and I need to set it up in this way due to some post processing I need to do:
const initialState = {
showAll: {
photos: null
}
}
Basically, I have a page where you see all your photos, and you can tag certain ones as your pinned photos.
Here's part of my reducer logic:
if (state.showAll.photos) {
const showAllState = state.showAll.photos;
showAllState.map(m => {
if (action.payload.id === m.id) {
m.pinned = true;
}
});
showAllAfterPin = showAllState;
} else {
showAllAfterPin = state.showAll.photos;
}
However, I get an error saying cannot read property 'photos' of undefined and I'm not sure what I am doing wrong.
Might be easier to just set your photos in initialState to empty array [] instead of null.
Another thing, your reducer should not mutate your state object.
Doing const showAllState = state.showAll.photos doesn't make it a new object.
Last thing, showAllState.map(...) needs to return an item inside the function body. It will create a new array.
Here's something you can do...
const { photos = [] } = state.showAll;
const updatedPhotos = photos.map(photo => {
if (action.payload.id === photo.id) {
return Object.assign({}, photo, { pinned: true })
}
return photo;
});
// return entire state if this is inside your root reducer
return {
...state,
showAll {
...state.showAll,
photos: updatedPhotos
}
}

Categories

Resources