React state updating without reason - javascript

My problem is simple to understand.
I have an userlist in my state. Filled by a DataBase call.
And I also have a selecteduser filled when a element in the userlist is clicked.
When this element from userlist is clicked, a modal is opening with input with a default value filled by selecteduser.
I assigned onChange function to those input, then each change is saved in selecteduser.
But the problem is, each change is also saved in userlist and I really don't understand why.
This userlist doesn't have any setState in the code except for the data call from the database.
class Users extends Component {
constructor(props) {
super(props);
this.state = {
userlist: [],
selecteduser: [],
IsModalUserVisible: false
};
}
Then call and store data in userlist.
componentWillMount() {
db.collection("user")
.get()
.then(collection => {
this.setState({
userlist: collection.docs.map(doc => doc.data())
});
});
}
Here is the onChange from the input
handleChangeEmail(event) {
event.preventDefault();
const selectedUserUpdate = this.state.selecteduser;
selectedUserUpdate.email = event.target.value;
this.setState({
selecteduser: selectedUserUpdate
});
}
And here the function called onClick on a userlist element.
UserSelected(user) {
this.setState({
selecteduser: user,
IsModalUserVisible: true
});
}
The userlist.map to show the user list.
{this.state.userlist.map(user => {
return (
<UserItem
callback={() => this.UserSelected(user)}
key={user.email}
email={user.email}
/>
);
})}
And for finish, the user manage modal, opened on click to an user list element.
<ManageUserItem
isOpen={this.state.IsModalUserVisible}
email={this.state.selecteduser.email}
changeEmail={this.handleChangeEmail.bind(this)}
/>
So, when I change the email inside the input, I can see on background that the list also change. And I checked with a console.log(this.state.userlist) in the handleChangeEmail, I can see the userlist is updated too.
I wanted to be clear, but i hope it's not to long to read.
Thanks in advance :)

userlist: collection.docs.map(doc => doc.data()) creates a Javascript Array that at each index holds a memory reference point to the data returned by doc.data(). When you initialize the <UserItem> component, you are providing this component with the same memory reference in the userlist state Array. When a user is clicked, that same reference is passed through the callback prop method UserSelected and in that method the same memory reference is assigned to the state variable selecteduser. Later on when the email change is handled for that selected user, the method handleChangeEmail is operating on a memory reference stored in two places, the userlist Array and the selecteduser Object. When you update an attribute of an Object reference, any other place that Object is referenced will show such a mutation because they are pointing to the same underlying data. One minor alteration that I would suggest for this code is in the constructor method, initialize selecteduser to be an Object ({}) not an Array ([]) since selecteduser is eventually assigned an Object anyway and not an array. Finally, if you'd prefer that selecteduser not be the same Object reference as what is referenced in the userlist Array, you may create a new Object (and thus a new Object reference) from each Object referenced in the userlist Array. Like so:
{this.state.userlist.map(user => {
// this is one way to do it, you can find others elsewhere on SO for more complex cases.
// Look into, Javascript as a pass by reference language versus pass by value languages
// and their nuances.
const newObject = JSON.parse(JSON.stringify(user))
return (
<UserItem
callback={() => this.UserSelected(newObject)}
key={newObject.email}
email={newObject.email}
/>
);
})}

Related

Multiple submissions in one Textfield overrides the previous value that was saved

I have this form with multiple checkboxes and below it, I also have the others where the user can enter any value. The problem is that if I'll enter a value for the 2nd time, it will remove the previous value entered by the user.
Assuming that I've entered books for my first submit. Now, I want to submit another value for the others again, but this time it will be movies. I want to save in the firestore the both of these values; books and movies. The problem is that if I'll submit movies, this will override the previous one books, meaning it will replace books. How can I avoid that and at the same time display the multiple values entered by the user in the field others?
Below are the codes:
const sample = (props) => {
const [others, setOthers] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
try {
const user = firestore.collection("users").doc(id);
const ref = user.set(
{
1: {
choices,
others
}
},
{ merge: true }
);
console.log(" saved");
} catch (error) {
console.log(error);
}
};
return (
<>
<form onSubmit={handleSubmit}>
<FormGroup>
//codes for the checkboxes here
<TextField
type="text"
label="Others:"
value={others}
onChange={(e) => setOthers(e.target.value)}
multiline
/>
</FormGroup>
<button type="submit">Submit</button>
<br />
</form>
</>
);
};
export default sample;
im going to preface this with im not familiar with react, but i do mess around with firestore alot. so the syntax maybe different for you.
but the first thing i notice is that you're using const ref = user.set to make the document. this is fine for first time creating a document, but if you use '.set' on an existing document it will override all the data in that document with whatever you're attempting to update it with.
you should use const ref = user.update to update fields in the document.
the 2nd bit is lets say you want to update the 'others' field. it would still override the data in that field even if you use '.update'. update is doing just that, its updating the field in question with whatever you're trying to update it with. what you want to do is add to it.
so your 'others' field needs to be an array and in order to add new values into it without overriding the previous data you need to use an arrayUnion.
const handleSubmit = (e) => {
e.preventDefault();
try {
const user = firestore.collection("users").doc(id);
const ref = user.update(
{
1: {
choices,
others: firebase.firestore.FieldValue.arrayUnion(others),
}
},
{ merge: true }
);
console.log(" saved");
} catch (error) {
console.log(error);
}
};
now i dont know how imports work in react but in VUEjs you'd need to import import firebase from "firebase/compat/app"; in the script tag in order to use the that firebase feature.
if you want to remove an item from that others array then use.
const handleSubmit = (e) => {
e.preventDefault();
try {
const user = firestore.collection("users").doc(id);
const ref = user.update(
{
1: {
choices,
others: firebase.firestore.FieldValue.arrayRemove(item), //item = whatever it is you're trying to remove.
}
},
{ merge: true }
);
console.log(" saved");
} catch (error) {
console.log(error);
}
};
From the React docs,
this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state. To fix it, use a second form of setState() that accepts a function rather than an object. That function will receive the previous state as the first argument, and the props at the time the update is applied as the second argument.
prevState is a name to the argument passed to setState callback function. What it holds is the value of state before the setState was triggered by React.
So if multiple setState calls are updating the same state, batching setState calls may lead to incorrect state being set. Consider an example.
Your 'others' field needs to be an array and in order to add new values into it without overriding the previous data you need to use prevState.
If you don’t want to use setState you can use
prevState in useState React Hook with Javascript spread operator to
concatenate the previous values with current values in array
Something like this.

NextJS - can't identify where state change is occurring on page component when using shallow Router push

I have an app. that uses NextJS. I have a page that looks like the following:
import React from 'react'
import { parseQuery } from '../lib/searchQuery'
import Search from '../components/search'
class SearchPage extends React.Component {
static getInitialProps ({ query, ...rest }) {
console.log('GET INITIAL PROPS')
const parsedQuery = parseQuery(query)
return { parsedQuery }
}
constructor (props) {
console.log('CONSTRUCTOR OF PAGE CALLED')
super(props)
this.state = props.parsedQuery
}
render () {
return (
<div>
<div>
<h1>Search Results</h1>
</div>
<div>
<h1>DEBUG</h1>
<h2>PROPS</h2>
{JSON.stringify(this.props)}
<h2>STATE</h2>
{JSON.stringify(this.state)}
</div>
<div>
<Search query={this.state} />
</div>
</div>
)
}
}
export default SearchPage
getInitialProps is ran for SSR - it receives the query string as an object (via Express on the back end) runs it through a simple 'cleaner' function - parseQuery - which I made, and injects it into the page via props as props.parsedQuery as you can see above. This all works as expected.
The Search component is a form with numerous fields, most of which are select based with pre-defined fields and a few a number based input fields, for the sake of brevity I've omitted the mark up for the whole component. Search takes the query props and assigns them to its internal state via the constructor function.
On changing both select and input fields on the Search component this code is ran:
this.setState(
{
[label]: labelValue
},
() => {
if (!this.props.homePage) {
const redirectObj = {
pathname: `/search`,
query: queryStringWithoutEmpty({
...this.state,
page: 1
})
}
// Router.push(href, as, { shallow: true }) // from docs.
this.props.router.push(redirectObj, redirectObj, { shallow: true })
}
}
)
The intention here is that CSR takes over - hence the shallow router.push. The page URL changes but getInitialProps shouldn't fire again, and subsequent query changes are handled via componentWillUpdate etc.. I confirmed getInitialProps doesn't fire again by lack of respective console.log firing.
Problem
However, on checking/unchecking the select fields on the Search component I was surprised to find the state of SearchPage was still updating, despite no evidence of this.setState() being called.
constructor isn't being called, nor is getInitialProps, so I'm unaware what is causing state to change.
After initial SSR the debug block looks like this:
// PROPS
{
"parsedQuery": {
"manufacturer": [],
"lowPrice": "",
"highPrice": ""
}
}
// STATE
{
"manufacturer": [],
"lowPrice": "",
"highPrice": ""
}
Then after checking a select field in Search surprisingly it updates to this:
// PROPS
{
"parsedQuery": {
"manufacturer": ["Apple"],
"lowPrice": "",
"highPrice": ""
}
}
// STATE
{
"manufacturer": ["Apple"],
"lowPrice": "",
"highPrice": ""
}
I can't find an explanation to this behaviour, nothing is output to the console and I can't find out how to track state changes origins via dev. tools.
Surely the state should only update if I were to do so via componentDidUpdate? And really shouldn't the parsedQuery prop only ever be updated by getInitialProps? As that's what created and injected it?
To add further confusion, if I change a number input field on Search (such as lowPrice), the URL updates as expected, but props nor page state changes in the debug block. Can't understand this inconsistent behaviour.
What's going on here?
EDIT
I've added a repo. which reproduces this problem on as a MWE on GitHub, you can clone it here: problem MWE repo.
Wow, interesting problem. This was a fun little puzzle to tackle.
TL;DR: This was your fault, but how you did it is really subtle. First things first, the problem is on this line:
https://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L17
Here in this example, it is this:
this.state = props.parsedQuery
Let's consider what is actually happening there.
In IndexPage.getInitialProps you are doing the following:`
const initialQuery = parseQuery({ ...query })
return { initialQuery }
Through Next's mechanisms, this data passes through App.getInitialProps to be returned as pageProps.initialQuery, which then becomes props.initialQuery in IndexPage, and which is then being passed wholesale through to your Search component - where your Search component then "makes a copy" of the object to avoid mutating it. All good, right?
You missed something.
In lib/searchQuery.js is this line:
searchQuery[field] = []
That same array is being passed down into Search - except you aren't copying it. You are copying props.query - which contains a reference to that array.
Then, in your Search component you do this when you change the checkbox:
const labelValue = this.state[label]
https://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L57
You're mutating the array you "copied" in the constructor. You are mutating your state directly! THIS is why initialQuery appears to update on the home page - you mutated the manufacturers array referenced by initialQuery - it was never copied. You have the original reference that was created in getInitialProps!
One thing you should know is that even though getInitialProps is not called on shallow pushes, the App component still re-renders. It must in order to reflect the route change to consuming components. When you are mutating that array in memory, your re-render reflects the change. You are NOT mutating the initialQuery object when you add the price.
The solution to all this is simple. In your Search component constructor, you need a deep copy of the query:
this.state = { ...cloneDeep(props.query) }
Making that change, and the issue disappears and you no longer see initialQuery changing in the printout - as you would expect.
You will ALSO want to change this, which is directly accessing the array in your state:
const labelValue = this.state[label]
to this:
const labelValue = [...this.state[label]]
In order to copy the array before you change it. You obscure that problem by immediately calling setState, but you are in fact mutating your component state directly which will lead to all kinds of weird bugs (like this one).
This one arose because you had a global array being mutated inside your component state, so all those mutations were being reflected in various places.

Why does state not show a newly created empty object? Found workaround

I'm using react 16.3.0-alpha.1 and having an issue adding an empty object, with the key "items", to a list object.
Addlist.js
createList = event => {
//Stop form from submitting
event.preventDefault();
const list = {
items: {},
title: this.titleRef.current.value
};
this.props.addList(list); //This method lives in app.js
// Clear the form after submitted
event.currentTarget.reset();
};
App.js
state = {
lists: {}
};
//Add new list with title and empty items object
addList = list => {
// Take a copy of current state
const lists = { ...this.state.lists };
// Add new list to the lists collection
lists[`${list.title}`] = list;
console.log(lists);
// Set the new lists collection to state
this.setState({ lists: lists });
};
When the createList event fires it calls the addList method on app.js and passes the list object to the method. When I console log lists right before setState I can see that "list3" has been added with "items" and "title". But when I used the dev tools to view state it only shows the key "title" with its value. I also get an error stating that the key "items" on "list3" is null or undefined.
What am I doing wrong here?
Does react remove empty objects on setState, is it a shallow copy problem, or something else?
So I found a workaround but I still don't know why I cant add an empty object.
For the workaround I changed list.items = {} to list.items = "" . The empty string did get added this time. Then later in a method where I add a new item to "list.items" I first checked to see if it was a string. If it was a string then I set "list.items" from a string to an empty object. This allowed me to continue adding nested objects. This step also happened right before setState.
Let me know if you know what caused this.

React- FLUX - Pass Object as Param - Undefined

I have a simple react app, I get a list of contacts from a web api and i want to display them. Each contact has a name, last name, phone etc
My app class gets the contacts then renders as
render(){
var contact= this.state.contacts[0];
return( <div><Contact label='First Contact' person={contact}/></div>
)
}
Then in my Contact class
render(){
return(
<div> {this.props.label} : {this.props.person.Name}</div>)
}
When I debug on chrome, I see that person is passed as prop, object has all the parameters, however when I run the code, on the {this.props.person.Name} I get error
Cannot read property Name of undefined
If I remove that, {this.props.label} is displayed without issue. So I can pass text as prop but not the object.
Any idea?
Edit: I can also pass the Name property as
Contact personName= {contact.Name}/>
This works but not passing the contact as object and then reading properties in the render of
my guess is (since you're using flux) that upon loading the page (initial load) the contacts on your state represents an empty array.
Try
var contact= this.state.contacts[0] || {};
and for some good tips => don't use vars, use const :-)
let your component listen to your flux store:
Make sure your flux store holds an addChangeListener and removeChangeListener function that you can call in your component so your component gets updated automatically
componentDidMount(){
myStore.addChangeListener(this._onChange);
}
componentWillUnmount(){
myStore.removeChangeListener(this._onChange);
}
_onChange = () => {
this.setState({contacts: myStore.getContacts()});
}

Attempt to render state value from ajax call in componentDidMount failing (React)

I've made an AJAX call in React with Axios, and I'm a bit confused about how the response is dealt with.
Here is my code:
componentDidMount() {
axios.get('https://jsonplaceholder.typicode.com/users')
.then( res => {
const users = res.data
this.setState({ users });
});
}
render() {
console.log(this.state.users)
return (
<div>
{this.state.users.map( user => <p>{user.name}</p>)}
</div>
)
}
When I console.log the results, two values are returned:
[] - the empty array assigned to the initial state
The expected array from the API.
This presumably can be explained by the state value being returned once on initialisation, and then again with componentDidMount.
My problem arises in how I access the this.state.users value. Obviously any time I access this, I want the values returned from the AJAX response, but if, for example I try to render the name property from the first object in the array...
{this.state.users[0].name}
...it attempts to access [], and thus returns nothing.
However, if I try to iterate through the arrays elements...
{users.map( user => <p>user.name</p> )}
...it returns the values as expected.
I don't know why it works with the second example but not the first. Surely if the first is attempting to pull data from the initial state value (an empty array), then when mapping through this.state.users it would also be attempting to map through an empty array and return nothing, or an error.
I'm obviously not fully understanding the React rendering process here and how componentDidMount works in the component lifecycle. Am I wrong in trying to assign the response value directly to state?
Would appreciate if anyone could help to clear this up.
When you use map() on this.state.users initially, it will do so over an empty array (which won't render anything). Once the response arrives, and this.setState({ users }) is called, render() will run again. This time, the array of users is no longer empty and the map() operation will be performed on each of the users in the array.
However, when you use {this.state.users[0].name} initially on the empty array, you're trying to access the name property of an object that doesn't yet exist — which will result in an error. To prevent the error, you could do a check before accessing the properties of the object:
{this.state.users.length > 0 && this.state.users[0].name}
Now it will behave the same way as in the first example. I.e. the first render() won't actually do anything, since this.state.users.length = 0. But as soon as this.setState() is called with new users, render() will run again, and this time this.state.users[0].name is available.
There is another way with constructor. just add your state with empty array users as-
constructor(props) {
super(props);
this.state = {
users: []
};
}
With this component first rendered with empty users array. When you trigger state update, component rerender with new array.

Categories

Resources