UseState hook not updating the step properly - reactjs

I want to change state value with change in the 'select' element. So I am calling the setFilter method in the onChange handler. But state is not getting updated. It's holding the previous value.
How to fix this issue?
I want to change state value with change in the 'select' element. So I am calling the setFilter method in the onChange handler. But state is not getting updated. It's holding the previous value.
How to fix this issue?
import React, { useState, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { useApolloClient} from 'react-apollo';
import { Formik, Form, ErrorMessage } from 'formik';
import * as Yup from "yup";
import AsyncPaginate from 'react-select-async-paginate'
import { GET_ITEM_CODES } from '../../library/Query';
export default function SampleForm({initialData}){
const history = useHistory();
const [productFilter, setProductFilter] = useState('');
const client = useApolloClient();
const defaultAdditional = {
cursor : null
}
const shouldLoadMore = (scrollHeight, clientHeight, scrollTop) => {
const bottomBorder = (scrollHeight - clientHeight) / 2
return bottomBorder < scrollTop
}
const loadItemcodeOptions = async (q = 0, prevOptions, {cursor}) => {
console.log('qu',q*1)
const options = [];
console.log('load')
const response = await client.query({
query:GET_ITEM_CODES,
variables : {filter: {
number_gte : q*1
},skip:0, first:4, after: cursor}
})
console.log('res',response)
response.data.itemCodes.itemCodes.map(item => {
return options.push({
value: item.number,
label: `${item.number} ${item.description}`
})
})
console.log('0',options)
return {
options,
hasMore: response.data.itemCodes.hasMore,
additional: {
cursor: response.data.itemCodes.cursor.toString()
}
}
}
const handleFilter = (e) => {
console.log('e',e)
setProductFilter(e.value)
console.log('pf',productFilter) // output is previous State(wrong)
}
useEffect(() => {
console.log('epf',productFilter) // output the current state(expected)
})
return(
<Formik
initialValues = {{
itemCode: !!initialData ? {value: initialData.itemCode, label: initialData.itemCode} : '',
}}
validationSchema = {Yup.object().shape({
itemCode: Yup.number().required('Required'),
})}
>
{({values, isSubmitting, setFieldValue, touched, errors }) => (
<Form>
<label htmlFor="itemCode">Item Code</label>
<AsyncPaginate
name="itemCode"
defaultOptions
debounceTimeout={300}
cacheOptions
additional={defaultAdditional}
value={values.itemCode}
loadOptions={loadItemcodeOptions}
onChange={option => {
handleFilter(option)
setFieldValue('itemCode', option)
}}
shouldLoadMore={shouldLoadMore}
/>
<ErrorMessage name="itemcode"/>
<pre>{JSON.stringify(values, null, 2)}</pre>
</Form>
)}
</Formik>
)
}

Actually setProductFilter is setting state asynchronously, so you'll get updated state in the effect, not right after calling setState. But your effect is going to run every time when your component gets re-rendered so you should add productFilter as a dependency of useEffect.
One other thing I want to mention is, I don't know about your use case but you should stick to the rule: Single source of truth. You have two states for productFilter, one is in Formik, i.e. itemCode, and other in your local state. I think you can remove your local state and use item code from formikProps.values.itemCode.

Related

Why local storage does not work with useEffect?

I have the following code and it is working fine. <AddContact /> is a simple component that presents a input form which collects name + email from user - I have attached its code at the end for completeness. The collected contacts array is stored in localStorage, and when I refresh the page, they simply get reloaded. all good
import './App.css'
import { useState, useEffect } from 'react'
function App() {
const LOCAL_STORAGE_KEY = 'contacts'
const [contacts, setContacts] = useState(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [])
const addNewContact = (newContact) => {
setContacts([...contacts, newContact])
}
useEffect(() => {
console.table(contacts)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}, [contacts])
return (
<AddContact newContact={addNewContact} />
)
}
export default App
my question is that the following revision does not work - every time the page is refreshed, local storage is wiped out. But it really look like it should work - I was following an online tutorial and it was working when the instructor did it.
import './App.css'
import { useState, useEffect } from 'react'
function App() {
const LOCAL_STORAGE_KEY = 'contacts'
const [contacts, setContacts] = useState([]) // changed line
const addNewContact = (newContact) => {
setContacts([...contacts, newContact])
}
useEffect(() => {
console.table(contacts)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}, [contacts])
// added
useEffect(() => {
const savedContacts = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_KEY)
)
if (savedContacts) {
setContacts(savedContacts)
}
}, [])
return (
<AddContact newContact={addNewContact} />
)
}
export default App
for completeness, here's the code for <AppContact />
import React, { Component } from 'react'
export class AddContact extends Component {
state = {
name: '',
email: '',
}
updateState = (e) => {
this.setState({ [e.target.name]: e.target.value })
}
addContact = (e) => {
e.preventDefault()
if (this.state.name === '' || this.state.email === '') {
return
}
this.props.newContact(this.state)
}
render() {
return (
<div className='ui main'>
<h2>Add Contact</h2>
<form className='ui form' onSubmit={this.addContact}>
<div className='field'>
<label>Name</label>
<input
type='text'
name='name'
value={this.state.name}
placeholder='Name'
onChange={this.updateState}
/>
</div>
<div className='field'>
<label>Email</label>
<input
type='text'
name='email'
value={this.state.email}
placeholder='Email'
onChange={this.updateState}
/>
</div>
<button className='ui button blue' type='submit'>
Add
</button>
</form>
</div>
)
}
}
export default AddContact
I would like to understand why the second method does not work.
The value you set when calling addNewContact does get stored in localStorage when the first useEffect runs (as expected). The problem is that, when you reload the page, that same useEffect is overwriting what's in localStorage because the state is reset to an empty array ([]). This triggers the useEffect with contacts equals to [], and stores that in localStorage.
There are a few ways to handle it, but one way is to check if contacts is an empty array before storing its value to localStorage.
useEffect(() => {
console.table(contacts)
if (contacts.length > 0) {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}
}, [contacts])
This prevents the initial state of contacts to be stored in localStorage when the page gets first loaded.
In a given render, effect callbacks will run in the order that they're declared. With this:
useEffect(() => {
console.table(contacts)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}, [contacts])
// added
useEffect(() => {
const savedContacts = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_KEY)
)
if (savedContacts) {
setContacts(savedContacts)
}
}, [])
On mount, the first one runs before the second - you call localStorage.setItem before the second one runs localStorage.getItem - so by the time the second one runs, storage has been set to the initial value of the contacts state, which is the empty array.
To fix it, reverse their order, so that the one that calls .getItem runs first.
useEffect(() => {
const savedContacts = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_KEY)
)
if (savedContacts) {
setContacts(savedContacts)
}
}, []);
useEffect(() => {
console.table(contacts)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts))
}, [contacts]);
That said, your first approach of
const [contacts, setContacts] = useState(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [])
looks a lot nicer than an effect hook with an empty dependency array, IMO.

React get value from key:value array

Beginner question. I know this is a simple question but I haven't been able to get this to work. I'm passing an object which holds an array of k:v pairs to a component. Eventually this props will contain multiple k:v pairs, but for now I'm just passing the one.
[{goal: 20000}]
In the component I'm trying to grab the value, 20000, so I can display it on screen. I can't seem to get just the number. If I look at props.goal I get the entire k:v.
[{goal: 20000}]
If I try props[0].goal I get 'TypeError: undefined is not an object (evaluating 'props[0].goal')'
What am I missing? Thanks for any help.
Update:
Here is the entire code for the component in question.
import { React, useState } from "react";
import Form from "react-bootstrap/Form";
import { Row, Col, Button } from "react-bootstrap";
import "./../css/Goal.css";
const Goal = (props) => {
// const [goal, setGoal] = useState("");
const [record, setRecord] = useState("");
const monthlyGoal = 2;
console.log("props[0]");
console.log(props[0]); //undefined
console.log("props");
console.log({ props }); //See below
props: Object
goal: Object
goals: [{goal: 20000}] (1)
const handleInput = (event) => {
console.log(event);
event.preventDefault();
setRecord(event.target.value);
console.log(record);
};
const defaultOptions = {
significantDigits: 2,
thousandsSeparator: ",",
decimalSeparator: ".",
symbol: "$",
};
const formattedMonthlyGoal = (value, options) => {
if (typeof value !== "number") value = 0.0;
options = { ...defaultOptions, ...options };
value = value.toFixed(options.significantDigits);
const [currency, decimal] = value.split(".");
return `${options.symbol} ${currency.replace(
/\B(?=(\d{3})+(?!\d))/g,
options.thousandsSeparator
)}${options.decimalSeparator}${decimal}`;
};
return (
<Form>
<Row className="align-items-center flex">
<Col sm={3} className="goal sm={3}">
<Form.Control
id="inlineFormInputGoal"
placeholder="Goal"
// onChange={(e) => setGoal(e.target.value)}
/>
<Button type="submit" className="submit btn-3" onSubmit={handleInput}>
Submit
</Button>
</Col>
<Col>
<h1 className="text-box">
Goal: {formattedMonthlyGoal(monthlyGoal)}
</h1>
</Col>
</Row>
</Form>
);
};
export default Goal;
Update 2:Here is the parent component:
import React, { useEffect, useState } from "react";
import Goal from "./Goal";
import axios from "axios";
const Dashboard = () => {
const [dashboardinfo, setdashboardinfo] = useState([]);
useEffect(() => {
async function fetchData() {
try {
const data = (await axios.get("/api/goals/getgoals")).data;
setdashboardinfo(data);
} catch (error) {
console.log(error);
}
}
fetchData();
}, []);
return (
<React.Fragment>
<Goal dashboardinfo={dashboardinfo} />
</React.Fragment>
);
};
export default Dashboard;
If you get an object like the following from console logging destructured props:
{
dashboardinfo: {goals: [{goal: 20000}]}
}
You need to use props.dashboardinfo.goals[0].goal to get the value.
Your props contains the object "dashboardinfo" so you need to do
props.dashboardinfo.goals[0].goal
or a better way is to destructure your props object like this
const Goal = ({dashboardinfo: { goals }}) => {
...
goals[0].goal
...
}
I believe I've resolved my issue. It wasn't so much a problem with accessing the key:value as I thought, because when the page was initialized I was able to grab the value and display it fine. However, when I refreshed the page I lost all of the props data and that resulted in an error. I tracked it down to the useState didn't seem to be updating the value before I was trying to read it. So I added a useEffect in the child component.
const Goal = (props) => {
const [goal, setgoal] = useState([]);
useEffect(() => {
setgoal(props.goal);
console.log("the goal", goal);
}, [props.goal, goal]);
...
This seems to have worked as I'm getting the information I want and not getting any errors when I refresh. This may not be the ideal way to go about this but it is working.

React hooks, does not change the checked property to checkbox

I am trying to change the ckeckbox property, every time I write a new value to the map, in my case when I click on the checkbox, it only changes the value of the checked property, for example, the initial value is true, on all subsequent clicks it will be false, false, false ... What's wrong here?
import React,{ useState,useEffect } from "react";
import {useSelector} from 'react-redux'
const ChangeArticle = (props) => {
const prevArticle = props.changeArtcle;
const age_groups = useSelector(state => state.app.age_groups);
const [checkedAges, setCheckAges] = useState(new Map());
const handleChangeCheckBoxe = (event) => {
setCheckAges(checkedAges => checkedAges.set(event.target.value, event.target.checked));
console.log("checkedItems: ", checkedAges);
}
useEffect(() => {
if(prevArticle.ages){
prevArticle.ages.forEach((age) =>{
setCheckAges(checkedAges => checkedAges.set(age.toString(), true));
});
}
},[prevArticle]);
return (<div>
{age_groups.map(age => {
return (<div key={age.id}>
<input type="checkbox" checked={checkedAges.has(age.id.toString()) ? checkedAges.get(age.id.toString()) : false} value={age.id} onChange={handleChangeCheckBoxe}
/>
{ age.title }
</div>)
}) }
</div>);
}
export default ChangeArticle;
In the handleChangeCheckBoxe function, you are only changing the values withing the Map. React only does a shallow reference check to see if the Map had changed. Since the reference is the same, the it will not re-render the component as you would expect.
You can change the function to be similar to the following to create a new Map and assign it to state.
const handleChangeCheckBoxe = (event) => {
setCheckAges(checkedAges => new Map(checkedAges.set(event.target.value, event.target.checked)));
console.log("checkedItems: ", checkedAges);
}
You can see this working in the following code sandbox https://codesandbox.io/s/wispy-leftpad-9sji0?fontsize=14&hidenavigation=1&theme=dark

Handling TextField change in Fluent UI React

Using Fluent UI React, I'm displaying some data from an AppSync API in a TextField. I want to be able to show text from the API for a contact form. I then want to edit that text and click a button to post it back to the AppSync API.
If I use the TextField component on its own, I can then use a hook to set a variable to result of an AppSync API call and then have the TextField component read the value coming from the variable I set with the hook. I can then edit that text as I feel like and its fine.
The problem I have is that if I want to take edits to the TextField and set them using my hook I lose focus on the TextField. To do this I am using the onChange property of TextField. I can set the variable fine but I have to keep clicking back in to the input window.
Any thoughts on how I can keep the focus?
import React, { useEffect, useState } from 'react';
import { API, graphqlOperation } from 'aws-amplify';
import * as queries from '../../graphql/queries';
import { Fabric, TextField, Stack } from '#fluentui/react';
const PhoneEntryFromRouter = ({
match: {
params: { phoneBookId },
},
}) => PhoneEntry(phoneBookId);
function PhoneEntry(phoneBookId) {
const [item, setItem] = useState({});
useEffect(() => {
async function fetchData() {
try {
const response = await API.graphql(
graphqlOperation(queries.getPhoneBookEntry, { id: phoneBookId })
);
setItem(response.data.getPhoneBookEntry);
} catch (err) {
console.log(
'Unfortuantely there was an error in getting the data: ' +
JSON.stringify(err)
);
console.log(err);
}
}
fetchData();
}, [phoneBookId]);
const handleChange = (e, value) => {
setItem({ ...item, surname: value });
};
const ContactCard = () => {
return (
<Fabric>
<Stack>
<Stack>
<TextField
label='name'
required
value={item.surname}
onChange={handleChange}
/>
</Stack>
</Stack>
</Fabric>
);
};
if (!item) {
return <div>Sorry, but that log was not found</div>;
}
return (
<div>
<ContactCard />
</div>
);
}
export default PhoneEntryFromRouter;
EDIT
I have changed the handleChange function to make use of prevItem. For this event it does accept the event and a value. If you log that value out it is the current value and seems valid.
Despite the change I am still seeing the loss of focus meaning I can only make a one key stroke edit each time.
setItem((prevItem) => {
return { ...prevItem, surname: e.target.value };
});
};```
I think you want the event.target's value:
const handleChange = e => {
setItem(prevItem => { ...prevItem, surname: e.target.value });
};
You should also notice that in your version of handleChange(), value is undefined (only the event e is being passed as a parameter).
Edit: Now I see that you're setting the value item with data from a fetch response on component mount. Still, the value of item.surname is initially undefined, so I would consider adding a conditional in the value of the <TextField /> component:
value={item.surname || ''}

Lodash debounce isn't preventing dispatch as expected onChange

Currently, I have a list of checkboxes that onChange will make a request to the server to return some data. However, I am using lodash debounce to try and make a request only when the user has stopped selecting the multi-checkbox after a certain amount of time.
Currently, it prevents dispatching straight away but will dispatch after the debounce time has met rather when the user has stopped interacting with the checkboxes. Can someone tell me how I would achieve this or where I am going wrong?
Thanks!
import React, { useContext, useState, useEffect } from 'react';
import { Context } from '../../pages/search-and-results/search-and-results.js';
import debounce from 'lodash.debounce';
const FilterCheckbox = ({ name, value }) => {
const checkboxContext = useContext(Context);
const [checked, setChecked] = useState(false);
const debounceCheckboxSelection = debounce(dispatchCheckbox, 2000);
function dispatchCheckbox(type, value) {
checkboxContext.dispatch({
type: type,
payload: { value }
});
}
return (
<Label>
<FilterInput
type="checkbox"
name={name}
onChange={() => {
if (checked) {
debounceCheckboxSelection('REMOVE_SELECTED_PROPERTY_TYPE', value);
setChecked(false);
return;
}
debounceCheckboxSelection('SET_SELECTED_PROPERTY_TYPE', value);
setChecked(true);
}}
checked={checked}
/>
{name}
</Label>
);
};
export default FilterCheckbox;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Your debounced function is getting created at each re-render, to fix it:
You can use useRef which returns a ref object which will persist for the full lifetime of the component:
const debounceCheckboxSelection = useRef(
debounce(dispatchCheckbox, 2000);
)
and access its initial value with debounceCheckboxSelection.current:
<FilterInput
type="checkbox"
name={name}
onChange={() => {
if (checked) {
debounceCheckboxSelection.current('REMOVE_SELECTED_PROPERTY_TYPE', value);
setChecked(false);
return;
}
debounceCheckboxSelection.current('SET_SELECTED_PROPERTY_TYPE', value);
setChecked(true);
}}
checked={checked}
/>
Or you can use useCallback will returns a memoized version of the callback that only changes when any of its dependencies change:
const debounceCheckboxSelection = useCallback(
() => debounce(dispatchCheckbox, 2000), []
)

Resources