React Functional Component - Store some prop calculation in const vs state - javascript

I have a component which renders only once and then is being destroyed,
I've implemented the "Comp3" way.
Using "useEffect" to calculate the str with the num value I received as a prop, then stored the result using "useEffect" setter in order to use the result in the html.
//Let say this function is inside the functional component
function numToStr(num:number):string{
switch(num){
case 1:
return "One";
case 2:
return "Two";
...
}
}
function Comp1({num:number}){
const numAsStr = numToStr(num);
return <span>{numAsStr}</span>
}
function Comp2({num:number}){
const [numAsStr] = useState(numToStr(num));
return <span>{numAsStr}</span>
}
function Comp3({num:number}){
const [numAsStr,setNumAsStr] = useState();
useEffect(()=>{
setNumAsStr(numToStr(num));
},[])
return <span>{numAsStr}</span>
}
function Comp4({num:number}){
const numAsStr = useMemo(()=>numToStr(num),[]);
return <span>{numAsStr}</span>
}
My question is:
What is the best solution in terms of best-practice/ "react-way"?
How each implementation effect the performance?
Does the fact that I know the component only renders once should impact way I choose to implement?
Or should I treat this component as if i don't know it should be render once and in this case still support the option to "watch" over the prop being changed? (add it to the useEffect / useMemo arrays)
Thanks!

If the calculations being done by strToNum are cheap (as they are in your simplified example), then the approach in Comp1 is probably the best and simplest. They'll run each time the component re-renders, so they're always up-to-date.
If the calculations are expensive, the recommended way to deal with this is the useMemo hook, like in your Comp4 example. However, you'd want to make sure to include num input variable in your dependents array so that numAsStr gets re-computed whenever num changes. E.g:
function Comp4({num:number}){
const numAsStr = useMemo(()=>numToStr(num),[num]);
return <span>{numAsStr}</span>
}
Using useState as you have in Comp2 would only run numToStr in the initial render, so you'd get stale values if num ever changed.
Using useEffect as you have in Comp3 introduces an unnecessary double-render - e.g. it renders first without a value for numAsStr and then renders again.
I know you said that you are currently sure that it never re-renders again - so some of the downsides/gotchas mentioned above might not apply in this case (and then I'd just go with the Comp1 approach, because it's simpler) but in my experience, it's best not to make that assumption - will you (or a team-mate) remember that in a month when you try to refactor something?

Related

React useState not updating when called from Child Component

I'm trying to create a resuable component that will be able to take a function passed down into it, allowing it to update the state of the parent component. I've got a demo on CodeSandbox that shows the issue.
TrackListing is passed the removeTrack function from the parent, and when the delete button is clicked inside the TrackListing, the function is called with the rows index to be removed from the tracks state. Unfortunately this doesn't seem to update the tracks state.
Your not changing the state, tracks it's the same tracks.
In JS if you do -> var a = [1,23]; var b = a; console.log(a === b); it would return true. Assigning an array to another var, does not make a new array.. Doing var a = [1,23]; var b = [...a]; console.log(a === b); will do a shallow copy of a, and this as expected will return false. IOW: In react causing a state change.
Also splice(index,index), I'm pretty sure you meant splice(index, 1).
const removeTrack = (index: number) => {
const t = tracks;
t.splice(index, index);
console.log(t);
setTracks(t); //t is still the same instance of t.
};
So you will need to make a copy of t.
const removeTrack = (index: number) => {
const t = tracks;
t.splice(index, 1);
setTracks([...t]); //lets make a copy of `t` state has changed.
};
As pointed out in the comments, using the callback version of setState is a good idea, it will prevent issues of scope, here scope isn't an issue, but it's easy to get caught out by it.
Also another issue I've found with your current implementation you might want to look into, using array index as a key is not a good idea, it's not unique and you could get some funky rendering quirks, you really need unique ids for your tracks to use as the key
eg. This bit ->
tracks.map((track, index) => (
<tr key={index}> //not a good idea.
tracks.map((track, index) => (
<tr key={track.id}> //much better
Also when it comes to deleting, use the track id, using the callback/filter version in comments from Ali, you could also then just change removeTrack(track:Track) {}, and pass the track. IOW: remove index array positions from all code.
I've done a fork of your CodeSandBox with these changes to make it easier. CodeSandBox

How does React determine if a reference type state is new?

I know that when a state is of primitive type, React performs a shallow comparison between the old state and the new when setState() is called, and will not rerender if the two are identical. When the state is reference type, however, React doesn't perform a deep comparison between new and old. Based on this test here, when I setState() with a newly constructed (though identical to a human eye) object, it rerenders the component. This is expected. However, when you return count (the original object returned by useState()) or prev with a functional setState(), React somehow knows that it is the same state and decides not to re-render. I am curious as to how it is able to determine that, despite count/prev being identical to { value:0 } when a deep comparison is performed?
Here's a rough version of the code inside the component (check the above snippet for demo):
console.log('Component re-rendered') // Will log if 'willRerender' is true
const [count,setCount] = useState({ value: 0 })
const handleUpdate = willRerender => {
console.log('setState is called')
if (willRerender) {
setCount(count) // The same effect is achieved with setCount(prev => prev)
} else {
setCount({ value: 0 })
}
}
Yes, because both of them have the same reference, count == count, but, count != {value: 0}, this is expected comparison behaviour of javascript.

What's the drawback of `let count = useRef(0).current`

Example Code:
(Pretend we are inside component function)
let count = useRef(0).current
useEffect(()=>{
count++
console.log(count)
}, [count])
Question:
What will happen if I run this code
(I am afraid that the endless loop execution will blow up my m1 chip macbookair, so I didn't run ^_^).
Should I awalys some_ref_object.curent = some_value to change the value?
The code probably will not do what you expect.
useRef returns a mutable object, where the object is shared across renders. But if you extract a property from that object into a variable and then reassign that variable, the object won't change.
For the same reason, reassigning the number below doesn't change the object:
const obj = { foo: 3 };
let { foo } = obj;
foo = 10;
console.log(obj);
In your code, the ref object never gets mutated. It is always the following object:
{ current: 0 }
So
let count = useRef(0).current
results in count being initially assigned to 0 at the beginning of every render.
This might be useful if for some odd reason you wanted to keep track of a number inside a given render, not to persist for any other render - but in such a case, it'd make a lot more sense to remove the ref entirely and just do
let count = 0;
Your effect hook won't do anything useful either - since count is always 0 at the start of every render, the effect callback will never run (except on the first render).
Should I awalys some_ref_object.curent = some_value to change the value
You should use current, not curent. But yes - if you want the change to persist, assign to a property of the object, instead of reassigning a standalone variable. (Reassigning a standalone variable will almost never have any side-effects.)
But, if you have something in the view that the count is used in - for example, if you want to return it in the JSX somewhere - you probably want state instead of a ref (or a let count = 0;), so that setting state results in the component re-rendering and the view updating.
I just tried it on my colleague's computer, and fortunately it didn't blow up
Conclusion 1:
The useEffect won't effect, because ref can't be denpendency.
Conclusion 2:
Only let count = useRef(0).current is the right way.

`setNum(() => num++)` not working, but `setNum(() => num + 1)`

I am practicing React using Hooks and Context, working on a simple Quiz App. The score should increment to 1 when the answer is correct.
const { qa, questionNumber } = useContext(GlobalContext);
const [score, setScore] = useState(0);
const answerOnClick = (e) => {
const correct = qa[questionNumber].correct_answer === e ? true : false;
if (correct) {
setScore(() => score++);
}
};
But I'm getting this error on line setScore(() => score++);:
TypeError: Assignment to constant variable
I also tried if (correct) { score++; setScore(() => score); } and setScore(() => ++score), still not working.
But when I try setScore(() => score + 1);, now it increments!
I have learned that the Increment is a valid JS operator. Aren't score++ and score + 1 equivalent? And why score treat as a constant variable? It is mutable, right? I'm still a novice developer. Can someone explain what's happening here? Thank you.
The problem is, that score is defined as constant. This means that is should not be reassigned. When using score++ you're reassigning it as follows score = score + 1 but as a short hand. Your JavaScript interpreter does not like that you're reassigning a variable which you defined as being constant. Therefore you get the error.
The useState hook provides an update function (in your case setScore) which you should use to update the state. You're not directly changing the value of score. You're telling react to initialise score with a higher value on next render. Your components function is called again with a new declaration of score, this time with a higher value. As you correctly pointed out setScore(() => score + 1) works, however, setScore(score + 1) should work too.
You mutate the state when you do score++. You should not mutate the state in React because setNum is async.
https://reactjs.org/docs/react-component.html#setstate
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
score++/++score would mutate the state. The best way to update state based on previous state is this way:
setScore((prevScore) => prevScore + 1);

Redux - Is using getState not elegant or inefficient?

Lets say I have a lot of entities in global store
Apples
Shops
Juices
If I would like to have function getAppleJuicePrice, I can do it in 2-3 ways
Via parameters
function getAppleJuicePrice(apples, juices) {
return apples * juices; // Some computing
}
Via getState, no parameters
function getAppleJuicePrice() {
return (dispatch, getState) => {
const {apples, juices} = getState();
return apples * juices; // Some computing
}
}
Via getState and parameters
function getAppleJuicePrice(applesParam, juicesParam){
return (dispatch, getState) => {
const apples = applesParam || getState().apples;
const juices = juicesParam || getState().juices;
return apples * juices; // Some computing
}
}
*In case 2,3 I need to dispatch the function
Could you please give me your advise about
A) Functions 1,2,3 - Is it ok to do it in this way? Or some of them are better?
B) Architecture (I mean about fact that we are having a lot entities in the global store so we can even create functions which depend on them)
I'd strongly argue for example #1.
This function is pure, descriptive, and doesn't rely on outside knowledge. Assuming you've already got access to state, it's almost always better to derive the values you're after rather than directly requesting state.
If that's not possible, then factor out the data retrieval to a second function, then pass your values into the first. That way you've got a clear separation of concerns, your methods are easy to understand, you're not relying on side-effects (the presence of state values), and both methods should be easily testable.
Hell, you could go one further and refactor function 1 to be a generic utility for comparing/combining the prices of whatever, not just Apples and Juice.
const stateObject = getState() // At top of whatever scope
// Generic, no side-effects
function processPrice (x, y) {
return x * y; // Whatever processing
}
// Specific, injected dependency on state
function getAppleJuicePrice (state) {
const { apples, juice } = state;
return processPrice(apples, juice);
}
// Explicit value assignment
const appleJuicePrice = getAppleJuicePrice(stateObject)
Just a quick comment on store architecture: Always aim to keep it as simple, flat, and data-related as possible. Avoid compromising the data structure of your store simply for convenience of value computation. Anything you can derive locally (and easily) you should do so. The store isn't the place for complex value mutations.
(*preemptive this is not a hard rule, just a guideline)

Categories