How to properly deal with code that need to wait for previous constants to be loaded? - reactjs

Here is a change to a long code before a render that wasted me time to correct many lines in the code:
Before:
const my_model:MyModel = JSON.parse(localStorage.getItem('my_model')!)
const my_positions:MyModel = JSON.parse(localStorage.getItem('my_positions')!)
After:
const waitformyIcm:Boolean = useDetailMyModelQuery(paramid.id).isSuccess;
const myIcm:undefined | MyModel| any = useDetailMyModelQuery(paramid.id).data
const waitforpositions:Boolean = usePatternPositionsPerModelQuery(paramid.icm).isSuccess;
const positions:undefined | PatternPositions[] = usePatternPositionsPerModelQuery(paramid.icm).data
The consequence is that for almost all the next constants I needed to let the program wait until those first lines completed loading. My question is how to cope with such situations because I feel my approach was not as it should:
For example this previous line would already cause crashes:
const harvestAfter:number[] = [myIcm.length_period1, myIcm.length_period2, myIcm.length_period3]
This was solved by:
const harvestAfter:number[] = waitformyIcm && [myIcm.length_period1, myIcm.length_period2, myIcm.length_period3]
Another challenge was to set a default state with data that had to be loaded first. I solved that with:
const [myFilters, setMyFilters] = useState(myfilters);
useEffect(() => setMyFilters(myfilters), [myfilters && waitforpositions]);
How to properly deal with code that need to wait for previous constants to be loaded in REACT?
Using myconst && ......?
Using useEffect?
Using useRef?
Using async await (how to do that for declaring constants)
Other?

Please don't call the same hook multiple times, that is just a waste of user memory and CPU time. (These hooks are doing work internally and you invoke them twice for no good reason.)
Just don't annotate the types here and assign them to a variable. They are already 100% typesafe from the hook.
Then, use skip:
const icmResult = useDetailMyModelQuery(paramid.id);
const patternsResult = usePatternPositionsPerModelQuery(paramid.icm, { skip: !icmResult.isSuccess });
As for the variable assignment, you could also use destructuring although I don't really see the point:
const { isSuccess: waitformyIcm, data: myIcm } = useDetailMyModelQuery(paramid.id);
As for a default state, there are many approaches. If it is an object you define inline, it would be a new reference on every render, so you best useMemo to get around that:
const filtersWithDefaults = useMemo(() => filters ?? defaultFilters, [filters])

Related

Problem at reading an array from redux store, the array is concatenated

I came to ask for help in a Redux project with arrays.
I have two variables, a string and an array and I'm only able to use the string variable.
For my problem I'll simplify things in an example, but in my real array there are a lot more elements.
let x = "test1";
let y = ["test2","test3"];
And what happens? During the path of the variables to the end, if I give a console log, it's OK. Even the array is "still" an array.
But when pulling it from the store, it comes concatenated
// teste2teste3
so like... it's no longer an array.
If I give a console log, it's still shown as array. Now if I give an index to the variable (var[0] or var[1]), it gives undefined because these index's don't exist.
So I probably have to do something I didn't care about. But just reporting it again, the string variable is fine, so at least the Redux structure is right, I imagine it must be something in the reducer.
Follow the data:
1st Dispatch as array itself is right ["teste2","teste3"] because I can give console log in the reducer and at least it gets there right.
DISPATCH
const dispatch = useDispatch();
setdispatch(["test2","test3"]);
const setdispatch = (args) => {dispatch(actiontest(args))};
2nd Action I think it's right here too
ACTIONS
export const actiontest= (args) => {
return {
type: "TEST",
payload:{test:args}
};
};
3rd Reducer I think the error must be here
REDUCER
const testReducer = (state = "", action) => {
switch(action.type) {
case "TEST":
return {
...state,
testpayload: action.payload.test,
};
default:
return state;
}
4th Load from Store
USELECTOR
const testReducer = useSelector((state) => state.test);
console.log(testReducer.testpayload);
//Array(2)
0: "Test2"
1: "Test3"
So far it's ok.
Now making a jsx {testeReducer.testepayload}
//Test2Test3
And testReducer.testepayload[0] gives undefined, because it looks like it is no longer an array.
And the other variable test1 is fine here.
I tried turning the payload into an array, even though it was already an array, just to test it out.
testpayload: [action.payload.test],
Array(1)
0: Array(2)
0: "test2"
1: "test3"
Anyway, the problem doesn't change anything.
So until I solved the problem, I had to stick with multiple variables, one for each value, what a shame. Anyone who can help me out there, thanks.

Problem with this react flow, maximum update depth exceeded

I have the following function
compareProducts = (empresa) => {
console.log(empresa.listaProductos)
let headerSetIn = false;
for (let i in empresa.listaProductos) {
//case1 : lookup for some data in an array, if found, setState and exit the whole function
if (pFichaInternacional && pFichaInternacional.length > 0) {
console.log("caso1")
let product: any = pFichaInternacional;
let nombreEmpresaApi = empresa.listaProductos[i].nombre
let productoFiltrado = product.filter(i => i.referencia == nombreEmpresaApi)
productoFiltrado = productoFiltrado[0]
if (productoFiltrado) {
headerSetIn = true
this.setState({
headerCardText: productoFiltrado.descripcion.toString(),
headerButtonText: productoFiltrado.label.toString(),
})
break;
}
}
}
//case 2: case1 didnt found the data, so we setup some predefined data.
if (!headerSetIn && pFichaInternacional.length > 0) {
let product: any = pFichaInternacional;
this.setState({
headerCardText: product[0].descripcion.toString(),
headerButtonText: product[0].label.toString()
})
}
}
Im receiving a
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
I have also tried using a setstate , instead of a local variable to set the headerSetIn parameter. But if I do it, I think js doesnt have time to evaluate the change, and both are executed, instead of only 1
Ive tried to use () , => after the first state, but it doesnt make sense in my flow
As far as I can understand is that you are calling setState in a for loop, which is not a good thing. Every time a certain condition is met setState is called, so you are constantly setting the state and rerendering smashing your performance and causing this.
I would suggest using a variable and get the needed data in the for loop then after you have excited the for loop simply use setState to set the state after all the data has been looped trough.

How can I make React append a number to a variable? [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 2 years ago.
Improve this question
I am trying to automate fetching data from a source and assigning it to a variable that goes up by 1 each loop. Each call to the data source returns a different URL (of a picture of a cat, in this case).
var i;
var urlCount = 7;
for (i=0; i < urlCount; i++) {
this.setState({...this.state, isFetching: true});
var response = await axios.get(USER_SERVICE_URL);
this.image = response.data[0];
url[i] = this.image.url;
console.log([i] + url[i]);
}
What I'm wanting it to do is create a list similar to the following:
url0 = (url here),
url1 = (another url here),
url2 = (yet another url here),
etc...
until the loop meets its condition. It works when I set the code to a manual value, like this:
url0 = this.image.url;
console.log([i] + url0);
But it doesn't work when I try to replace 0 with [i], as in the first example. It seems to be treating [i] as a property of url, instead of appending it to url. How can I make React append a number to a variable so that I can achieve the desired result above?
would not recommend to do this with a for loop, also missing information about the component itself. Also using an object here as an example, but would recommend an array.
// assuming a functional component
// add a state with a object (would rather go with an array)
const [urls, setUrls] = useState({});
// this should be within a function, better a useEffect with
// an async function defined inside and then called within the effect
var i;
var urlCount = 7;
// set fetching state
this.setState({ isFetching: true });
// loop (btw not recommended with async, better go with a map, return promises and then await Promise.all(yourArray))
for (i = 0; i < urlCount; i++) {
var response = await axios.get(USER_SERVICE_URL);
this.setState((oldState) => ({ ...oldState, [`url${i}`]: response.data[0] }));
}
// now you could access
const { url0, url1, url2... } = urls;
This is a suggestion or a tip rather than an answer
The state variable isFetching is used like a flag. You are using the setState method before fetching the data.
Calling this method will re-render the component.
Also it is called in a for loop. So the component will re-render multiple times.
My suggestion is avoid setting state unnecessarily. It will re-render the components and cause unexpected behavior.
Try this code
var i;
var urlCount = 7;
//If you set state inside a for loop and urlCount is a large value
//your component will render many times unnecessarily
this.setState({...this.state, isFetching: true});
for (i=0; i < urlCount; i++) {
var response = await axios.get(USER_SERVICE_URL);
this.image = response.data[0];
url[i] = this.image.url;
console.log([i] + url[i]);
}

Cloning an array in Javascript/Typescript

I have array of two objects:
genericItems: Item[] = [];
backupData: Item[] = [];
I am populating my HTML table with genericItemsdata. The table is modifiable. There is a reset button to undo all changes done with backUpData. This array is populated by a service:
getGenericItems(selected: Item) {
this.itemService.getGenericItems(selected).subscribe(
result => {
this.genericItems = result;
});
this.backupData = this.genericItems.slice();
}
My idea was that, the user changes will get reflected in first array and second array can be used as backup for reset operation. The issue I am facing here is when the user modifies the table (genericItems[]) the second array backupData also gets modified.
How is this happening and how to prevent this?
Clone an object:
const myClonedObject = Object.assign({}, myObject);
Clone an Array:
Option 1 if you have an array of primitive types:
const myClonedArray = Object.assign([], myArray);
Option 2 - if you have an array of objects:
const myArray= [{ a: 'a', b: 'b' }, { a: 'c', b: 'd' }];
const myClonedArray = [];
myArray.forEach(val => myClonedArray.push(Object.assign({}, val)));
Cloning Arrays and Objects in javascript have a different syntax. Sooner or later everyone learns the difference the hard way and end up here.
In Typescript and ES6 you can use the spread operator for array and object:
const myClonedArray = [...myArray]; // This is ok for [1,2,'test','bla']
// But wont work for [{a:1}, {b:2}].
// A bug will occur when you
// modify the clone and you expect the
// original not to be modified.
// The solution is to do a deep copy
// when you are cloning an array of objects.
To do a deep copy of an object you need an external library:
import {cloneDeep} from 'lodash';
const myClonedArray = cloneDeep(myArray); // This works for [{a:1}, {b:2}]
The spread operator works on object as well but it will only do a shallow copy (first layer of children)
const myShallowClonedObject = {...myObject}; // Will do a shallow copy
// and cause you an un expected bug.
To do a deep copy of an object you need an external library:
import {cloneDeep} from 'lodash';
const deeplyClonedObject = cloneDeep(myObject); // This works for [{a:{b:2}}]
Using map or other similar solution do not help to clone deeply an array of object.
An easier way to do this without adding a new library is using JSON.stringfy and then JSON.parse.
In your case this should work :
this.backupData = JSON.parse(JSON.stringify(genericItems));
When using the last version of JS/TS, installing a large library like lodash for just one/two function is a bad move. You will heart your app performance and in the long run you will have to maintain the library upgrades! check https://bundlephobia.com/result?p=lodash#4.17.15
For small objet lodash cloneDeep can be faster but for larger/deeper object json clone become faster. So in this cases you should not hesitate to use it. check https://www.measurethat.net/Benchmarks/Show/6039/0/lodash-clonedeep-vs-json-clone-larger-object and for infos https://v8.dev/blog/cost-of-javascript-2019#json
The inconvenient is that your source object must be convertible to JSON.
try the following code:
this.cloneArray= [...this.OriginalArray]
The following line in your code creates a new array, copies all object references from genericItems into that new array, and assigns it to backupData:
this.backupData = this.genericItems.slice();
So while backupData and genericItems are different arrays, they contain the same exact object references.
You could bring in a library to do deep copying for you (as #LatinWarrior mentioned).
But if Item is not too complex, maybe you can add a clone method to it to deep clone the object yourself:
class Item {
somePrimitiveType: string;
someRefType: any = { someProperty: 0 };
clone(): Item {
let clone = new Item();
// Assignment will copy primitive types
clone.somePrimitiveType = this.somePrimitiveType;
// Explicitly deep copy the reference types
clone.someRefType = {
someProperty: this.someRefType.someProperty
};
return clone;
}
}
Then call clone() on each item:
this.backupData = this.genericItems.map(item => item.clone());
Array copy explained - Deep & Shallow
Below code might help you to copy the first level objects
let original = [{ a: 1 }, {b:1}]
const copy = [ ...original ].map(item=>({...item}))
so for below case, values remains intact
copy[0].a = 23
console.log(original[0].a) //logs 1 -- value didn't change voila :)
Fails for this case
let original = [{ a: {b:2} }, {b:1}]
const copy = [ ...original ].map(item=>({...item}))
copy[0].a.b = 23;
console.log(original[0].a) //logs 23 -- lost the original one :(
Try lodash separate ES module - cloneDeep:
I would say go for lodash cloneDeep API ( This can be
installed as a separate module, reduced code footprint for treeshaking ) which helps you to copy the objects
inside objects completely dereferencing from original one's.
As another option you can rely on JSON.stringify & JSON.parse
methods to dereference quickly and performant too.
Refer documentation: https://github.com/lodash/lodash
Individual Package : https://www.npmjs.com/package/lodash.clonedeep
you can use map function
toArray= fromArray.map(x => x);
Clone an object / array (without reference) in a very powerful way
You can get deep-copy of your object / array using #angular-devkit.
import { deepCopy } from '#angular-devkit/core/src/utils/object';
export class AppComponent {
object = { .. some object data .. }
array = [ .. some list data .. ]
constructor() {
const newObject = deepCopy(this.object);
const newArray = deepCopy(this.array);
}
}
I have the same issue with primeNg DataTable. After trying and crying, I've fixed the issue by using this code.
private deepArrayCopy(arr: SelectItem[]): SelectItem[] {
const result: SelectItem[] = [];
if (!arr) {
return result;
}
const arrayLength = arr.length;
for (let i = 0; i <= arrayLength; i++) {
const item = arr[i];
if (item) {
result.push({ label: item.label, value: item.value });
}
}
return result;
}
For initializing backup value
backupData = this.deepArrayCopy(genericItems);
For resetting changes
genericItems = this.deepArrayCopy(backupData);
The magic bullet is to recreate items by using {} instead of calling constructor.
I've tried new SelectItem(item.label, item.value) which doesn't work.
the easiest way to clone an array is
backUpData = genericItems.concat();
This will create a new memory for the array indexes
If your items in the array are not primitive you can use spread operator to do that.
this.plansCopy = this.plans.map(obj => ({...obj}));
Complete answer : https://stackoverflow.com/a/47776875/5775048
Try this:
[https://lodash.com/docs/4.17.4#clone][1]
var objects = [{ 'a': 1 }, { 'b': 2 }];
var shallow = _.clone(objects);
console.log(shallow[0] === objects[0]);
// => true
It looks like you may have made a mistake as to where you are doing the copy of an Array. Have a look at my explanation below and a slight modification to the code which should work in helping you reset the data to its previous state.
In your example i can see the following taking place:
you are doing a request to get generic items
after you get the data you set the results to the this.genericItems
directly after that you set the backupData as the result
Am i right in thinking you don't want the 3rd point to happen in that order?
Would this be better:
you do the data request
make a backup copy of what is current in this.genericItems
then set genericItems as the result of your request
Try this:
getGenericItems(selected: Item) {
this.itemService.getGenericItems(selected).subscribe(
result => {
// make a backup before you change the genericItems
this.backupData = this.genericItems.slice();
// now update genericItems with the results from your request
this.genericItems = result;
});
}
Looks like what you want is Deep Copy of the object. Why not use Object.assign()? No libaries needed, and its a one-liner :)
getGenericItems(selected: Item) {
this.itemService.getGenericItems(selected).subscribe(
result => {
this.genericItems = result;
this.backupDate = Object.assign({}, result);
//this.backupdate WILL NOT share the same memory locations as this.genericItems
//modifying this.genericItems WILL NOT modify this.backupdate
});
}
More on Object.assign(): https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
array level cloning solutions didn't work for me, added this clone utility in typescript which does a deeper clone that worked in my case
export function cloneArray<T>(arr: T[]): T[] {
return arr.map((x) => Object.assign({}, x))
}
by "didn't work" i meant, that if i passed an array to a function [...arr] or Object.assign([], arr) or arr.splice() it was still mutating the original array
Try this
const returnedTarget = Object.assign(target, source);
and pass empty array to target
in case complex objects this way works for me
$.extend(true, [], originalArray) in case of array
$.extend(true, {}, originalObject) in case of object

Chrome extension won't enter for..of loop despite array length equal to 1

Background
Developing a Chrome extension (latest Chrome running on Mac OS Sierra) and I can't work out how to loop over an array which is also dynamically built at runtime.
Forgive me if I am missing something really obvious, but I cannot for the life of me work out why this for..of loop is not being entered.
I've also tried a for..in and the good old classic for loop structure i.e. for (let i = 0; i < array.length; i++) - no matter what style of loop this block is never entered, despite my array at runtime reportedly having a single item in it.
Problem Code and Statement
This code gets all files inside a directory and slices off the last 3 chars (to remove .js):
const getDirectoryContents = (path) => {
let fileNames = []
chrome.runtime.getPackageDirectoryEntry( (directoryEntry) => {
directoryEntry.getDirectory(path, {}, (subDirectoryEntry) => {
const subDirectoryReader = subDirectoryEntry.createReader()
subDirectoryReader.readEntries( (entries) => {
for (const entry of entries) {
fileNames.push(entry.name.slice(0, -3))
}
})
})
})
return fileNames
}
From inside the chrome.runtime.onStartup() callback function we want to add some context menus, which we do like so:
const addContextMenus = () => {
console.log(getDirectoryContents('commands'))
for (const command of getDirectoryContents('commands')) {
const properties = {
id: command,
title: command,
contexts: ['editable']
}
chrome.contextMenus.create(properties)
console.log(`Created context menu ${properties.title}`)
}
console.log('End addContextMenus')
}
Now, during runtime, the above code will output this inside the background page console:
However as we can see (due to the lack of the console logging "Created context menu ..." - the loop is never entered, despite the array having a length of 1.
I've found nothing online inside the Chrome developer docs that indicated that getDirectoryContents is asynchronous -- which would be one possible explanation -- but just to be sure I even tried adding a callback param to the getDirectoryContents function to ensure we are looping after the array has been populated.
EDIT: after closer inspection of the original function, it's clear that the array is in fact being returned before is has a chance to be populated by the directory reader. Answer below.
Same result!
Any help would be much appreciated. Thanks for your time.
How embarrassing! Passing in a callback function and executing it at the right time solved it. Comments were all correct - typical async issue - thanks for the support.
The problem was on line 15 of the original function: I was returning the array before it had a chance to be populated.
Working function:
const getDirectoryContents = (path, callback) => {
chrome.runtime.getPackageDirectoryEntry( (directoryEntry) => {
directoryEntry.getDirectory(path, {}, (subDirectoryEntry) => {
const subDirectoryReader = subDirectoryEntry.createReader()
let fileNames = []
subDirectoryReader.readEntries( (entries) => {
for (const entry of entries) {
fileNames.push(entry.name.slice(0, -3))
}
callback(fileNames)
})
})
})
}

Resources