React Hooks: State Is Not Updating in Function - reactjs

I have spent several hours trying to fix this issue with no luck. I have a reusable component that starts with a state of any empty object, the object is given a series of properties based off of props, and then based on user input on each set of radio buttons will show a child component.
Unfortunately, the function that is supposed to update the state that will then trigger whether the user sees the child component is not working.
I believe the issue is in my editDisplay function. Currently when I click one of the radio buttons the new object that should become the new state is logged to the console correctly, but when I use React Devtools to inspect the page, I see that state is not updating. That being said, I could see a case where I am misunderstanding useEffect, and perhaps useEffect is running each time editDisplay is running. Any help would be appreciated.
import React, {useState, useEffect} from 'react'
import {Form} from 'react-bootstrap'
import RatingInput from './RatingInput'
export default function MembershipForm({inputs, name, record, setRecord}) {
const [display, setDisplay] = useState({})
useEffect(()=>{
inputs.forEach(input => {
var newDisplay = display
newDisplay[name+input]=false
setDisplay(newDisplay)})
},[])
const editDisplay = (input, visible) => {
var newDisplay =display
newDisplay[name+input]=visible
console.log(newDisplay)
setDisplay(newDisplay)
}
const questions=inputs.map(input => {
return (
<div key={name+input}>
<Form.Group>
<Form.Label className='mx-2'>Do you have a {name} {input} Rating?</Form.Label>
<Form.Check inline label='Yes' value={true} name={name+input} type='radio' className = 'mx-2' onClick ={() =>{editDisplay(input, true)}} />
<Form.Check inline label='No' value ={false} name={name+input} type='radio' className = 'mx-2' defaultChecked onClick = {() =>{editDisplay(input, false)}} />
</Form.Group>
{display[name+input] && <RatingInput input={input}/>}
</div >
)
})
return (
<div className='ml-3'>
{questions}
</div>
)
}

const editDisplay = (input, visible) => {
var newDisplay = display
newDisplay[name+input]=visible
console.log(newDisplay)
setDisplay(newDisplay)
}
You should perform react state update in an immutable way.
What you are doing above is you are mutating existing state variable (that assignment on first line doesn't add much: newDisplay refers to same object as display). In such case react might not detect change. Do this instead:
var newDisplay = { ... display };

Related

How to setState with select element in react - WITHOUT LAG

There seems to be a lag of one render cycle when I change a select element and when it's state actually changes. I know that there are several similar questions on this but none of them see to work for me. useEffect is still showing the old state from one render cycle before.
Anyone know how to address this?
Parent component code:
import React, {useCallback, useEffect, useState} from 'react'
import Dropdown from '../components/Dropdown.js'
const ValueCalculation = () => {
const industryData = require('../../../backend/standard_data/industry_benchmarks.json')
var industryList = []
industryData.map((record)=>{
var x = record.Industry;
industryList.push(x)
})
const [industry, setIndustry] = useState(industryList[8]);
const updateIndustry = (updatedValue)=>{
console.log(updatedValue); //<--This is updated with the right value!
setIndustry(updatedValue) //<--This is NOT updating the current value
console.log(industry); //<-- This does NOT show the updated value
}
useEffect(()=>{
console.log(industry); //<--Still showing value from previous render cycle
},[])
return (
<div>
<Dropdown
label="Industry"
value={industry}
onChange={(e)=>updateIndustry(e.target.value)}
list={industryList}
/>
</div>
)
}
export default ValueCalculation
Code for Child Dropdown component..
import React from 'react'
const Dropdown = (props) => {
return (
<div className="form-group mb-3">
<label>{props.label}</label>
<select
className="form-control"
value={props.value}
onChange={props.onChange}
>
{
props.list.map(item=>(
<option key={props.list.indexOf(item)} value={item}>{item}</option>
))
}
</select>
</div>
)
}
export default Dropdown
SetState is async so your console.log is going to run before the state has been set. The code you have works correctly as you can see in the sandbox link provided.
const updateIndustry = (updatedValue) => {
//This is updated with the right value!
console.log(updatedValue);
//This is updating correctly and will show on re render
setIndustry(updatedValue);
//This will not work since setState is async
//Console.log() is firing before the state has been set
console.log(industry);
};
As for the useEffect. You will need to add industry as a dependency so that the console.log is called as soon as the state changes :
useEffect(() => {
console.log(industry);
}, [industry]);
Sandbox : https://codesandbox.io/s/hidden-voice-8jvt2f?file=/src/App.js
So, it's a little bit complicated but your state is changing on every rerender cycle, so ur state it's updated after the updateIndustry it's finished (popped out from js callstack). I tested your code, and it is working perfectly and i refactored it a little bit
import React, { useEffect, useState } from "react";
import Dropdown from "./Dropdown.js";
const App = () => {
var industryList = ["a", "b", "c", "d"];
const [industry, setIndustry] = useState(industryList[0]);
useEffect(() => {
console.log(industry);
}, [industry]);
return (
<div>
<Dropdown
label="Industry"
value={industry}
onChange={(e) => setIndustry(e.target.value)}
list={industryList}
/>
</div>
);
};
export default App;
Also, useEffect hook is reexecuted when its dependency changes value, in your case your dependency array is empty so I added [industry] to it.

Child component prop causes "Maximum update depth exceeded"

I have the following two components.
Brands.js is a parent component that contains the SearchField component and passes a few props to it. I'll explain each prop:
brands is a list of Brand entities each containing an Id and a Name,
searchableItems is a list of Brand entities converted to an array of searchable items
setFilteredItems is a useState returned function that sets the filteredItems variable used by a table component
// I'm cutting away most of the code done in this component,
// but if necessary will expand this section.
...
<SearchField
wrapperClassName="ms-auto me-2 h-100"
inputClassName={"form-control-sm"}
items={brands}
searchableItems={searchableItems}
setFilteredItems={setFilteredItems}
itemIdResolver={(item) => item.id}
placeholder="Search for brand..."
/>
...
SearchField.js is a child component that renders a search input above the table in Brands.js component.
import React, {useEffect, useState} from 'react';
import {
Input
} from 'reactstrap';
export default function SearchField({ inputClassName, wrapperClassName, items, itemIdResolver, searchableItems, setFilteredItems, style, placeholder }) {
if (!inputClassName) inputClassName = '';
if (!wrapperClassName) wrapperClassName = '';
if (!itemIdResolver) itemIdResolver = (item) => item.id;
const [filterText, setFilterText] = useState("");
useEffect(() => {
let filteredSearchableItems = searchableItems?.filter(
searchableItem =>
searchableItem.searchString.indexOf(filterText.toLowerCase()) !== -1
);
setFilteredItems(
items?.filter(
item => filteredSearchableItems?.some(fsi => fsi.id === itemIdResolver(item))
));
}, [filterText, items, searchableItems, setFilteredItems, itemIdResolver]);
return (
<div className={"search-field-wrapper " + wrapperClassName}>
<i className="ri-search-line"></i>
<Input
type="text"
className={inputClassName}
placeholder={placeholder}
value={filterText}
onChange={e => setFilterText(e.target.value)}
style={style}
/>
</div>
)
}
The problem:
The SearchField components itemIdResolver causes the Maximum update depth exceeded error since it constantly re-renders the whole page/component.
I don't understand why fitting the itemIdResolver inside the dependency array of SearchField useEffect causes this.
Any help is appreciated.
P.S.
I tried removing the resolver and that got me rid of the maximum update error, so it's definitely due to that.

How could I rewrite this non-redux into redux approach?

I am kind of novice in react redux.
In my react project
I have three components
BigForm , FormA and FormB.
In BigForm, there are two state, DataA and DataB, which would be passed into FormA and FormB.
Question:
Whenever I add input value into Form A or Form B, it will also trigger unnecessary render on the other components. I heard redux may help, but I am not sure how to put this in work in this example.
What have thought before:
For some reason, it is my intention to set the state in parent, instead of having the child to held its own state, because at the end, I need to aggregate the data from all other forms for other purpose.
.Memo is not what I want too coz in my real examples, it is not working due to there is other complexity preventing it to work.
How can I avoid it? Example code would be appreciated. (please also see my edit before trying) thanks ; I heard redux may help, but I am not sure how to put this in work in this example
Below is my code:
import React, {useState, useEffect} from "react";
const FormA = (props) => {
useEffect(()=>{ console.log('Form A was just rendered')})
const { dataA, setDataA } = props;
return (
<div>
<input onChange={(e) => setDataA(e.target.value)}></input>
<p>Input Form A{dataA}</p>
</div>
);
};
const FormB = (props) => {
const { dataB, setDataB } = props;
useEffect(()=>{ console.log('Form B was just rendered')})
return (
<div>
<input onChange={(e) => setDataB(e.target.value)}></input>
<p>Input Form B{dataB}</p>
</div>
);
};
export function BigForm (props) {
const [dataA,setDataA] = useState()
const [dataB,setDataB] = useState()
return (
<div className="App">
<FormA dataA={dataA} setDataA={setDataA}></FormA>
<FormB dataB={dataB} setDataB={setDataB}></FormB>
</div>
);
}

React Hooks with Styled Components - Form not working

So I'm using React Hooks with styled components, I have tried to style a form but when I make it into a styled component the form doesn't work ie you type one letter then the form looses focus, you have to click back into the input box, type one letter and it looses focus again etc...
I'm also getting this warning in the dev tools but I don't really understand what I need to do -
index.js:27 The component styled.form with the id of "sc-eCssSg" has been created dynamically.
You may see this warning because you've called styled inside another component.
To resolve this only create new StyledComponents outside of any render method and function component.
at LocationForm (https://3deis.csb.app/src/Components/LocationForm.js:31:39)
at div
at App (https://3deis.csb.app/src/App.js:53:39)
How do I change the below code to do what is needed to make it work ?
import React, { useState } from "react";
import axios from "axios";
import styled from "styled-components";
const LocationForm = (props) => {
const [locationName, setName] = useState("");
const Form = styled.form``;
const handleSubmit = (evt) => {
evt.preventDefault();
axios
.get(
`http://www.mapquestapi.com/geocoding/v1/address?key=z2G40AM2VSDfXx7MQtCqAvmXmoYEX8cV&location=${locationName}&maxResults=1`
)
.then((res) => {
const latitude = res.data.results[0].locations[0].displayLatLng.lat;
const longitude = res.data.results[0].locations[0].displayLatLng.lng;
const city = res.data.results[0].locations[0].adminArea5;
// const submitted = !true;
props.callbackFromParent(
locationName,
// submitted,
latitude,
longitude,
city
);
})
.catch((error) => {
console.log(error);
});
};
return (
<Form onSubmit={handleSubmit}>
<label>
Location:
<input
type="text"
value={locationName}
onChange={(e) => setName(e.target.value)}
/>
</label>
<input type="submit" value="Submit" />
</Form>
);
};
export default LocationForm;
It says exactly what is happening. You are creating const Form = styled.form inside your LocationForm render function. If you move it 4 lines up outsie the function it will stop giving the warning. In general you should never create a styled component inside a render function, because it will recreate the form each render (so each time you input a character) instead of only once upon initialization.

How to connect redux store using useSelector() when input fields already mapped using useState()

I am playing around with the new React-Redux Hooks library
I have an react component that has two input fields that update to the react store using useState() - desc and amount. In order to update changes to the the redux store when field has been edited I use onBlur event and call dispatch to the redux store. That works fine.
When I want to clear the fields from another component I would like this to work in same manner as for class based functions via connect & map State to Props, however to to this with functional component I need to utilise useSelector(). I cannot do this as the identifiers desc and amount are already used by useState()
What am I missing here?
import { useDispatch, useSelector } from "react-redux"
import { defineItem, clearItem } from "../store/actions"
const ItemDef = props => {
const dispatch = useDispatch()
const [desc, setDesc] = useState(itemDef.desc)
const [amount, setAmount] = useState(itemDef.amount)
//MAPSTATETOPROPS
//I WANT TO HAVE THESE VALUES UPDATED WHEN REDUX STORE CHANGES FROM ANOTHER COMPONENT
//THESE LINES WILL CAUSE ERROR to effect - identifier has already been declared
const desc = useSelector(state => state.pendingItem.desc)
const amount = useSelector(state => state.pendingItem.amount)
return (
<div>
<p>Define new items to be added below - before clicking Add Item</p>
<input
value={desc}
type="text"
name="desc"
placeholder="Description of Item"
onChange={e => setDesc(e.target.value)}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => dispatch(defineItem(desc, amount))}
/>
<input
value={amount}
type="number"
name="amount"
placeholder="Amount"
onChange={e => setAmount(e.target.value)}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => {
dispatch(defineItem(desc, amount))
}}
/>
</div>
)
}
export default ItemDef
SOLUTION - WITH FULL CODE IN REPOSITORY
I worked out a solution by using useSelector (to map pendingItem part of redux state to itemDef) and the setEffect hook to apply useState to either state item (from input) or itemDef (from Redux State - this happens when redux is updated by another component or through the ADD ITEM TO INPUT button)
I have posted the working component below. I have also posted this small application to demonstrate how to use reacdt-redux libraries with both class based components and fuinctional components using hooks
The repository is https://github.com/Intelliflex/hiresystem
//**************************************************************************************************
//***** ITEMDEF COMPONENT - Allow entry of new Items (dispatched from button in HireList Table) ****
//**************************************************************************************************
import React, { useState, useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { defineItem, clearItem } from '../store/actions'
import _ from 'lodash'
const ItemDef = props => {
//BRING IN DISPATCH FROM REDUX STORE
const dispatch = useDispatch()
//DEFINE SELECTOR - EQUIV TO MAPSTATETOPROPS
const { itemDef } = useSelector(state => ({
itemDef: state.pendingItem
}))
const [item, setItem] = useState({ desc: '', amount: 0 })
const onChange = e => {
setItem({
...item,
[e.target.name]: e.target.value
})
}
const prevItem = useRef(item)
useEffect(() => {
//WE NEED TO CONDITIONALLY UPDATE BASED ON EITHER STORE BEING CHANGED DIRECTLY OR INPUT FORM CHANGING
if (!_.isEqual(item, prevItem.current)) {
//INPUT HAS CHANGED
setItem(item)
} else if (!_.isEqual(item, itemDef)) {
//REDUX STATE HAS CHANGED
setItem(itemDef)
}
prevItem.current = item
}, [item, itemDef]) //Note: item and ItemDef are passed in as second argument in order to use setItem
const clearIt = e => {
dispatch(clearItem())
}
const addIt = e => {
dispatch(defineItem({ desc: 'MY NEW ITEM', amount: 222 }))
}
return (
<div>
<p>Define new items to be added below - before clicking Add Item</p>
<input
value={item.desc}
type='text'
name='desc'
placeholder='Description of Item'
onChange={onChange}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => dispatch(defineItem(item))}
/>
<input
value={item.amount}
type='number'
name='amount'
placeholder='Amount'
onChange={onChange}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => dispatch(defineItem(item))}
/>
<button onClick={clearIt}>CLEAR ITEM</button>
<button onClick={addIt}>ADD ITEM TO INPUT</button>
</div>
)
}
export default ItemDef

Resources