Weird Reactjs state behaviour when accessing it from outside render - javascript

I'm working on a simple drawing example, where the user can draw straight lines by clicking and dragging.
One of the features I want to implement is the ability to delete a line by double clicking on it, the problem I have is removing such line from the state array, I have a bare bones example here.
Unfortunately, when I try to console.log() the state array allPaths the app freezes but that doesn't happen on my local setup.
For example:
User draw 4 lines
On double click on the first line the console logs, its ID and empty state array[]
On double click the fourth line the log displays line ID and state array of length 3, that contains the first 3 lines, but it does not show the entire array... ?
On my local setup, when I console.log(allPaths) on doubleclick I get the allPaths state before that particular lines has been created, it doesn't show all entries, but when I log in the render method it shows all entries fine.
I have no idea what I'm doing wrong ...

In every render cycle you create new layers, instead use a ref to keep track of the layer, preventing the creation of new ones.
Make the following changes:
const markersLayer = useRef(new Paper.Group());
const floorplanLayer = useRef(new Paper.Group());
const trashAllPaths = () => {
setAllPaths([]);
};
const removePath = (id) => {
setAllPaths((paths) => paths.filter((p) => p.id !== id));
};
const savePath = (path) => {
setAllPaths((paths) => [...paths, path]);
};
markersLayer.current.addChildren([ghostLine]);
useEffect(() => {
floorplanLayer.current.removeChildren();
floorplanLayer.current.addChildren(allPaths);
}, [allPaths, floorplanLayer]);

The reason it freezes is because Path is a heavy object. For what I can see, you have allPaths, which is an array of objects that contain the property id. I can also see that on a path double click you get the path id.
So, in order to delete a line, I assume this would work:
const removeLine = (id) => {
const allPathsCopy = [...allPaths]
setAllPaths(allPathsCopy.filter(path => path.id !== id));
}

Related

React useState doesn't update even with useEffect added

Probably it is a classic issue with useState which is not updating.
So there is a tree with some checkboxes, some of them are already checked as they map some data from an endpoint.
The user has the possibility to check/uncheck them. There is a "cancel" button that should reset them to the original form.
Here is the code:
const [originalValues, setOriginalValues] = useState<string[]>([]);
...
const handleCancel = () => {
const originalValues = myData || []; //myData is the original data stored in a const
setOriginalValues(() => [...myData]);
};
...
useEffect(() => {
setOriginalValues(originalValues);
}, [originalValues]);
However, it is not working, the tree is not updating as it should. Is it something wrong here?
Just do the following, no need for ()=> the state will update inside the hook if called, plus change the constant it will cause confusion inside your code and protentional name clash later on, with the current state variable name, and also make sure your data are there and you are not injection empty array !!!! which could be the case as well !.
// Make sure data are available
console.log(myData)
// Then change the state
setOriginalValues([...myData]);

React setState, using array methods in callback function

I am currently following a React course on Scrimba on creating a web app for taking notes.
The problem requires me to bump a note to the top of the note list every time it's updated.
The notes are initialised through useState as follows:
const [notes, setNotes] = useState([])
The array consists of the individual notes as objects with an id and body
Every time an onChange is triggered, the following function is ran:
function updateNote(text) {
setNotes(oldNotes => {
let updated = oldNotes.map(oldNote => {
return oldNote.id === currentNoteId
? { ...oldNote, body: text }
: oldNote
})
const currNoteIndex = updated.findIndex(
note => note.id === currentNoteId
)
console.log(currNoteIndex)
updated.unshift(updated.splice(currNoteIndex, 1))
return updated
})
}
However, I keep getting an error as shown in the image.
It's very unclear to me where the problem lies, but I'm thinking it has to do with the array methods.
Any explanation for this issue would be greatly appreciated!
Credits to jsN00b for the answer:
array.splice returns an array, not the object.
Since that array is inserted to the start of the array containing the objects, there will be an error when updateNote() is called again.

Weird behavior on array map after state update

Hope you're all keeping well in the current climate!
I'm working with Firebase and ReactJS at the moment and have encountered an odd behavior with the state update in react and the array map functionality in JavaScript. The code below shows my logic for listening to updates from the firestore database:
listenForContentChanges = async () => {
let content = [];
await db.collection('clients').doc(this.props.client)
.collection('projects').doc(this.props.id)
.collection('content').orderBy('created').onSnapshot(querySnapshot => {
querySnapshot.docChanges().forEach(async change => {
if (change.type === 'added') {
content.push({
id: change.doc.id,
uploaded: change.doc.data().created.seconds
});
}
if (change.type === 'removed') {
content = content.filter(arrayItem => arrayItem.id !== change.doc.id);
}
});
this.setState({
content: content,
addingContent: false
});
});
}
As you can see, an array is populated with 'content' information just the ID of the document and a field containing the time in seconds of when that document was created. This gives me an array back:
0: {id: "SZ4f0Z27rN2MKgXAlglhZVKDsNpKO6", uploaded: 1586323802}
I need this array sorted so the most recent document comes first and because Firebase doesn't offer this as a query parameter (you can only sortBy and not change the direction) I copy the array to a new array and then loop over that instead to get everything in the correct order:
const sortedArr = [...this.state.content];
sortedArr.reverse();
/// IN THE RETURN: ///
{sortedArr.map((content, index) => (
<Content key={index} />
))}
This works okay with no issues. My problem is that now when a new element is added/one is taken from the state array this.state.content and the component is re-rendered, I am seeing a weird behavior where the last elements, instead of showing the new data, seem to duplicate themselves. Please see an example timeline below:
As you can see, when there is a new document added in firestore, the code shown above fires correctly, pushing a new array element onto the state and re-rendering the component. For those who are interested, yes the state is being correctly updated, this is what is being logged inside the render function after the state update:
0: {id: "x07yULTiB8MhR6egT7BW6ghmZ59AZY", uploaded: 1586323587}
1: {id: "SZ4f0Z27rN2MKgXAlglhZVKDsNpKO6", uploaded: 1586323802}
You can see there index 1 is new which is the document that has just been added. This is then, reversed and mapped in the return() function of the render() but causes the weird behavior shown in the image above. Any help on what may be causing this would be a great help, I've been banging my head against the wall for a while now!
Using array index as react key fails when the rendered array is mutated (i.e. unstable). React sees the new length, but the key for the first element is still the same so it bails on rerendering (even though it's the new element).
Try instead to always use unique keys within your dataset, like that id property.
{sortedArr.map((content, index) => (
<Content key={content.id} />
))}

Javascript saying object value is undefined. What am I doing wrong?

I'm trying to display the value of an object within an array of Github repositories (retrieved from a parsed JSON) in my React app but I'm getting an error that the browser can't read property key of undefined.
I'm able to console.log the JSON data and the objects within it but if I try to log the values within those objects, I get the error.
const App = () => {
const [repoList, setRepoList] = useState({});
useEffect(() => {
fetch("https://example.com/api")
.then(res => res.json())
.then(data => setRepoList(data));
});
return (
<div>{repoList[0].id}</div>
)
}
If I put JSON.stringify(repoList) within the div element, it shows the data.
If I put JSON.stringify(repoList[0]) within the div element, it also shows the data.
But if I put something like repoList[0].id nothing appears. This happens with each key listed. And I can confirm that the key is there. What am I doing wrong?
Try this.
const [repoList, setRepoList] = useState([]); // Change to array not object.
...
return repoList.length ?
(<React.Fragment>{repoList[0].id}</React.Fragment>)
: (<span>Loading ....</span>)
If your div does not have styles, better user Fragment, it result in better performance.
You're calling async functions to populate the repo list. The first time the return tries to use repoList[0].id, there is no repoList[0]. You may want to initialize repoList to a non-error-ing dummy object when you set it up with useState({})

Firebase child_added for new items only

I am using Firebase and Node with Redux. I am loading all objects from a key as follows.
firebaseDb.child('invites').on('child_added', snapshot => {
})
The idea behind this method is that we get a payload from the database and only use one action to updated my local data stores via the Reducers.
Next, I need to listen for any NEW or UPDATED children of the key invite.
The problem now, however, is that the child_added event triggers for all existing keys, as well as newly added ones. I do not want this behaviour, I only require new keys, as I have the existing data retrieved.
I am aware that child_added is typically used for this type of operation, however, i wish to reduce the number of actions fired, and renders triggered as a result.
What would be the best pattern to achieve this goal?
Thanks,
Although the limit method is pretty good and efficient, but you still need to add a check to the child_added for the last item that will be grabbed. Also I don't know if it's still the case, but you might get "old" events from previously deleted items, so you might need to watch at for this too.
Other solutions would be to either:
Use a boolean that will prevent old added objects to call the callback
let newItems = false
firebaseDb.child('invites').on('child_added', snapshot => {
if (!newItems) { return }
// do
})
firebaseDb.child('invites').once('value', () => {
newItems = true
})
The disadvantage of this method is that it would imply getting events that will do nothing but still if you have a big initial list might be problematic.
Or if you have a timestamp on your invites, do something like
firebaseDb.child('invites')
.orderByChild('timestamp')
.startAt(Date.now())
.on('child_added', snapshot => {
// do
})
I have solved the problem using the following method.
firebaseDb.child('invites').limitToLast(1).on('child_added', cb)
firebaseDb.child('invites').on('child_changed', cb)
limitToLast(1) gets the last child object of invites, and then listens for any new ones, passing a snapshot object to the cb callback.
child_changed listens for any child update to invites, passing a snapshot to the cb
I solved this by ignoring child_added all together, and using just child_changed. The way I did this was to perform an update() on any items i needed to handle after pushing them to the database. This solution will depend on your needs, but one example is to update a timestamp key whenever you want the event triggered. For example:
var newObj = { ... }
// push the new item with no events
fb.push(newObj)
// update a timestamp key on the item to trigger child_changed
fb.update({ updated: yourTimeStamp })
there was also another solution:
get the number of children and extract that value:
and it's working.
var ref = firebaseDb.child('invites')
ref.once('value').then((dataSnapshot) => {
return dataSnapshot.numChildren()
}).then((count) =>{
ref .on('child_added', (child) => {
if(count>0){
count--
return
}
console.log("child really added")
});
});
If your document keys are time based (unix epoch, ISO8601 or the firebase 'push' keys), this approach, similar to the second approach #balthazar proposed, worked well for us:
const maxDataPoints = 100;
const ref = firebase.database().ref("someKey").orderByKey();
// load the initial data, up to whatever max rows we want
const initialData = await ref.limitToLast(maxDataPoints).once("value")
// get the last key of the data we retrieved
const lastDataPoint = initialDataTimebasedKeys.length > 0 ? initialDataTimebasedKeys[initialDataTimebasedKeys.length - 1].toString() : "0"
// start listening for additions past this point...
// this works because we're fetching ordered by key
// and the key is timebased
const subscriptionRef = ref.startAt(lastDataPoint + "0");
const listener = subscriptionRef.on("child_added", async (snapshot) => {
// do something here
});

Categories

Resources