UseState delay in multiple checkboxes - javascript

I am new to coding and would very much appreciate any assistance as i've been struggling with this for a while.
I have a form with over 10 fields, some are multiple checkboxes, some are not. Every field is its own component and has its own onChange. The problem is that in a multiple checkbox field, if i choose a number of options, submitting will only log the number minus 1. So if i choose 3, it will log only 2. This is the case for all multiple checkboxes.
I have learnt that useState is asynchronous and that useEffect should be used instead, i just don't know how to go about it for something like this.
import "./styles.css";
import Availability from "./components/Availability"
import { useState } from "react";
export default function App() {
const [formData, setFormData] = useState()
const addData = (id, value) => {
setFormData((prevValue) => {
return {
...prevValue,
[id]: value,
};
});
};
const onSubmit = (e) => {
e.preventDefault()
console.log(formData)
}
return (
<div className="App">
<form onSubmit={onSubmit}>
<Availability handleAddData={addData} />
<button>Submit</button>
</form>
</div>
);
}
And this is the component with the days of the week as an example:
import { useState } from "react";
export default function Availability({ handleAddData }) {
const [data, setData] = useState([]);
const availabilityArray = [
{
id: "availability",
title: "Monday",
value: "monday",
type: "checkbox",
},
{
id: "availability",
title: "Tuesday",
value: "tuesday",
type: "checkbox",
},
{
id: "availability",
title: "Wednesday",
value: "wednesday",
type: "checkbox",
},
{
id: "availability",
title: "Thursday",
value: "thursday",
type: "checkbox",
},
{
id: "availability",
title: "Friday",
value: "friday",
type: "checkbox",
},
{
id: "availability",
title: "Saturday",
value: "saturday",
type: "checkbox",
},
{
id: "availability",
title: "Sunday",
value: "sunday",
type: "checkbox",
},
];
const onChange = (e) => {
const { id, value, checked } = e.target;
if (!checked) {
setData(data.filter((word) => word !== value));
return;
}
if (checked) {
setData((prevValue) => {
return [...prevValue, value];
});
}
handleAddData(id, data);
};
return (
<>
<h3>Please confirm your availability</h3>
<div onChange={onChange}>
<ul>
{availabilityArray.map((item) => (
<li
key={item.value}
>
<div>
<label>
<input
id={item.id}
type={item.type}
value={item.value}
/>
<span>{item.title}</span>
</label>
</div>
</li>
))}
</ul>
</div>
</>
);
}
Here is a codesandbox so that you can see how it works. The console.log of -1 whatever has been selected when submitting is my problem.

In the onChange function in availability component, you are setting the value for the state and passing the value to the addData function in the same onChange function.
Since useState is a async operation. It will have the old value when you pass the value first and when you pass the next value only the last value will be updated and so on.
so I have created a useEffect which listens to the changes made for the data state and call the handleAddData. Now whenever the data changes in the data state. It will call the handleAddData with the updated value.
checkout the codesandbox link: https://codesandbox.io/s/serene-einstein-xhf1qc?file=/src/components/Availability.js
Availability.js
import { useEffect, useState } from "react";
export default function Availability({ handleAddData }) {
const [data, setData] = useState([]);
const [dataid, setId] = useState();
const availabilityArray = [
{
id: "availability",
title: "Monday",
value: "monday",
type: "checkbox"
},
{
id: "availability",
title: "Tuesday",
value: "tuesday",
type: "checkbox"
},
{
id: "availability",
title: "Wednesday",
value: "wednesday",
type: "checkbox"
},
{
id: "availability",
title: "Thursday",
value: "thursday",
type: "checkbox"
},
{
id: "availability",
title: "Friday",
value: "friday",
type: "checkbox"
},
{
id: "availability",
title: "Saturday",
value: "saturday",
type: "checkbox"
},
{
id: "availability",
title: "Sunday",
value: "sunday",
type: "checkbox"
}
];
useEffect(() => {
if (data.length !== 0) {
handleAddData(dataid, data);
}
}, [dataid, data]);
const onChange = (e) => {
const { id, value, checked } = e.target;
if (!checked) {
setData(data.filter((word) => word !== value));
return;
}
if (checked) {
setData((prevValue) => {
return [...prevValue, value];
});
}
setId(id);
};
return (
<>
<h3>Please confirm your availability</h3>
<div onChange={onChange}>
<ul>
{availabilityArray.map((item) => (
<li key={item.value}>
<div>
<label>
<input id={item.id} type={item.type} value={item.value} />
<span>{item.title}</span>
</label>
</div>
</li>
))}
</ul>
</div>
</>
);
}

Related

TypeError: undefined is not an object (evaluating 'expensesCtx.expenses.filter')

I am creating an expense tracker app while using the filter method it can't receive the array properly can someone help me to figure out where i am doing wrong
Error:TypeError: undefined is not an object (evaluating 'expensesCtx.expenses.filter')
The above specified error is present in the RecentExpenses.js file
expenses-context.js
import { createContext, useReducer } from "react";
const DUMMY_EXPENSES = [
{
id: "e1",
description: "A pair of shoes",
amount: 59.99,
date: new Date("2021-12-19"),
},
{
id: "e2",
description: "A pair of trousers",
amount: 89.29,
date: new Date("2022-01-05"),
},
{
id: "e3",
description: "A pair of shoes",
amount: 5.99,
date: new Date("2021-12-01"),
},
{
id: "e4",
description: "A book",
amount: 14.99,
date: new Date("2022-02-19"),
},
{
id: "e5",
description: "Another book",
amount: 18.59,
date: new Date("2022-02-18"),
},
{
id: "e6",
description: "A pair of shoes",
amount: 59.99,
date: new Date("2021-12-19"),
},
{
id: "e7",
description: "A pair of trousers",
amount: 89.29,
date: new Date("2022-01-05"),
},
{
id: "e8",
description: "A pair of shoes",
amount: 5.99,
date: new Date("2021-12-01"),
},
{
id: "e9",
description: "A book",
amount: 14.99,
date: new Date("2022-02-19"),
},
{
id: "e10",
description: "Another book",
amount: 18.59,
date: new Date("2022-02-18"),
},
];
export const ExpenseContext = createContext({
expenses: [],
addExpense: ({ description, amount, date }) => {},
deleteExpense: (id) => {},
updateExpense: (id, { description, amount, date }) => {},
});
function expenseReducer(state, action) {
switch (action.type) {
case "ADD":
const id = new Date().toString() + Math.random().toString();
return [{ ...action.payload, id: id }, ...state]; //spreading existing items into array
case "UPDATE":
const updatetableExpenseIndex = state.findIndex(
(expense) => expense.id == action.payload.id
);
const updatableExpense = state[updatetableExpenseIndex];
const updateItem = [...updatableExpense, ...action.payload.data];
const updatedExpenses = [...state];
updatableExpense[updatetableExpenseIndex] = updateItem;
return updatedExpenses;
case "DELETE":
return state.filter((expense)=>expense.id!==action.payload);
default:
return state;
}
}
function ExpensesContextProvider({ children }) {
const [expensesState, dispatch] = useReducer(expenseReducer, DUMMY_EXPENSES);
function addExpense(expenseData) {
dispatch({ type: "ADD", payload: expenseData }); //it will dispatch the type of action(handled in reducer function) we need to perform
}
function deleteExpense(id) {
dispatch({ type: "DELETE", payload: id });
}
function updateExpense(id, expenseData) {
dispatch({ type: "UPDATE", payload: { id: id, data: expenseData } });
}
const value={
expense:expensesState,
addExpense:addExpense,
deleteExpense:deleteExpense,
updateExpense:updateExpense,
}
return <ExpenseContext.Provider value={value}>{children}</ExpenseContext.Provider>;
}
export default ExpensesContextProvider;
AllExpenses.js
import { useContext } from 'react';
import {Text} from 'react-native';
import ExpensesOutput from '../components/ExpensesOutput/ExpensesOutput';
import {ExpenseContext} from '../store/expenses-context'
function AllExpenses(){
const expensesCtx=useContext(ExpenseContext);
return <ExpensesOutput expenses={expensesCtx.expenses} expensesPeriod="Total"/>;
}
export default AllExpenses;
RecentExpenses.js
import { useContext } from "react";
import ExpensesOutput from "../components/ExpensesOutput/ExpensesOutput";
import { ExpenseContext } from "../store/expenses-context";
import { getDateMinusDays } from "../util/date";
function RecentExpenses() {
const expensesCtx = useContext(ExpenseContext);
const recentExpenses = expensesCtx.expenses.filter((expense) => {
const today = new Date();
const date7DaysAgo = getDateMinusDays(today, 7);
return expense.date > date7DaysAgo;
});
return (
<ExpensesOutput expenses={recentExpenses} expensesPeriod={"Last 7 Days"} />
);
}
export default RecentExpenses;

how to solve asynchronous behaviour in search Box react

so im trying to implement a search box with useState and useEffect. we have an array of objects and want to filter it according to our search term. here is my implementation:
import React, {useEffect, useState} from "react";
const array = [
{ key: '1', type: 'planet', value: 'Tatooine' },
{ key: '2', type: 'planet', value: 'Alderaan' },
{ key: '3', type: 'starship', value: 'Death Star' },
{ key: '4', type: 'starship', value: 'CR90 corvette' },
{ key: '5', type: 'starship', value: 'Star Destroyer' },
{ key: '6', type: 'person', value: 'Luke Skywalker' },
{ key: '7', type: 'person', value: 'Darth Vader' },
{ key: '8', type: 'person', value: 'Leia Organa' },
];
let available = []
const Setup = () => {
const [state, setState] = useState('');
useEffect(() => {
available = array.filter(a => a.value.startsWith(state));
},[state])
const show = state ? available : array;
return <>
<input value={state} onChange={e => setState(e.target.value)} type="text" className="form"/>
{show.map(a => {
return <Data id={a.key} key={parseInt(a.key)} value={a.value} type={a.type}/>
})}
</>
}
const Data = (props) => {
return <>
<div>
<p>{props.value}</p>
</div>
</>
}
export default Setup;
the problem starts when we give our search box a valid search term(like 'T'). i expect it to change the output accordingly(to only show 'Tatooine') but the output does not change.
meantime if you add another character to search term(like 'a' which would set our search term to 'Ta') it will output the expected result. in the other words, search term is not applied synchronously. do you have any idea why is that
The useEffect hook is triggered when the component mounts, rerenders or unmounts. In your case, the change of the search field causes a rerender because of the change of the state. This results in your useEffect triggering after the state change and is too late for what you need.
If you type "Ta" into your field, you'll see it works, but it appears as if the search is one step behind.
You can simply remove the use of useEffect and filter when you render. This means you can also remove the whole logic around the available and show variables:
const Setup = () => {
const [state, setState] = useState("");
return (
<>
<input
value={state}
onChange={(e) => setState(e.target.value)}
type="text"
className="form"
/>
{array
.filter((a) => a.value.startsWith(state))
.map((a) => (
<Data
id={a.key}
key={parseInt(a.key, 10)}
value={a.value}
type={a.type}
/>
))}
</>
);
};
There is some good information in the Using the Effect Hook docs.
You just add toLowerCase mehtod to your filter function. just like this :
import React, { useEffect, useState } from "react";
const array = [
{ key: "1", type: "planet", value: "Tatooine" },
{ key: "2", type: "planet", value: "Alderaan" },
{ key: "3", type: "starship", value: "Death Star" },
{ key: "4", type: "starship", value: "CR90 corvette" },
{ key: "5", type: "starship", value: "Star Destroyer" },
{ key: "6", type: "person", value: "Luke Skywalker" },
{ key: "7", type: "person", value: "Darth Vader" },
{ key: "8", type: "person", value: "Leia Organa" }
];
let available = [];
const Setup = () => {
const [state, setState] = useState("");
useEffect(() => {
available = array.filter((a) => a.value.toLowerCase().startsWith(state));
}, [state]);
const show = state ? available : array;
return (
<>
<input
value={state}
onChange={(e) => setState(e.target.value)}
type="text"
className="form"
/>
{show.map((a) => {
return (
<Data
id={a.key}
key={parseInt(a.key)}
value={a.value}
type={a.type}
/>
);
})}
</>
);
};
const Data = (props) => {
return (
<>
<div>
<p>{props.value}</p>
</div>
</>
);
};
export default Setup;
and here is the working example : here
You can simply just pull out useEffect.
import React, { useState } from 'react';
const array = [
{ key: '1', type: 'planet', value: 'Tatooine' },
{ key: '2', type: 'planet', value: 'Alderaan' },
{ key: '3', type: 'starship', value: 'Death Star' },
{ key: '4', type: 'starship', value: 'CR90 corvette' },
{ key: '5', type: 'starship', value: 'Star Destroyer' },
{ key: '6', type: 'person', value: 'Luke Skywalker' },
{ key: '7', type: 'person', value: 'Darth Vader' },
{ key: '8', type: 'person', value: 'Leia Organa' },
];
let available = [];
const Setup = () => {
const [state, setState] = useState('');
available = array.filter(a => a.value.startsWith(state));
const show = state ? available : array;
return (
<>
<input
value={state}
onChange={e => setState(e.target.value)}
type='text'
className='form'
/>
{show.map(a => {
return (
<Data
id={a.key}
key={parseInt(a.key)}
value={a.value}
type={a.type}
/>
);
})}
</>
);
};
const Data = props => {
return (
<>
<div>
<p>{props.value}</p>
</div>
</>
);
};
export default Setup;
This must solve it
import React, { useEffect, useState } from "react";
const array = [
{ key: "1", type: "planet", value: "Tatooine" },
{ key: "2", type: "planet", value: "Alderaan" },
{ key: "3", type: "starship", value: "Death Star" },
{ key: "4", type: "starship", value: "CR90 corvette" },
{ key: "5", type: "starship", value: "Star Destroyer" },
{ key: "6", type: "person", value: "Luke Skywalker" },
{ key: "7", type: "person", value: "Darth Vader" },
{ key: "8", type: "person", value: "Leia Organa" }
];
const Setup = () => {
const [state, setState] = useState("");
const [available, setAvailable] = useState(array);
useEffect(() => {
setAvailable(array.filter((a) => a.value.startsWith(state)));
}, [state]);
return (
<>
<input
value={state}
onChange={(e) => setState(e.target.value)}
type="text"
className="form"
/>
{available.map((a) => {
return (
<Data
id={a.key}
key={parseInt(a.key)}
value={a.value}
type={a.type}
/>
);
})}
</>
);
};
const Data = (props) => {
return (
<>
<div>
<p>{props.value}</p>
</div>
</>
);
};
export default Setup;

How to filter the data array student based on the room using react hook form

index.tsx
data = {
room: [
{
id: 1,
name: 'room1'
},
{
id: 2,
name: 'room2'
},
{
id: 3,
name: 'room3'
}
],
student: [
{
id: 1,
room: 'room1',
name: 'josh'
},
{
id: 2,
room: 'room1',
name: 'jake'
}
]
}
const _ROOM = data['room'];
const _STUDENT = data['student'];
const form = {
config: [
{
label: "Room",
name: "room",
type: "select",
rule: yup.string().required(),
renderer: (data: any) => {
const { control, register, errors } = useFormContext();
return (
<SelectPicker
placeholder="Room"
data={
_ROOM && _ROOM.length > 0 ?
_ROOM.map(x => ({ label: x.name, value: x.id })) : []
}
style={{ width: '100%' }}
onChange={(val) => {
control.setValue('room', val);
}}
value={control.getValues()['room']}
/>
);
}
},
{
label: "Student",
name: "student",
type: "select",
rule: yup.string().required(),
renderer: (data: any) => {
const { control, register, errors } = useFormContext();
return (
<SelectPicker
placeholder="Student"
data={
_STUDENT && _STUDENT.length > 0 ?
_STUDENT.map(x => ({ label: x.name, value: x.id })) : []
}
style={{ width: '100%' }}
onChange={(val) => control.setValue('student', val)}
value={control.getValues()['student']}
/>
);
}
}]}
How to filter the student based on the room. for example I select the room1 then on the student it will filter which it has value room1. I try to filter inside the onchange in room but it doesn't work also not filtering or display the console log. also I used the state then set inside the onChange but it doesn't work also.
Take a look at this: https://react-hook-form.com/advanced-usage/#ConditionalControlledComponent
Basically, you can watch for changes in room and filter out the options in the student field.
const {room} = watch();
And in the SelectPicker, modify the data prop to:
data={
_STUDENT && _STUDENT.length > 0 ?
_STUDENT.filter(s => s.room === room).map(x => ({ label: x.name, value: x.id })) : []
}

How to sum values in an array of objects in javascript

I have an array of objects, like so:
[
{
Daypart: "G_POSTPEAK",
day_of_week: "Monday",
uplift: 1
},
{
Daypart: "A_BREAKFAST",
day_of_week: "Thursday",
uplift: 1
},
{
Daypart: "C_DAYTIME",
day_of_week: "Sunday",
uplift: 2
},
{
Daypart: "G_POSTPEAK",
day_of_week: "Monday",
uplift: 2
},
]
I have only shown a sample of objects in the array, I am working with a lot more. They all have the specified properties, the daypart property could be one of 8 values and the day of week value could be one of 7 (days of the week).
I want to return the sum of the uplift value for all the objects that have the same daypart and day_of_week value.
So the above should return something like:
{
G_POSTPEAK_Monday: {
Daypart: "G_POSTPEAK",
day_of_week: "Monday",
uplift: 3
},
A_BREAKFAST_Thursday: {
Daypart: "A_BREAKFAST",
day_of_week: "Thursday",
uplift: 1
},
C_DAYTIME_Sunday: {
Daypart: "C_DAYTIME",
day_of_week: "Sunday",
uplift: 2
}
}
Appreciate any help
The following function can be used. I have used ES6. The function takes inputs which will be your input object.
const sumIt = (inputs) => {
const result = {};
inputs.forEach((input) => {
const key = `${input.Daypart}_${input.day_of_week}`;
if (key in result) {
result[key].uplift = result[key].uplift + input.uplift;
} else {
result[key] = { ...input };
}
});
return result;
};
You can use ES6 reduce function to perform the sum in one line instead of using foreach.
let array = [
{
Daypart: "G_POSTPEAK",
day_of_week: "Monday",
uplift: 1
},
{
Daypart: "A_BREAKFAST",
day_of_week: "Thursday",
uplift: 1
},
{
Daypart: "C_DAYTIME",
day_of_week: "Sunday",
uplift: 2
},
{
Daypart: "G_POSTPEAK",
day_of_week: "Monday",
uplift: 2
},
];
const totalUplift = array.reduce((acc, array) => acc + array.uplift, 0);
console.log(totalUplift);
You can use array#reduce to group data based on the Daypart and day_of_week in an object accumulator.
let data = [ { Daypart: "G_POSTPEAK", day_of_week: "Monday", uplift: 1 }, { Daypart: "A_BREAKFAST", day_of_week: "Thursday", uplift: 1 }, { Daypart: "C_DAYTIME", day_of_week: "Sunday", uplift: 2 }, { Daypart: "G_POSTPEAK", day_of_week: "Monday", uplift: 2 }],
result = data.reduce((r,o) => {
let key = `${o.Daypart}_${o.day_of_week}`;
r[key] = r[key] || {...o, uplift: 0};
r[key].uplift += o.uplift;
return r;
},{});
console.log(result);
.as-console-wrapper {max-height: 100% !important; top: 0;}
A somewhat more functional version, using reduce like several other solutions.
const combine = inputs => Object .values (
inputs .reduce ((a, {Daypart, day_of_week, uplift}) => {
const key = `${Daypart}|${day_of_week}`
return {...a, [key]: ({
Daypart,
day_of_week,
uplift: (a[key] && a[key].uplift || 0) + uplift
})}
}, {})
)
let inputs = [
{Daypart: "G_POSTPEAK", day_of_week: "Monday", uplift: 1},
{Daypart: "A_BREAKFAST", day_of_week: "Thursday", uplift: 1},
{Daypart: "C_DAYTIME", day_of_week: "Sunday", uplift: 2},
{Daypart: "G_POSTPEAK", day_of_week: "Monday", uplift: 2},
]
console .log (
combine (inputs)
)
You can use loadash package to perform this in optimized way...
let _ = require('lodash');
let arrays = [
{
'name':'monday',
vaue:1
},
{
'name':'monday',
vaue:2
},
{
'name':'tuesday',
vaue:3
},
{
'name':'wednesday',
vaue:11
},
{
'name':'wednesday',
vaue:11
},
]
let newarray = _(arrays).groupBy("name").map( (name) => ({
name: name,
value0: _.sumBy(name, 'vaue')
}))
.value()
console.log(newarray);

How to properly use Yup schema with single react-select?

I'm using react-select with Formik and Yup to validate my form, but for some reason my validation is not working. My Schema looks like this:
const Schema = Yup.object().shape({
age: Yup.object().shape({
label: Yup.string().required("Required"),
value: Yup.string().required("Required")
})
});
And my data looks like this:
export const ageOptions = [
{ value: 0.1, label: "Not born yet" },
{ value: 0.3, label: "Baby - 0 to 3 months" },
{ value: 0.6, label: "Baby - 3 to 6 months" },
{ value: 0.12, label: "Baby - 6 to 12 months" },
{ value: 0.18, label: "Baby - 12 to 18 months" },
{ value: 0.24, label: "Baby - 18 to 24 months" },
{ value: 2, label: "2 years" },
{ value: 3, label: "3 years" },
{ value: 4, label: "4 years" },
{ value: 5, label: "5 years" },
{ value: 6, label: "6 years" },
{ value: 7, label: "7 years" },
{ value: 8, label: "8 years" },
{ value: 9, label: "9 years" },
{ value: 10, label: "10 years" },
{ value: 11, label: "11 years" },
{ value: 12, label: "12 years" },
{ value: 13, label: "13 years" },
{ value: 14, label: "14 years" }
];
When I select an option in the select inside the UI, the following error is returned:
age must be a `object` type, but the final value was: `null` (cast from the value `0.6`). If "null" is intended as an empty value be sure to mark the schema as `.nullable()`
How do I make the validation work correctly?
Link to sandbox
You require age to be of type object, but set it the value of the selected option. That is what triggers your wrong validation. Here is how to fix your validation:
If you want to keep age to be an object, change your schema to the following:
const Schema = Yup.object().shape({
age: Yup.object().shape({
label: Yup.string().required("Required"),
value: Yup.string().required("Required")
})
});
else set it to the following:
const Schema = Yup.object().shape({
age: Yup.string()
});
Update your onChange on the Select component to set the value to the option instead of the option.value if you want to use the object in your schema validation.
<Select
{ ... }
value={field.value} // This can be set like this as a result of the change
onChange={option => form.setFieldValue(field.name, option)}
/>
That should get it to work.
Yup Validation Schema
const validationSchema = function (values) {
return Yup.object().shape({
courseId: Yup.string().required("Required").nullable()
})
}
React-select custom class component
import React from 'react'
import Select from 'react-select'
class SingleSelectField extends React.Component {
constructor(props) {
super(props);
}
handleChange = (item) => {
if(item)
this.props.onChange(this.props.type, item.value);
else
this.props.onChange(this.props.type, "");
};
handleBlur = () => {
this.props.onBlur(this.props.type, true);
};
render() {
const defaultValue = (options, value) => {
return options ? options.find(option => option.value === value) : "";
};
return (
<div>
<Select
options={this.props.options}
onChange={this.handleChange}
onBlur={this.handleBlur}
value={defaultValue(this.props.options, this.props.value)}
isClearable
/>
{!!this.props.error && this.props.touched && (
<div style={{ color: "red", marginTop: ".5rem" }}>
{this.props.error}
</div>
)}
</div>
)}
}
export { SingleSelectField as default };

Categories

Resources