ReactJS: [ this.props ] being impossibly different to [ props in this ] - javascript

In a continuous section of code called inside componentDidMount:
console.log('hv props');
for(var i = 0; i<20; i++){
console.log(this);
console.log(this.props);
}
console.log('hv props end');
this is supposed to be having this.props.account.
in all
console.log(this);
this.props.account is an object with fields.
However, in all
console.log(this.props);
this.props.account is null. (the account field of this.props is null)
I have placed this section in different parts of my program, and i have tried adding a time consuming process between console.log(this) and console.log(this.props), but this is still the case.
What could have caused this? Thank you.

console.log won't necessarily show you the structure the object has at the time it is logged, but the structure it has when you clicked the triangle to inspect it. In the meantime (between logging and inspecting in the console) the component might have been rerendered with new props. See Can't iterate over my array/object. Javascript, React Native for basically the same problem.
Here is an example that demonstrates the issue (open your browser console):
var obj = {props: {account: null}};
console.log(obj);
console.log(obj.props);
obj.props = {account: 'not null' };
Note how it shows account: "not null" even though the second log shows account: null:
I bet if you delay the evaluation of console.log(this.props), e.g. via
setTimeout(() => console.log(this.props), 2000)
you would see the value you want.
This was just an explanation of what you are seeing. How you want to solve probably depends on the rest of your application. You certainly can't access this.props.account on mount, but you can do on subsequent renders. Keep in mind that componentDidMount is only called when the component is rendered/mounted the first time, not on subsequent renders. Have a look at the other lifecycle methods.

Related

React: Missing property in state object

So I started learning React and after doing some basics (counters, todo lists) I decided to do something more challenging; a chessboard with pieces that can be moved around by clicking. My main component is Chessboard. It then renders Tiles that may contain a Piece. Information about the game is stored in Chessboard's state, looking like this:
interface State {
board: Record<string, pieceInterface>;
isPieceMoving: boolean;
movingPieceId: string;
}
I handle moving pieces in Chessboard's method onTileClick(id: string). It is passed to a Tile as a prop and when a tile is clicked it's called with a tile id (like "a4", "f6", "h3"). It has following logic. Game can be in two states: I can be currently moving a piece or I can currently do nothing and then start moving a piece by clicking on it. When I start moving a piece I store it's ID (or rather an ID of a tile on witch piece stands) in state.movingPieceId. When I try to place it I check if the tile is empty and then change state.board accordingly. Here is my code:
onTileClick = (id: string): void => {
if (!this.state.isPieceMoving) {
if (this.state.board[id]) {
this.setState({
isPieceMoving: true,
movingPieceId: id,
});
}
} else {
if (!this.state.board[id]) {
this.setState((state) => {
let board: Record<string, pieceInterface> = state.board;
const piece: pieceInterface = board[state.movingPieceId];
delete board[state.movingPieceId];
board[id] = piece;
// console.log(board[id]);
// console.lob(board);
const isPieceMoving: boolean = false;
const movingPieceId: string = "";
return { board, isPieceMoving, movingPieceId };
});
}
}
};
The first part works just fine. But in the second there is some bug, that I cannot find. When I uncomment this console logs the output looks like this (I want to move a piece from "a2" to "a3"):
Object { "My piece representation" }
Object {
a1: { "My piece representation" },
a3: undefined,
a7: { "My piece representation" },
...
}
My code properly "takes" clicked piece from "a2" (board has no "a2" property) but it apparently does not place it back. Even when printing board[id] gives correct object! I was moving logging line console.log(board) up to see where bug happens. And even when I put it directly behind else it still was saying that "a3" was undefined.
I spend hours trying to understand what was happening. I tried to emulate this fragment of code in console, but there it always worked! Can anybody explain to me what am I doing wrong? Is this some weird React's setState mechanism that I haven't learned yet? Or is it some basic javascript thing, that I am not aware of? Any help would we wonderful, because I cannot move on and do anything else while being stuck here.
EDIT #1
Ok. So I was able to fix this bug by deep copying state.board into board (inspiredy by #Linda Paiste's comment). I just used simple JSON stringify/parse hack:
let board: Record<string, pieceInterface> = JSON.parse(JSON.stringify(state.board));
This fixes the bug but I have no idea why. I will keep this question open, so maybe someone will explain to me what and why was wrong with my previous code.
EDIT #2
Ok. Thanks to #Linda's explanation and reading the docs i finally understood what I was doing wrong. Linda made detailed explanation in an answer below (the accepted one). Thank you all very much!
Problem: Mutation of State
let board: Record<string, pieceInterface> = state.board;
You are creating a new variable board that refers to the same board object as the one in your actual state. Then you mutate that object:
delete board[state.movingPieceId];
board[id] = piece;
This is an illegal mutation of state because any changes to board will also impact this.state.board.
Solution: Create a New Board
Any array or object that you want to change in your state needs to be a new version of that object.
Your deep copy of board works because this new board is totally independent of your state so you can mutate it without impacting the state. But this is not the best solution. It is inefficient and will cause unnecessary renders because every piece object will be a new version as well.
We just need to copy the objects which are changing: the state and the board.
this.setState((state) => ({
isPieceMoving: false,
movingPieceId: '',
board: {
// copy everything that isn't changing
...state.board,
// remove the moving piece from its current position
[state.movingPieceId]: undefined,
// place the moving piece in its new location
[id]: state.board[state.movingPieceId],
}
}));
Your typescript type for the board property of State should be Partial<Record<string, pieceInterface>> because not every slot on the board has a piece in it.
Very ugly but functional Code Sandbox demo
Edit: About the setState Callback
I heard that using setState with a function makes state a copy of this.state, so I can mutate it.
That is incorrect. You should never mutate state and this is no exception. The advantage of using a callback is that the state argument is guaranteed to be the current value, which is important because setState is asynchronous and there might be multiple pending updates.
Here's what the React docs say (emphasis added):
state is a reference to the component state at the time the change is being applied. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from state and props."
Both state and props received by the updater function are guaranteed to be up-to-date.

Mutation payload changes value by itself in vuex store mutation

I'm trying to build an Electron app with VueJS using the electron-vue boilerplate. I have a mutation which updates parts of the state based on the payload it receives.
However, somewhere between the action call and the mutation, the property payload.myItem.id changes without any intention.
The action is called by a Vue modal component:
handleModalSave() {
let payload = {
layerId: this.layer.id,
myItem: {
id: this.editLayerForm.id,
text: this.editLayerForm.text
}
}
console.log('save', payload.myItem.id)
this.$store.dispatch('addLayerItem', payload)
}
Here are said action and mutation:
// Action
addLayerItem: ({ commit }, payload) => {
commit('ADD_LAYER_ITEM', payload)
}
// Mutation
ADD_LAYER_ITEM: (state, payload) => {
console.log('mutation', payload.myItem.id)
let layer = state.map.layers.find(layer => layer.id === payload.layerId)
if (payload.myItem.id !== '') {
// Existing item
let itemIdx = layer.items.findIndex(item => item.id === payload.myItem.id)
Vue.set(layer.items, itemIdx, payload.myItem)
} else {
// New item
payload.myItem.id = state.cnt
layer.items.push(payload.myItem)
}
}
Here is a screenshot of the console logs:
As far as I can see, there is no command to change myItem.id between console.log('save', payload) and console.log('mutation', payload). I use strict mode, so there is no other function changing the value outside of the mutation.
Edit
I updated the console.logs() to display the property directly instead of the object reference.
As far as I can see, there is no command to change myItem.id between console.log('save', payload) and console.log('mutation', payload).
It doesn't need to change between the console logging.
In the pictures you've posted you'll see a small, blue i icon next to the console logging. If you hover over that you'll get an explanation that the values shown have just been evaluated.
When you log an object to the console it grabs a reference to that object. It doesn't take a copy. There are several reasons why taking a copy is not practical so it doesn't even try.
When you expand that object by clicking in the console it grabs the contents of its properties at the moment you click. This may well differ from the values they had when the object was logged.
If you want to know the value at the moment it was logged then you could use JSON.stringify to convert the object into a string. However that assumes it can be converted safely to JSON, which is not universally true.
Another approach is to change the logging to directly target the property you care about. console.log(payload.myItem.id). That will avoid the problem of the logging being live by logging just the string/number, which will be immutable.
The line that changes the id appears to be:
payload.myItem.id = state.cnt
As already discussed, it is irrelevant that this occurs after the logging. The console hasn't yet grabbed the value of the property. It only has a reference to the payload object.
The only mystery for me is that the two objects you've logged to the console aren't both updated to reflect the new id. In the code they appear to be the same object so I would expect them to be identical by the time you expand them. Further, one of the objects shows evidence of reactive getters and setters whereas the other does not. I could speculate about why that might be but most likely it is caused by code that hasn't been provided.
I use strict mode, so there is no other function changing the value outside of the mutation.
Strict mode only applies to changing properties within store state. The object being considered here is not being held in store state until after the mutation. So if something were to change the id before the mutation runs it wouldn't trigger a warning about strict mode.
Okay I found the root cause for my issue. Vuex was configured to use the createPersistedState plugin which stores data in local storage. Somehow, there was an issue with that and data got mixed up between actual store and local storage. Adding a simple window.localStorage.clear() in main.js solved the problem!

Getting React errors when traversing an in-scope JSON object via Dot Notation [duplicate]

This question already has answers here:
How to handle calling functions on data that may be undefined?
(2 answers)
Closed 3 years ago.
render() {
return (
<p>{this.state.recipes[0].title}</p> // This is what I believe should work - Ref Pic #1
// <p>{this.state.recipes[0]}</p> // This was trying random stuff to see what happens - Ref Pic #2
// <p>{this.state.recipes.title}</p> // This was me trying crazy nonsense - Ref Pic #3
)
}
I am attempting to traverse through some JSON and am getting some very wonky responses. If anyone would like to look at the JSON themselves, it's available at this link.
When the first p tag is run, I get the following response:
This is my first question, so I can't embed images, I'm sorry.
Being unsure why it said recipes[0] was undefined, I ran it again with the second p tag soley uncommented, to which I get the following response: Still the same question, still can't embed, sorry again.
That response really caught me off guard because the keys it reference's (nid, title, etc..) are the ones I know are in the object. 'Title' is what I want.
Last, I tried just the third p tag, to which the app actually compiled with no errors, but the p tag was empty. Here's what I have in React Devtools, but I still can't embed.
Looking at < Recipes > it clearly shows that state has exactly what I want it to, and image two shows recipes[0] has the key I try to call the first time. I have searched and Googled and even treated my own loved ones as mere rubberducks, but to no avail.
What am I doing wrong?
The error in the first p tag tells you what exactly is happening. Accessing a property of an 'undefined' will break javascript/ react.
You must probably not have the expected data in the state, to verify what I am saying, simply debug by console.log(this.state) in your render:
render() {
console.log(this.state); // probably has this.state.recipes empty array at first render
...
this probably happened because your state is being populated by an async call from an api. Render will be called first before your async request from an api resolves (which means unfortunately you don't have the ..recipes[0] data yet in your state)
I am guessing you have this in your componentDidMount:
componentDidMount() {
ThirdParty.fetchRecipes().then(data => {
this.setState({ recipes: data.recipes }); // after setState react calls render again. This time your p tag should work.
})
...
If you have newer react version I would recommend to use optional chaining because the property you are interested in is deeply nested: How to handle calling functions on data that may be undefined?
<p>{this.state?.recipes?.[0]?.title}</p>
Another effective way but too wordy:
const title =
this.state &&
this.state.recipes &&
this.state.recipes.length &&
this.state.recipes[0].title
? this.state.recipes[0].title
: "";
render() {return (<p>{title}</p>)}
I believe you are fetching the JSON asynchronously. In that case you should consider boundary cases where your initial recipes state is an empty array.
When you run the first p tag, since JSON is fetched asynchronously, initially this.state.recipes is not defined, hence you get the error.
When you run the second p tag, during the first render, this.state.recipes is undefined, hence it works inside p tag, but after the JSON is fetched and state is set, render is called again and this time this.state.recipes[0] is an object, hence it can't be counted as a valid react component, thus the error shows up.
With the 3rd p tag, as you have mentioned yourself, this.state.recipes.title will compile successfully with no errors, but the value will be undefined.
You just need to check for the edge cases where initial state is empty.
constructor(props) {
super(props);
this.state = { recipes: [] }
}
render() {
return (
<p>
{this.state.recipes.length > 0 ? {this.state.recipes[0].title} : ''}
</p>
)
}
you can do something like this to handle state when there is data and when there is no data
constructor(props) {
super(props);
this.state = {
recipes: []
};
}
render() {
return (
<p>
{this.state.recipes && this.state.recipes.length ? {this.state.recipes[0].title} : {this.state.recipes}}
</p>
)
}

Parsing JSON objects array and displaying it in ReactJS

I've been facing a weird issue lately with my React App. I'm trying to parse a JSON object that contains arrays with data. The data is something like this:
{"Place":"San Francisco","Country":"USA", "Author":{"Name":"xyz", "Title":"View from the stars"}, "Year":"2018", "Places":[{"Price":"Free", "Address":"sfo"},{"Price":"$10","Address":"museum"}] }
The data contains multiple arrays like the Author example I've just shown. I have a function that fetches this data from a URL. I'm calling that function in componentDidMount. The function takes the data i.e responseJson and then stores it in an empty array that I've set called result using setState. In my state I have result as result:[]. My code for this would look something like this:
this.setState({result:responseJson})
Now, when I've been trying to access say Author Name from result I get an error. So something like this:
{this.state.result.Author.Name}
I'm doing this in a function that I'm using to display stuff. I'm calling this function in my return of my render function. I get an error stating :
TypeError:Cannot read property 'Name' of undefined. I get the same error if I try for anything that goes a level below inside. If I display {this.state.result.Place} or {this.state.result.Country} it's all good. But if I try,say {this.state.result.Author.Title} or {this.state.result.Places[0].Price} it gives me the same error.
Surprising thing is I've parsed this same object in a different component of mine and got no errors there. Could anyone please explain me why this is happening?
If I store the individual element while I setState in my fetch call function, I can display it. For example:
{result:responseJson,
AuthorName:responseJson.Author.Name
}
Then I'm able to go ahead and use it as {this.state.AuthorName}.
Please help me find a solution to this problem. Thanks in advance!
It could be that your state object is empty on the first render, and only updated with the data from the API after the request has completed (i.e. after the first render). The Name and Place properties don't throw an error, as they probably resolve to undefined.
Try putting an if block in your render method to check if the results have been loaded, and display a loading indicator if they haven't.
I'm guessing your initial state is something like this:
{ results: {} }
It's difficult to say without seeing more code.
[EDIT]: adding notes from chat
Data isn't available on first render. The sequence of events rendering this component looks something like this:
Instantiate component, the initial state is set to { results: [] }
Component is mounted, API call is triggered (note, this asynchronous, and doesn't return data yet)
Render method is called for the 1st time. This happens BEFORE the data is returned from the API request, so the state object is still {results: [] }. Any attempts to get authors at this point will throw an error as results.Authors is undefined
API request returns data, setState call updates state to { results: { name: 'test', author: [...] } }. This will trigger a re-render of the component
Render method is called for the 2nd time. Only at this point do you have data in the state object.
If this state evolves, means it is changed at componentDidMount, or after a fetch or whatever, chances are that your state is first empty, then it fills with your data.
So the reason you are getting this error, is simply that react tries to get this.state.result.Author.Name before this.state.result.Author even exists.
To get it, first test this.state.result.Author, and if indeed there's something there, then get Author.Name like this.
render(){
return(
<div>
{this.state.result.Author ? this.state.result.Author.Name : 'not ready yet'}
</div>
);
}
[EDIT] I'll answer the comment here:
It's just because they are at a higher level in the object.
this.state.result will always return something, even false if there is no result key in your state (no result key in your constructor for instance when the component mounts).
this.state.result.Country will show the same error if result is not a key of your state in your constructor. However, if result is defined in your constructor, then it will be false at first, then become the data when the new state populates.
this.state.result.Author.Name is again one level deeper...
So to avoid it, you would have to define your whole "schema" in the constructor (bad practice in my opinion). This below would throw no error when getting this.state.result.Author.Name if I'm not mistaken. It would first return false, then the value when available.
constructor(props){
super(props);
this.state={
result: {
Author: {}
}
}
}

React - this.state is empty in render when getInitialState is properly defined

I'll try my best to explain the scenario, as making a fiddle of the smallest possible case might be hard. Essentially, I have a SPA using React-Router. I'm currently getting a strange behavior in specifically one version of Firefox (31.4esr).
I have two sidebar icons which trigger a change in routes, navigating to a new page. On occasion when I switch quickly between them, I'm getting an error that this.state.list is undefined(this is a list that I populate a dropdown with).
The issue is, upon debugging, console.log(this.state) is returning an empty object just before the call (that errors) to this.state.list happens in my render method. However, I have list defined in getInitialState (along with a bunch of other state variables) and so this.state definitely shouldn't be empty.
The only thing I could think of that would be causing this is if due to the quick switching there is some confusion with mounting/unmounting of components and my component still thinks it is mounted, so skips the getInitialState and goes ahead and tries to render. Either that or some bug in React-Router.
Any thoughts? Thanks in advance for the help!
Nick
P.S I should reiterate this also only occurs very rarely during quick switching, and normally the componentDidMount -> getInitialState -> render occurs as expected, so it is not simply an error in how my getInitialState is written etc.
Edit: Using React 0.13.3 and React router 0.13.3
Edit 2: Here is the stripped down version of the lifecycle methods, very basic.
getInitialState: function() {
return { list: listStore.getList("myList") || [] }
},
render: function() {
var newList = [];
//this is the line that errors with this.state.list is undefined
this.state.list.forEach(function(listItem) {
...
}
return (
<div>
<OtherComponent newList={newList} />
</div>
)
};
When putting console.log in componentWillMount (just attaches store listeners), getInitialState, and render, I get output like this when the error occurs:
"Setting initial state"
"2,3" //This is this.state.list in componentWillMount
"2,3" //This is this.state.list in initial Render
Object { } //This is this.state the next time it gets called in render :S.
you will have to use mixin like this:
var state = {
getInitialState: function() {
return { list: listStore.getList("myList") || [] }
}
}
var nameRouter = React.createClass({
mixins : [state],
// more content
})
this is because react-router ignore the getInitialState that was definite

Categories

Resources