So, you’ve learned the basics of React, then when you finally started using you started falling for one pitfall after another. Well, you’re in the right place... finally!
Few things:
- This is not to be a comprehensive guide
- It is not for those just learning (hence 102 and not 101)
- Also, I might take some generalizations for the sake of simplicity
- as you may know… JS is weird!
- Finally, I’ll pretend class components don’t exist and so neither should you.
- I’m not saying you never should learn them, but today… not at the 102 level.
If you do want to learn from zero, I recommend checking out the Beta React Docs, you’ll learn things there that many people using it for quite a while don’t know (really!).
Finally, feel free to ask anything you might be having problems with. I love to help! (It might also help me find things to write, so win-win-win.)
Lifecycle and data flows
One of the biggest enemies of those starting React is to render stuff on the screen and have it change.
The basic render
function BasicRender(){
return (
<div>I'm rendering!</div>
)
}
This will just render whatever is in the JSX.
With props
function WithProps({counter}){
return (
<div>The counter is: { counter }</div>
)
}
First thing is that I’m destructuring the props value.
This will render at { counter }
whatever the counter value is. And every time it gets a new value, it will render the new value.
In this case, there are two things to know/remember:
- React shallowly compares to check if the component should rerender or not.
- The difference between primitives and non-primitives.
Knowing that you would know that primitives are checked by their value while everything else is checked by their reference.
With split flows
function WithIf() {
const random = Math.random();
if (random > 0.5) {
return (
<div>Random is big!</div>
);
} else {
return (
<div>Random is small!</div>
);
}
}
if
works just as you expect, there are rules about hooks where you can’t initiate them inside split flows (you can only use them at the top level of the component), but after that, it works just like that.
With state
function WithState() {
const [counter, setCounter] = useState(0);
return (
<div>
<div>The counter value is: { counter }</div>
<button onClick={() => setCounter(counter + 1)}>Click me!</button>
</div>
)
}
function WithVariable() { // ???
let counter = 0;
return (
<div>
<div>The counter value is: { counter }</div>
<button onClick={() => counter += 1}>Click me!</button>
</div>
)
}
First things first: the second approach, with variable, will not work.
Why? The short answer is that React needs a hook to know that something changed in order to rerender. This happens because functional components are closures and while it will indeed change the value internally, React will never know when to rerender the component and when it does rerender it will recreate it at the 0
value.
So, why does the first approach works? That’s the React magic, and for now, you should just accept it and what you can do with it.
Remember about the props? That also applies to the state.
Non-primitives like objects or arrays, if you simply change the value inside and return the object, this means that the reference doesn’t change, even if you use the setState
function.
The right way to think is that you always need to return a new reference, this means:
const newArr = [...oldArr];
const newObj = {
...oldObj
};
// spreading itself doesn't create new references
// but the container you're spreading it into is a new reference.
With effects
function WithEffect1() {
const [state, setState] = useState('initial state');
useEffect(() => {
setState('useEffect []');
const timeout = setTimeout(() => {
setState('useEffect [] > timeout');
}, 500);
return () => {
setState('useEffect [] > return');
clearTimeout(timeout);
};
}, []);
return (
<div>{state}</div>
);
}
// you might see below duplicated, probably with the return,
// but in dev mode, it's the Strict Mode
// also, the initial state you'll never see in this situation
// because react is really fast
// If you slowed time down, you might see this happening:
/**
* initial state
* useEffect []
* ~500 ms~
* useEffect [] > timeout
*/
This is the most basic use of useEffect
. (Think of the setTimeout
as a fetch
call.)
Usually, we have more things going on, for example:
function WithEffect2() {
const [state, setState] = useState('initial state');
const [otherState, setOtherState] = useState(0);
useEffect(() => {
setState('useEffect [otherState]');
const timeout = setTimeout(() => {
setState('useEffect [otherState] > timeout');
}, 500);
return () => {
setState('useEffect [otherState] > return');
clearTimeout(timeout);
};
}, [otherState]);
return (
<div>
<div>{state}</div>
<button onClick={() => setOtherState(otherState + 1)}>
Change the Other State
</button>
</div>
);
}
/**
* initial state
* useEffect [otherState]
* ~500 ms~
* useEffect [otherState] > timeout
* ~click~
* // batched update, you'll never see the return
* [useEffect [otherState] > return | useEffect [otherState]]
* ~500 ms~
* useEffect [otherState] > timeout
*/
In this one there are more things happening.
But the idea is that the initial state
will always be rendered first, regardless.
Then any and all useEffects
will render the very next tick.
Anything that’s asynchronous like promises
or fetches
calls will render so soon they finish.
Return statements inside the useEffect
trigger before the next calculation when they are triggered.
Usually, you have a state that will be rendered first (usually you’ll render as a nullish value using a if
to render a Loading...
or something like this). Then the fetch
will start, finish and set a new state, that will trigger a render, now with the values.
One thing to take care is that if you put in the dependency array
the very state
you’re mutating in the useEffect
, unless you have some check before, you’ll probably trigger an infinite recursion:
// this will break your page!
useEffect(() => {
// add some exit condition if you really need to do this
setState(state + 1);
}, [state])
Bonus: derived state
function DerivedState({ myValue }) {
const [state, setState] = useState(0);
const [secondState, setSecondState] = useState(0);
useEffect(() => {
setState(myValue * 2);
}, [myValue]);
useEffect(() => {
setSecondState(state * 2);
}, [state])
return (
<div>
<div>{state}</div>
<div>{secondState}</div>
</div>
);
}
function DoThisInstead({ myValue }) {
const stateToRender = myValue * 2;
const secondState = stateToRender * 2;
return (
<div>
<div>{stateToRender}</div>
<div>{secondState}</div>
</div>
);
}
Things like this are common. If you already have the value, you don’t need to put it inside a state
and use useEffect
to change it.
The way it’s been done in the first example you’ll see a cascade happening every time myValue
changes and it might have unintended consequences.
If it comes from props
, you can just calculate from it, when it changes, it will recalculate automatically.
If you’re doing something like a filteredItems
from some fetch
, same thing. The useEffect
will save to the state
the values, then you just use a const filtered = state.filter()
.
Every time state
changes, so will the filter. If you need, you can always memo
the values.
Again: questions? Just ask! (And to not be ignored… read this helpful “how to ask a good question”)
And… question for YOU! Where are you having trouble with React?
Cover Photo by Solen Feyissa on Unsplash