I have a massive data that I can obtain only using third party hooks, for example:
const data1 = useSmthHook(1) where 1 - index;
Lets imagine that we need an object of 100 values:
const data1 = useSmthHook(1)
const data2 = useSmthHook(2)
const data3 = useSmthHook(3)
...
const data100 = useSmthHook(100)
It would be great to get as final result object with all 100 values.
Solution with loop doesn't work because of hooks can't be triggered inside loops.
//does not work!
for (var i = 1; i < 101; i++) {
const data = useSmthHook(i)
....
}
I have only one idea (im sure a bad one) that works: create empty array, rendering 100 components, in Item component logic: while render get hook value => update global state with this data. So every new Item render causes +1 row (update) global state. 100 updates and 100 re-reneders.
Related
I have two objects that I'm trying to loop through using an increment and decrement function. I have the variables being stored in localStorage and the expectation is that no matter which value has been selected last, upon refresh or reload, the counter should still work. However, I've experienced some unexpected occurrences upon using the increment and decrement functions where the variable does not line with the index of the object as shown by the reducer state.
const increment = (props) => {
return (props.state + 1) % props.length;
};
const decrement = (props) => {
return (props.state - 1 + props.length) % props.length;
};
const colors = [
{ id: 0, type: "Red" },
{ id: 1, type: "Blue" },
{ id: 2, type: "Green" },
{ id: 3, type: "Yellow" }
];
For example, at times when I call the state, it will tell me that the color is Yellow and the index is 2, and is generally inconsistent. I've tried storing the counter variable within localStorage and calling from that in hopes that it will synchronize the counter with the intended variable, however, this has not worked.
Here is a demo with CodeSandbox. I'm still relatively new to React and I'm not sure if using a counter is the best method for this problem.
Here is a reproducible example of the output I've received (upon clearing localStorage and refreshing the app) and the expected output for the shapes.
Output:
Square (next) Square (previous) Circle (previous) Square (previous) Octagon (next) Triangle
Expected output:
Square (next) Circle (previous) Square (previous) Octagon (previous) Triangle (next) Octagon
Having forked and refactored your original sandbox here there were a few challenges.
Firstly, the reducer actions were expanded to include increment and decrement. The example provided in React Hooks Reference has an example of incrementing and decrementing, though reducers are not just applicable for counting. The dispatch of your reducer will set the state, so having Action.Set is redundant.
Let's take a look at one of the original buttons' onClick methods. Originally, colorCount was being decremented and then an update to color occurs based on the state at the time of click, not when the state is updated. To visualize this on the original demo, log the state before and after setData.
onClick={() => {
setData({
payload: decrement({
state: state.colorCount,
length: colors.length
}),
name: "colorCount"
});
setData({
payload: colors[state.colorCount].type,
name: "color"
});
}}
Now, the same onClick calls the decrement method.
onClick={() => {
decrement({
name: "colorCount"
});
}}
The decrement method, moved to the context, just calls the dispatch with proper type and payload containing the name of the value to update.
const decrement = (payload) => {
dispatch({
type: ACTIONS.DECREMENT,
payload
});
};
Lastly, the reducer updates the states colorCount paired with its prefix color and shapeCount paired with its prefix shape
const reducer = (state, action) => {
// Verify which value we need to update along with its count
const isColor = action.payload.name.includes("color");
// Select the proper option in context
const options = isColor ? colors : shapes;
switch (action.type) {
case ACTIONS.INCREMENT:
// Increment the count for use in the value and type setting
const incrementedCount =
(state[action.payload.name] + 1) % options.length;
return {
...state,
// Set the new count
[action.payload.name]: incrementedCount,
// Set the new color or shape type
[isColor ? "color" : "shape"]: options[incrementedCount].type
};
case ACTIONS.DECREMENT:
// Decrement the count for use in the value and type setting
const decrementedCount =
(state[action.payload.name] - 1 + options.length) % options.length;
return {
...state,
// Set the new count
[action.payload.name]: decrementedCount,
// Set the new color or shape type
[isColor ? "color" : "shape"]: options[decrementedCount].type
};
default:
return state;
}
};
As far as updating the localStorage on update of a value, the easiest way is another useEffect dependent on the state values. Feel free to update the localStorage how and when you want, but for the purposes of keeping the state on reload the simplest approach was kept.
useEffect(() => {
localStorage.setItem("colorCount", JSON.stringify(state.colorCount));
localStorage.setItem("color", JSON.stringify(state.color));
localStorage.setItem("shapeCount", JSON.stringify(state.shapeCount));
localStorage.setItem("shape", JSON.stringify(state.shape));
}, [state.colorCount, state.color, state.shape, state.shapeCount]);
To the point made about contexts, this counter example does benefit from simplicity. The reducer can be used all within the App. Contexts are best used when passing down props to children becomes cumbersome.
What you've provided here is not sufficient for a minimum reproducible example. We can't offer much help if your problem is only happening "at times" -- please provide specific cases of what steps you take to obtain a specific problem.
Generally speaking, I think it might simplify your code to use two separate state variables. Context seems like overkill for this use case.
const [colorIdx, setColorIdx] = useState(0);
const [shapeIdx, setShapeIdx] = useState(0);
And, as a style note, it is usually a good idea to avoid inline function definitions. The following is much more readable, for example:
const incrementColorIdx = () => {
setColorIdx((colorIdx + 1) % colors.length);
}
...
<button onClick={incrementColorIdx}>Next</button>
const columns = ['task', 'category'];
for (const text in columns) {
const [text, setText] = useState();
}
I wish to create multiple useState things using a loop but to join things together seem to be a problem.
What I want it to do is to create consts: task, setTask | category, setCategory
Check this out:
https://reactjs.org/docs/hooks-rules.html
Explicitly mentions:
Only Call Hooks at the Top Level Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top
level of your React function. By following this rule, you ensure that
Hooks are called in the same order each time a component renders.
That’s what allows React to correctly preserve the state of Hooks
between multiple useState and useEffect calls. (If you’re curious,
we’ll explain this in depth below.)
const [text, setText] = ... is just a deconstructing assignment, you can store the result of useState(..) (an array with two elements) however you want, for example:
const columns = ['task', 'category'];
const state = {};
for (const prop of columns) {
const resultArr = useState();
state[prop] = {
val: resultArr[0],
fn: resultArr[1]
};
}
And you can use the results like this:
<button onClick={() => state.task.fn('new value')}>{state.task.val}</button>
But as #EmileBergeron mentioned, there doesn't seem to be any reason to use useState like this. There is definitely a more conventional way of solving the problem that you're facing.
Under the hood, useState relies on a linked-list-type nested data structure in the fiber. Each time you call useState that structure is "unwrapped" to get the data associated with the next hook call.
This is a good article that goes into depth about how hooks work.
In theory, so long as your loop always executes the same number of times, you could use a hook inside of the loop. HOWEVER, as other people have said here, this is not a standard way of doing things and is not advisable.
const columns = ['task', 'category'];
columns.map( (column, index) => {
let col = column;
let setCol = column+index;
[col, setCol] = useState(false)
})
This way it will give unique state for each iteration of elements in the array columns
const [uploadAnimal, setAnimal] = useState(['cat']);
const loopFun = () => {
const arr = uploadAnimal
for (let i = 0; i < 10; i++) {
arr.push('DOG')
setUploadCount([...arr])
} };
Working with an array of data that we want to be able to sort for display in a component, and it doesn't seem to be sorting or updating the DOM, however I have a working code sample that properly demonstrates the concept, and it should be sorting, but in the angular app, it's simply not getting sorted.
The parent component that houses the original data stores the data on an Input parameter object called Batch, and the array we're sorting is on Batch.Invoices.Results. The event from the child component is fine, and the appropriate data is confirmed to bubble to the parent component.
The function that's supposed to sort the array looks like this:
public OnInvoiceSortChange({orderValue, orderAscending}){
console.log(`Invoice Sorting has been called. Value: ${orderValue} . Ascending? ${orderAscending}`);
console.log(`Before:`);
console.log(this.BatchViewModel.Invoices.Results.map(x => x.VendorName));
const sortingArray = [...this.BatchViewModel.Invoices.Results];
if(orderAscending){
const sorted = sortingArray.sort((a, b) => a[orderValue] > b[orderValue] ? 1 : 0);
this.BatchViewModel.Invoices.Results = sorted;
console.log('Sorted');
console.log(sorted.map(x => x.VendorName));
} else {
const sorted = sortingArray.sort((a, b) => a[orderValue] < b[orderValue] ? 1 : 0);
this.BatchViewModel.Invoices.Results = sorted;
console.log(sorted.map(x => x.VendorName));
}
console.log(`After:`);
console.log(this.BatchViewModel.Invoices.Results.map(x => x.VendorName));
}
All the console logs are for debugger visibility, and the output is this:
Where in my testing file (non-angular) looks like this:(where data is a direct copy of the array from the Angular app.
const ascendingData = [...data];
const descendingData = [...data];
const sortedDescending = descendingData.sort((a, b) => a['VendorName'] < b['VendorName']? 0 : 1)
const sortedAscending = ascendingData.sort((a, b) => a['VendorName'] > b['VendorName']? 0 : 1);
const vendorListAscending = sortedAscending.map(x => x.VendorName);
const vendorListDescending = sortedDescending.map(x => x.VendorName);
console.log(vendorListDescending);
console.log(vendorListAscending);
and the output looks like this:
So I see that the sorting should work, but it's just not happening in Angular.
How can I get the array sorted, and update the DOM as well?
The function you pass to sort is wrong. It is supposed to return a negative value for "less", a positive value for "greater" or zero for "equal". If orderValue is numeric then it's easiest to just return a[orderValue] - b[orderValue], if not then just change your 0 to -1.
(By the way, name orderKey could be a bit clearer maybe?)
I don't think angular has anything to do here, but I cannot tell now why you get different results. Anyway, your sort function is invalid (it states that a equals b, but at the same time b is greater than a), I hope fixing this function helps.
I have a list of people who have scores. In state I have them listed in an array, one of the items in the array is 'scoreHistory' which is an array of objects containing their scores at different points in time. I want to filter this set for different time periods i.e. -5 days, -30 days so instead of just seeing the overall score I can see the scores if everyone started at 0 say 30 days ago.
I have it (kind of) working. See my code below:
filteredScores () {
if(!this.people) {
return
}
// Here I was trying to ensure there was a copy of the array created in order to not change the original array. I thought that might have been the problem.
let allPeople = this.people.slice(0) // this.people comes from another computed property with a simple getter. Returns an array.
let timeWindow = 30 //days
const windowStart = moment().subtract(timeWindow,'days').toDate()
for (const p of allPeople ) {
let filteredScores = inf.scoreHistory.filter(score => moment(score.date.toDate()).isSameOrAfter(windowStart,'day'))
p.scoreHistory=filteredScores
//calculate new score
p.score = inf.scoreHistory.reduce(function(sum,item) {
return sum + item.voteScore
},0)
}
return allInf
}
I expected it to return to me a new array where each person's score is summed up over the designated time period. It seems to do that OK. The problem is that it is altering the state that this.people reads from which is the overall data set. So once it filters all that data is gone. I don't know how I am altering global state without using vuex??
Any help is greatly appreciated.
Your problem isn't that you're modifying the array, but that you're modifying the objects within the array. You change the scoreHistory and score property of each item in the array. What you want to do instead is create a new array (I recommend using map) where each item is a copy of the existing item plus a new score property.
filteredScores () {
if(!this.people) {
return
}
let timeWindow = 30 //days
const windowStart = moment().subtract(timeWindow,'days').toDate()
return this.people.map(p => {
let filteredScores = p.scoreHistory.filter(score => moment(score.date.toDate()).isSameOrAfter(windowStart,'day'))
//calculate new score
let score = filteredScores.reduce(function(sum, item) {
return sum + item.voteScore
}, 0)
// Create a new object containing all the properties of p and adding score
return {
...p,
score
}
}
})
I want to average the related values when the data in the FireBase is updated.
I am using Firebase functions and can not load data.
I can change the data I want when the event occurs, but I can not calculate the average of the data.
exports.taverage = functions.database.ref('/User/tsetUser/monthQuit/{pushId}')
.onCreate((snapshot, context) => {
const promiseRoomUserList = admin.database().ref('/User/tsetUser/monthQuit/{pushId}').once('value');
var sum=0;
const arrayTime = [];
snapshot.forEach(snapshot => {
arrayTime.push('/User/tsetUser/monthQuit/{pushId}'.val());
})
for(let i=0; i<arrayTime.length; i++){
sum+=arrayTime[i];
}
return admin.database().ref('/User/tsetUser/inform/standardQuit').set(sum);
});
//I Want 'standardQuit' value set average.
I'm not sure why you can't calculate the average, but a simpler version of your code would be:
exports.taverage = functions.database.ref('/User/tsetUser/monthQuit/{pushId}')
.onCreate((snapshot, context) => {
return admin.database().ref('/User/tsetUser/monthQuit/{pushId}').once('value')
.then(function(snapshot) {
let sum=0;
snapshot.forEach(child => {
sum = sum + child.val();
})
let avg = sum / snapshot.numChildren();
return admin.database().ref('/User/tsetUser/inform/standardQuit').set(avg);
});
});
The biggest differences:
This code returns promises from both the top-level, and the nested then(). This is needed so Cloud Functions knows when your code is done, and it can thus stop billing you (and potentially shut down the container).
We simply add the value of each child to the sum, since you weren't using the array in any other way. Note that the child.val() depends on your data structure, which you didn't share. So if it fails there, you'll need to update how you get the exact value (or share you data structure with us).
The code actually calculates the average by dividing the sum by the number of child nodes.
Consider using a moving average
One thing to keep in mind is that you're now reading all nodes every time one node gets added. This operation will get more and more expensive as nodes are added. Consider if you can use a moving average, which wouldn't require all child nodes, but merely the current average and the new child node. The value will be an approximate average where more recent value typically have more weight, and is much cheaper to calculate:
exports.taverage = functions.database.ref('/User/tsetUser/monthQuit/{pushId}')
.onCreate((snapshot, context) => {
return admin.database().ref('/User/tsetUser/inform/standardQuit').transaction(function(avg) {
if (!avg) avg = 0;
return (15.0 * avg + snapshot.val()) / 16.0;
});
});