React update nested object state - reactjs

In one of my react project, I have initial state as follow:
const initialValues = {
en: {
notificationTitle: "",
notificationSubTitle: "",
},
hi: {
notificationTitle: "",
notificationSubTitle: "",
},
webUrl: "",
};
const [formData, setFormData] = useState(initialValues);
I'm passing this state as a props to child components which I'm using them as tabs like this
{selectedTab === "EnNotification" ? (
<EnNotification
formData={formData}
setFormData={setFormData}
/>
) : (
<HiNotification
formData={formData}
setFormData={setFormData}
/>
)}
when I enter the data in one tab suppose in EnNotification tab the state updates but when I tried to switch the tab it give me the following error:
TypeError: Cannot read properties of undefined (reading 'notificationTitle')
My input component looks like:
<Input
value={formData.en.notificationSubTitle || ""}
placeholder="Notification Sub Title"
onChange={(event) => {
const tempval = event.target.value;
setFormData((data) => ({
en: { ...data.en, notificationSubTitle: tempval },
}));
}}
/>
I think the problem is from only one component I'm able to update the state, but I want it should be updated from both.
Thank you.

When you are updating the formData state, before replacing the object en, you should also copy the rest of the value from the current state. Try this instead:
setFormData((data) => ({
...data,
en: { ...data.en, notificationSubTitle: tempval },
}));

Related

Formik renders twice on initialization

I have this simple example of Formik where i have a simple input. When i run the page i see in the console that it renders twice. The formik package is exactly the same as the first message.
Why it renders twice if there is nothing changed?
const SignupForm = () => {
const [data, setData] = useState({
firstName: "",
lastName: "",
email: "",
});
return (
<Formik
initialValues={data}
enableReinitialize
validateOnBlur={false}
validateOnChange={false}
onSubmit={(values, { setSubmitting }) => {
}}
>
{(formik) => {
console.log(formik);
return (
<form onSubmit={formik.handleSubmit}>
<label htmlFor="firstName">First Name</label>
<input
id="firstName"
type="text"
{...formik.getFieldProps("firstName")}
/>
{formik.touched.firstName && formik.errors.firstName ? (
<div>{formik.errors.firstName}</div>
) : null}
</form>
);
}}
</Formik>
);
};
That is happening due to enableReinitialize property.
Formik itself has a few useEffects inside of it, and a formikReducer. So when you pass enableReinitialize - formikReducer is called 2 times:
payload: {}, type: "SET_ERRORS"
payload: {}, type: "SET_TOUCHED"
Which is happening due to folowing useEffects inside of the source codes:
React.useEffect(function () {
if (enableReinitialize && isMounted.current === true && !isEqual(initialErrors.current, props.initialErrors)) {
initialErrors.current = props.initialErrors || emptyErrors;
dispatch({
type: 'SET_ERRORS',
payload: props.initialErrors || emptyErrors
});
}
}, [enableReinitialize, props.initialErrors]);
React.useEffect(function () {
if (enableReinitialize && isMounted.current === true && !isEqual(initialTouched.current, props.initialTouched)) {
initialTouched.current = props.initialTouched || emptyTouched;
dispatch({
type: 'SET_TOUCHED',
payload: props.initialTouched || emptyTouched
});
}
}, [enableReinitialize, props.initialTouched]);
And those ifs are passed due to the initialTouched and initialErrors are initialized inside of the Formik with this:
var initialErrors = React.useRef(props.initialErrors || emptyErrors);
var initialTouched = React.useRef(props.initialTouched || emptyTouched);
So initial values are equal to empty ones which are {}. But inside of the if they have !isEqual(initialErrors.current, props.initialErrors)) for example, so comparison between {} and undefined is passed and we are going inside of the if body and updating the Formik internal state. That is what is causing an additional rerender.
So if you pass the following props to Formik component - console.log will be executed only once
initialErrors={{}}
initialTouched={{}}
Now about how to collect that information:
Configure local minimal workspace with React and Formik
Go to node_modules/formik/dist/formik.cjs.development.js and inject some logging code inside of the formikReducer, simple console.log
In the component that is using <Formik> import it from modified development js file. import { Formik } from "formik/dist/formik.cjs.development";
Formik version: 2.2.9

Update object from input without setState

I'm new to React JS (and JS, in general). Now I'm trying to code a simple task tracker.
So, I have all tasks in state element of MyTodoList class. There I draw each task separately with Task constant.
I want to implement adding a new task with 2 inputs: name and description.
I do it in MyTodoList with creating a new object (newTask), so that I can add it to state list later. However, I guess that I'm writing onChange method for input incorrectly. newTask seems to be updating inside the function (logged it in console), but it does not change outside (in input space there are no changes with typing). Obviously I cannot use setState as I want to update a non-state object (object is mutable, so I do not understand why it won't change).
I'm not sure whether I'm updating the object wrongly or whether my whole concept of adding new task is wrong. Would be grateful if you could explain me my mistakes.
Here's the code:
const TaskAdd = ({value, onChange, placeholder, name}) => {
return (
<input value={value} onChange={onChange} placeholder={placeholder} name={name}/>
)
}
const Task = ({id, name, description, completed}) => {
const handleClick = () => {
}
return (
<div className='task'>
<h3>{name}</h3>
<div>{description}</div>
<div>{completed}</div>
<button onClick={handleClick} className='button1'>CLICK</button>
</div>
)
}
class MyTodoList extends React.Component {
state = {
tasks: [
{
id: 1,
name: 'Walk the dog',
description: 'Have to walk the dog today',
completed: false,
},
]
}
maxId = this.state.tasks[this.state.tasks.length - 1].id;
newTask = {
id: this.maxId,
name: '',
description: '',
completed: false,
}
handleChange = (event) => {
const {value, name} = event.currentTarget
this.newTask[name] = this.newTask[name] + value
}
render () {
return(
<div>
<header><h1>TO-DO</h1></header>
<div className='addTask'>
<h2>Let's add something new</h2>
<TaskAdd value={this.newTask.name} onChange={this.handleChange}
placeholder='Name' name='name'/>
<TaskAdd value={this.newTask.description} onChange={this.handleChange}
placeholder='Description' name='description'/>
<p> {this.newTask.name}</p>
<button className='button1'><h3>Add</h3></button>
</div>
<div>{this.state.tasks.map(task => <Task id={task.id} name={task.name}
description={task.description} completed={task.completed}/>)}
</div>
</div>
)
}
}
const App = () => {
return (
<MyTodoList />
)
}
export default App;
Obviously I cannot use setState as I want to update a non-state object
If you want the screen to update you have to use state. The setState function is the only* way to tell react that something change and it needs to rerender.
So, expand your state to have new task in it:
state = {
tasks: [
{
id: 1,
name: 'Walk the dog',
description: 'Have to walk the dog today',
completed: false,
},
]
newTask: {
id: 2,
name: '',
description: '',
completed: false,
}
}
With that you'll need to update your render function to access it in state, as in:
<TaskAdd
value={this.state.newTask.name}
onChange={this.handleChange}
placeholder='Name'
name='name'
/>
And then when you set state, make a copy instead of mutating:
handleChange = (event) => {
const {value, name} = event.currentTarget
this.setState({
newTask: {
...this.state.newTask,
[name]: this.state.newTask[name] + value
}
});
}
Your code didn't include an implementation for the add button, but when you do, you'll probably take this.state.newTask and add it to the end of this.state.tasks (you'll make a copy of the array, not mutate it), and then create a new object to replace this.state.newTask
*ok, technically there's forceUpdate, but don't use that.

object Object when entering a value into the input

An error appears when entering any value
At first I used class components, but then I started redoing them for functional ones and everything broke.
In my state I get a value like this:title > title:"[object Object] and the last symbol which I entered.
Here is the code
reducer
export const postsReducer = (state = initialState, action) => {
switch (action.type) {
case CREATE_POST:
return {...state, posts: state.posts.concat(action.payload)}
default:return state
}
}
Action
export function createPost(post){
return{
type: CREATE_POST,
payload:post
}
}
and a function in a class component
this.setState(prev => ({
...prev, ...{
[event.target.name]: event.target.value
}
}))
so I converted it into a functional one. In setTitle I store the value const [title, setTitle] = useState('');
setTitle(prev => ({
...prev, ...{
[event.target.name]:event.target.value
}
}))
This depends on how you are referencing value on your input. Given your current setTitle operation, if you are referencing title like:
<input type="text" name="title" onInput={handleInput} value={title} />
The problem is that you are turning title into an object with your setTitle operation. An object with property "title" such as { title: "some text" }. That then get's stringified into [object Object].
You could change setTitle to the following to keep it as a flat string:
setTitle(e.target.value)
Or you could change the structure of your state to be an object of form properties:
// create an object with properties to hold your form values
const [form, setForm] = useState({ title: '' });
function handleInput(e) {
setForm(prev => ({
...prev,
[e.target.name]: e.target.value,
}));
}
// reference specific property on form state object
<input type="text" onInput={handleInput} value={form.title} />
Hopefully that helps!

AntDesign Cascader: error Not found value in options

I am wanting to use the "Cascader" component of "Ant Design" but I am having trouble filling it with data. This is my code, which I am doing wrong, sorry I am still a newbie and I need your support please.
function CascaderEmpCliUn(props) {
const optionLists = { a: []}
const [state, setState] = useState(optionLists);
useEffect(() => {
async function asyncFunction(){
const empresas = await props.loginReducer.data.empresas;
const options = [
empresas.map(empresa => ({
value: empresa.id,
label: empresa.siglas,
children: [
empresa.cli_perm.map(cliente => ({
value: cliente.id,
label: cliente.siglas,
children: [
cliente.uunn_perm.map(un => ({
value: un.id,
label: un.nombre,
}))
]
}))
]})
)
];
setState({a : options})
}
asyncFunction();
}, [])
return (
<Cascader options={state.a} placeholder="Please select" />
)
}
ERROR
Not found value in options
I was able to reproduce your error with dummy data whenever I had an empty array of children at any level. I'm not sure why this should be a problem, but it is. So you need to modify your mapping function to check the length of the child arrays. It seems to be fine if passing undefined instead of an empty array if there are no children.
General Suggestions
You don't need to store the options in component state when you are getting them from redux. It can just be a derived variable. You can use useMemo to prevent unnecessary recalculation.
You are passing the entire loginReducer state in your props which is not ideal because it could cause useless re-renders if values change that you aren't actually using. So you want to minimize the amount of data that you select from redux. Just select the empresas.
Revised Code
function CascaderEmpCliUn() {
// you could do this with connect instead
const empresas = useSelector(
(state) => state.loginReducer.data?.empresas || []
);
// mapping the data to options
const options = React.useMemo(() => {
return empresas.map((empresa) => ({
value: empresa.id,
label: empresa.siglas,
children:
empresa.cli_perm.length === 0
? undefined
: empresa.cli_perm.map((cliente) => ({
value: cliente.id,
label: cliente.siglas,
children:
cliente.uunn_perm.length === 0
? undefined
: cliente.uunn_perm.map((un) => ({
value: un.id,
label: un.nombre
}))
}))
}));
}, [empresas]);
return <Cascader options={options} placeholder="Please select" />;
}
The final code of "options" object:
const options = useMemo(() => {
return empresas.map((empresa) => ({
value: empresa.id,
label: empresa.siglas,
children:
empresa.cli_perm.length === 0
? console.log("undefined")
:
empresa.cli_perm.map((cliente) => ({
value: cliente.id,
label: cliente.siglas,
children:
cliente.uunn_perm.length === 0
? console.log("undefined")
:
cliente.uunn_perm.map((un) => ({
value: un.id,
label: un.nombre
}))
}))
}));
}, [empresas]);

How Refresh Table component onChange Event

I have react component in which I am showing data in the table, also have Select / dropdown. The table and select are in same component. I need to refresh table component soon the value change in the dropdown. My implementation does refresh the table and call API but there is delay when that happened. I am not sure if I have implemented correctly?
the idea is eziSearchCriteria is in useState and onChange event I am assign value to it.
const MyComponent = () => {
const[eziSearchCriteria, setEziSearchCriteria] = useState<IEziStatusSearchCriteriaForm>();
const eziSitesStatusCovers = [
{ label: 'UNSCHEDULED', value: 'UNSCHEDULED' },
{ label: 'COVERED', value: 'COVERED' },
{ label: 'PART COVERED', value: 'PART COVERED' },
];
useEffect(() =>{
setInitialPageLoad(true);
setDefaultSearchCriteria();
},[]);
const handleSearchFilter = (event) =>{
if(event!=null){
eziSearchCriteria.coverStatus = event.value;
setEziSearchCriteria(eziSearchCriteria);
}
}
return (
<div>
<div className="searchFilter">
<Select
options={eziSitesStatusCovers}
onChange = {handleSearchFilter}
/>
</div>
{ eziSearchCriteria ? (
<TableItems
url={url}
apiUrl ={api.eziTrackerStatus}
columns={columns}
customParams= {eziSearchCriteria}
key={eziSearchCriteria.coverStatus}
></TableItems> ) : null}
</div>
);
};
....
export interface IEziStatusSearchCriteriaForm{
startTime: string,
endTime: string,
scheduleId?: number,
coverStatus: string
}
I have found the issue and answer.
I was changing one value in the object eziSearchCriteria but object is same/ reference variable and react state not considering to render on this change. I created new object and assign to eziSearchCriteria and it worked straight away.
const handleSearchFilter = (event) =>{
if(event!=null){
eziSearchCriteria.coverStatus = event.value;
var searchCriteria2 : IEziStatusSearchCriteriaForm = {
startTime: "12-08-2020", //MM:DD:YYYY
endTime: "12-09-2020",
schedAction_Active: "Active",
coverStatus: event.value
}
setEziSearchCriteria(searchCriteria2);
}
}

Resources