mutate in useSWR hook not updating the DOM - javascript

It's a next.js app and I populate the data using a useSWR hook.
const { data, error } = useSWR('/api/digest', fetcher, {
revalidateOnFocus: false,
})
The issue is that the DOM doesn't get updated as expected after the mutate() line below. But if I hard code the data as updatedData and pass it as the arg for the mutate it works normally. The fact is that data and the updatedData are the same. See comments below.
Edit: If I click on any Navbar <Link/> it gets updated.
Any clues of what is happening?
const handleAddRowClick = async () => {
const newRow = { category: selectedCategory, entity: selectedEntity }
data.subscription[selectedCategory].push(selectedEntity);
console.log(data); // This data is equal to updatedData
const updatedData = {
"subscription": {
"materialFact": [
"Boeing"
],
"headlines": [
"thejournal",
"onemagazine" // ***This is the value added.
]
}
}
// mutate('/api/digest', data, false) // This does not works.
mutate('/api/digest', updatedData , false) // It works.
}

I am assuming that the data property you use in handleAddRowClick is the same that you get from useSWR. Now, if you update some deeply nested object in data, it doesn't change the data reference. It still points to the old object. So, if you pass it again to mutate, for mutate, the object is still the same and hence, it won't trigger a re-render.
I would recommend that you do something like the following to derive updatedData from data and then pass it to the mutate function.
const handleAddRowClick = async () => {
const updatedData = {
...data,
subscription: {
...data.subscription,
[selectedCategory]: [
...data.subscription[selectedCategory],
selectedEntity,
]
}
}
mutate('/api/digest', updatedData , false);
}
On a side note, you can also use something like immer to simplify copying the state.

Related

Local storage with hooks in react

So I have an array with the following structure:
`
export const transacciones = [
{
id:100,
cantidad: 0,
concepto : 'Ejemplo',
descripcion: 'Ejemplo',
},
]
`
This array will dynamically increase or decrease as I push or filter items in it (Exactly like data in a task list)
The problem is that I am trying to add some data persistence using local storage. I guess data is getting stored but not shown when I refresh my browser (chrome).
However, when I refresh data disappears from where it was in the upper image so I`m not even sure if I am correctly storing it.
I've tried two things using useEffect hooks.
First aproach:
`
const [transacciones,setTransacciones] = useState([]);
useEffect(()=>{
localStorage.setItem('transacciones',JSON.stringify(transacciones))
},[transacciones])
useEffect(() =>{
const transacciones = JSON.parse(localStorage.getItem('transacciones'))
if (transacciones){
setTransacciones(transacciones)
}
},[])
`
I read somewhere that as the initial value of use state is [] I should chage things in there, so...
Second aproach:
`
const [transacciones,setTransacciones] = useState([],()=>{
const localData = localStorage.getItem('transacciones');
return localData ? JSON.parse(localData) : [];
});
useEffect(()=>{
localStorage.setItem('transacciones',JSON.stringify(transacciones))
},[transacciones])
`
However, when I refresh I get the same result: No persistence.
What am I missing here? Any help would be appreciated
In both scenarios your transacciones array is empty when you perform the localStorage.setItem. if you're trying to keep your local state sync with localStorage this might help:
export function useTransacciones(initialValue){
const localData = localStorage.getItem('transacciones');
const [transacciones,_setTransacciones] = useState(localData?JSON.parse(localData) : initialValue); // you can choose your own strategy to handle `initialValue` and cachedValue
const setTransacciones = (data) => {
_setTransacciones(data)
localStorage.setItem(JSON.stringify(data))
}
hydrate(){
const data = localStorage.getItem("transacciones")
setTransacciones(JSON.prase(data))
}
return [ transacciones, setTransacciones, hydrate ]
}
which you can use it anywhere with caching compelexity hidden inside:
const [transacciones, setTransacciones] = useTransacciones([])

Why react state (useState) is updated but not updated when log it?

Hi am trying to create a simple multi file component, But the files state is not behaving as expected.
The FileUpload component works fine and when the user chooses a file and it starts uploading the onStart prop method is called. And then when it finishes successfully the 'onFinish' prop method is called. This code seems fine to the point of consoling the object in the onfinish method. it consoles an old value of the object before it was modified by the onStart Method. I expected the file object value in the console to include the buffer key since it was added when the onStart method was called but it's not there.
Example initial state files should be [] when the use effect is called on the state files should be updated to [{_id:"example_unique_id"}] then a button for upload will appear and when user chooses a file and onStart modifies the object and the state should be updated to [{_id:"example_unique_id", buffer:{}] and finally when it finishes files should be [{_id:"example_unique_id", buffer:{}] but instead here it returns [{_id:"example_unique_id"}].
What could I be missing out on?
Also, I have React Dev tools installed and it seems the state is updated well in the dev tools.
import React, { useState } from 'react'
import { useEffect } from 'react';
import unique_id from 'uniqid'
import FileUpload from "./../../components/FileUpload";
const InlineFileUpload = ({ onFilesChange }) => {
const [files, setFiles] = useState([]);
function onFinish(file, id) {
const old_object = files.filter((file) => file._id == id)[0];
console.log("old object on after upload", old_object);
}
const addFile = (file, id) => {
const old_object = files.filter((file) => file._id == id)[0];
const index = files.indexOf(old_object);
const new_files = [...files];
new_files.splice(index, 1, { ...old_object, buffer: file });
setFiles(new_files);
};
useEffect(() => {
const new_attachments = files.filter(({ buffer }) => buffer == undefined);
if (new_attachments.length == 0) {
setFiles([...files, { _id: unique_id() }]);
}
const links = files.filter((file) => file.file !== undefined);
if (links.length !== 0) {
onFilesChange(links);
}
}, [files]);
return (
<>
{files.map((file) => {
const { _id } = file;
return ( <FileUpload
key={_id}
id={_id}
onStart={(e) => addFile(e, _id)}
onFinish={(e) => onFinish(e, _id)}
/>
);
})}
</>
);
};
export default InlineFileUpload
I think the problem is caused by the fact that your this code is not updating the state:
const addFile = (file, id) => {
const old_object = files.filter((file) => file._id == id)[0];
const index = files.indexOf(old_object);
const new_files = [...files];
new_files.splice(index, 1, { ...old_object, buffer: file });
setFiles(new_files);
}
files looks like an array of objects.
Spread operator will not do a deep copy of this array. There are a lot of examples on the internet, here is one.
let newArr = [{a : 1, b : 2},
{x : 1, y : 2},
{p: 1, q: 2}];
let arr = [...newArr];
arr[0]['a'] = 22;
console.log(arr);
console.log(newArr);
So your new_files is the same array. Splice must be making some modifications but that is in place. So when you are doing this setFiles(new_files);, you are basically setting the same reference of object as your newState. React will not detect a change, and nothing gets updated.
You have the option to implement a deep copy method for your specific code or use lodash cloneDeep.
Looking at your code, this might work for you : const new_files = JSON.parse(JSON.stringify(files)). It is a little slow, and you might lose out on properties which have values such as functions or symbols. Read
The reason you are getting the old log is because of closures.
When you do setFiles(new_files) inside addFiles function. React updates the state asynchronously, but the new state is available on next render.
The onFinish function that will be called is still from the first render, referencing files of the that render. The new render has the reference to the updated files, so next time when you log again, you will be getting the correct value.
If it's just about logging, wrap it in a useEffect hook,
useEffect(() => {
console.log(files)
}, [files);
If it's about using it in the onFinish handler, there are answers which explore these option.

ReactJS: Updating array inside object state doesn't trigger re-render

I have a react hooks function that has a state object apiDATA. In this state I store an object of structure:
{
name : "MainData", description: "MainData description", id: 6, items: [
{key: "key-1", name : "Frontend-Test", description: "Only used for front end testing", values: ["awd","asd","xad","asdf", "awdr"]},
{key: "key-2", name : "name-2", description: "qleqle", values: ["bbb","aaa","sss","ccc"]},
...
]
}
My front end displays the main data form the object as the headers and then I map each item in items. For each of these items I need to display the valuesand make them editable. I attached a picture below.
Now as you can see I have a plus button that I use to add new values. I'm using a modal for that and when I call the function to update state it does it fine and re-renders properly. Now for each of the words in the valuesI have that chip with the delete button on their side. And the delete function for that button is as follows:
const deleteItemFromConfig = (word, item) => {
const index = apiDATA.items.findIndex((x) => x.key === item.key);
let newValues = item.value.filter((keyWord) => keyWord !== word);
item.value = [...newValues];
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
apiDATA.items = [...apiDataItems];
setApiDATA(apiDATA);
}
});
};
Unfortunately this function does not re-render when I update state. And it only re-renders when I update some other state. I know the code is a bit crappy but I tried a few things to make it re-render and I can't get around it. I know it has something to do with React not seeing this as a proper update so it doesn't re-render but I have no idea why.
It is not updating because you are changing the array items inside apiDATA, and React only re-render if the pointer to apiDATA changes. React does not compare all items inside the apiDATA.
You have to create a new apiDATA to make React updates.
Try this:
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
setApiDATA(prevState => {
return {
...prevState,
items: apiDataItems
}
});
}
Using splice isn't a good idea, since it mutates the arrays in place and even if you create a copy via let apiDataItems = [...apiDATA.items];, it's still a shallow copy that has original reference to the nested values.
One of the options is to update your data with map:
const deleteItemFromConfig = (word, item) => {
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
const items = apiDATA.items.map(it => {
if (it.key === item.key) {
return {
...item,
values: item.value.filter((keyWord) => keyWord !== word)
}
}
return item;
})
setApiDATA(apiData => ({...apiData, items});
}
});
}

How to create multiple copies of a deeply nested immutable Object?

I'm trying to get data from firebase/firestore using javascript so i made a function where i get my products collection and passing this data to reactjs state.products by setState() method
My goal is to pass these products to my react state but also keeping the original data and not changing it by manipulating it. I understand that in JavaScript whenever we are assigning the objects to a variable we are actually passing them as a reference and not copying them, so that' why i used the 3 dots (spread syntax) to copy firestore data into tempProducts[] same way to copy it in virgins[]
getData = () => {
let tempProducts = [];
let virgins = [];
db.collection("products")
.get()
.then(querySnapshot => {
querySnapshot.forEach(item => {
const singleItem = { ...item.data() };
virgins = [...virgins, singleItem];
tempProducts = [...tempProducts, singleItem];
});
tempProducts[0].inCart = true;
this.setState(
() => {
return { products: tempProducts };
},
() => {
console.log(
"this.state.products[0].inCart: " + this.state.products[0].inCart
);
console.log("tempProducts[0].inCart: " + tempProducts[0].inCart);
console.log("virgins[0].inCart: " + virgins[0].inCart);
}
);
});
};
then calling this method in componentDidMount
componentDidMount() {
this.getData();
}
So I changed the first product inCart value in tempProducts to true when I console log tempProducts value and state.products value it gives me true all fine but I'm expecting to get false when i console log virgins value but i did not. I should also mention that all inCart values are false in firestore data.
I solved the issue by passing the original firestore data to virgins[] instead of the singleItem as it is an object referenced by tempProducts[] like so virgins = [...virgins, item.data()]; also it works if i copied the singleItem object instead of referencing it like so virgins = [...virgins, { ...singleItem }]; keep in mind that i have no idea if this solutions are in fact efficient (not "memory waste") or not.

How do I loop all Firebase children in React Native?

ANSWER AT BOTTOM OF POST
(ALSO SEE #soutot ANSWER)
I have successfully gained text output from my Firebase code and so I know it works. Now I need to loop all the children in Firebase. Coming from swift, the way to do it is to store each item on a tempObject then append that to an array. Then you can simply use that array any way you like. This doesn't seem to work with JavaScript, or I'm doing something wrong. The fully functional FB code I now have is:
componentDidMount(){
let child = ""
var ref = firebase.database().ref("testCategory");
ref.once("value")
.then(function(snapshot) {
child = snapshot.child("test1/testHeader").val()
this.setState( {
child
})
}.bind(this));
}
I can then successfully print this in console or in <Text>. Now, the problem I'm having is looping all children, adding them to an array, then using that array to display the data. In my head, this is how it should work:
componentDidMount(){
let child = ""
var ref = firebase.database().ref("testCategory");
ref.once("value")
.then(function(snapshot) {
snapshot.forEach(function(childSnapshot){
let tempObject = MyFirebase
tempObject.testHeader =
childSnapshot.val()
myArray.append(tempObject) //or .push???
})
this.setState( { //PASSING VARIABLE TO STATE
child
})
}.bind(this)); //BINDING TO SET STATE
}
I know that this is obviously wrong. Creating an object like that doesn't even work... MyFirebase -class looks like this:
render() {
let testHeader = ""
let testText = ""
)
}
My Firebase database looks like this:
(I ignored subText for now)
All suggestions are much appreciated :)
WORING CODE
//FIREBASE CODE
componentDidMount(){
const ref = firebase.database().ref("testCategory");
ref.once("value")
.then(function(snapshot) {
const categories = []
//LOOPING EACH CHILD AND PUSHING TO ARRAY
snapshot.forEach(item => {
const temp = item.val();
categories.push(temp);
return false;
});
this.setState( { //PASSING VARIABLE TO STATE
child,
categories
})
}.bind(this)); //BINDING TO SET STATE
}
According to the code you provided, it looks like it works until this point
var ref = firebase.database().ref("testCategory");
ref.once("value")
.then(function(snapshot) {
Am I correct?
From this point, if you add a console.log(snapshot.val()) it might print an array of objects, something like this:
[ { test1: { testHeader: 'FirstHeader', testText: 'FirstText' } }, { test2: { testHeader: 'SecondSub', testText: 'SecondSub } }]
Right?
If so, you can for example store this snapshot into your state and then consume this state in your render method. Something like this:
const ref = firebase.database().ref('testCategory');
ref.once('value').then((snapshot) => {
this.setState({ categories: snapshot.val() });
});
Then in your render method:
const { categories } = this.state;
categories.map(category => <Text>{category.testHeader}</Text>)
The result in your screen should be:
FirstHeader
SecondSub
Let me know if this helped
Some links that might explain more about es6 codes I used in this example:
array map categories.map(...): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
object destructuring const { categories } = this.state: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
const instead of var const ref = ...: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
setState: https://reactjs.org/docs/state-and-lifecycle.html
arrow function (snapshot) => ...: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
Some about firebase snapshots: https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot
Hope it helps

Categories

Resources