Spoiler π«: this one is not worth reading. Upfront you are warned!β οΈ
react
is a library to define the blueprint of UI. It doesn't know how to render, it is just to define the UI.react-dom
is the one that takes care of the rendering part. We can pass the actual DOM element into who the react defined UI should be shoved into.
ReactDom.render(domElement,reactElement)
- for markup react uses something called JSX, which is not pure HTML. Under the hood, it is babel which transpile this to JavaScript
<div isBold>Shihab</div>
...
React.createElement('<div>',{ isBold:true },'Shihab')
In React.createElement()
, first and second params are fixed as type and props respectively. The rest of the arguments are treated as children, and it can be of any count.
React.createElement('<div>',{ isBold:true },'Shihab',' ','learning',' ','React')
...
<div>Shihab is learning React</div>
- React is UI in terms of state. What we tell React is for a given state, how the UI should look like. This is what makes React declaratively. If you start to think of React in terms of state, it is much easier and cleaner to build things. To relate, think of like a UI designer. For the given condition(state) what should be the UI like. If you initially start thinking about the interactions and all the stuff, you are actually making things difficult.
useState
-
useState
take an initialValue and return [currentValue,setValue] - if value changes it should re-render
function useState(initialValue){
function setValue(newValue){
initialValue = newValue
render()
}
return [initialValue,setValue]
}
function render(){
ReactDOM.render(....)
}
render()
But here, there is a problem. It is the component which calls the useState. So each time the component(function) gets called, it is calling useState afresh. In that case, if we try to set a new value to the initial value, it wouldnβt hold. Between the renders, it will lose value. So we should have some mechanism to persist the values, across renders. For that, we can use a global variable, let us call it states
.
states
will have the [currentValue,setValue] getting pushed for all renders.
const states=[]
function useState(initialValue){
function setValue(newValue){
states[0][0] = newValue
render()
}
const tuples = [initialValue,setValue]
states.push(tuples)
return tuples
}
function render(){
ReactDOM.render(....)
}
render()
Still, there are problems.
- It is evident that we keep on pushing into the
states
array and soon it will run out of space. - if we have multiple useStates inside the component, how we identify its current value from states array. So there should be some sort of identifier.
We only need to push to the states array on the initial render. On subsequent renders, we just have to find the respective value, update it and return its tuple.
Since the useStates are being called once per useState
statements inside the component, we can have, a call counter for the useState
. On each call we increment it and on each render we reset it.
const states=[]
let callCount=-1
function useState(initialValue){
const id = ++callCount
if(states[id]){
return states[id]
}
function setValue(newValue){
states[id][0] = newValue
render()
}
const tuples = [initialValue,setValue]
states.push(tuples)
return tuples
}
function render(){
callCount=-1
ReactDOM.render(....)
}
render()
To note, this is not the actual implementation of useState in React. This is just a thought. From the above snippet, we can conclude that,
- useState should be called in the same order and the same no.of times between the renders. In other words, useStates canβt be called inside conditions.
useRef
In the non-react world, we have different ways to reference or identify the DOM element. We usually use querySelectors
,document.getElementById
,etc. But in react, it is not encouraged much, because we can have multiple instances of the same component and if we use an id
to identify a DOM element, it would cause problems.
So in React world, we have refs
. refs
are usually used to "ref"erence DOM element in React.
// create ref
const inputRef=useRef()
// assign ref
<input ref={inputRef}/>
// use ref
inputRef.current.focus()
Note:
handler(e){
e.preventDefault()
}
<button onClick={(e)=>this.handler(e)}>Click Me</button>
// same as
<button onClick={this.handler}>Click Me</button>
Implicit arguments of callbacks are passed by default. We don't need an intermediate param for them to be captured.
useEffect
- we can think of them as side effects to be run
useEffect(()=>{
// effect
return ()=> // clean up
},[...deps])
- effect run when any of the deps changes and clean up follows
- if no deps is provided with the effect is triggered on each render and clean up follows
- if deps is an empty array, it runs only once in the entire life cycle of the component that too when the component mounts and clean up is triggered on the unmounting
Note: suppose you have a fetch inside an useEffect and you want to avoid the unwanted state updates if the useEffect is triggered while the current fetch is in progress. Use the below snippet for the clean up the intermittent calls, just before the last
useEffect(()=>{
let isCurrent = true
fetchUser(id).then(user=>{
if(isCurrent) setUser(user)
})
return ()=> isCurrent=false
},[id])
function clickHandler(id){
setId(id)
}
over here, no matter how many times the clickHandler
is triggered, only the last one will trigger a state update
useContext
const MyContext=React.createContext()
function App(){
const [productId,setProductId]=useState(1)
return (
<MyContext.Provider value={{ productId,nsetProductId }}>
<Header/>
<Body>
<Product/>
</Body>
<Footer/>
</MyContext.Provider>
}
// deeply nested child component
function Product(){
const { productId,setProductId } = useContext(MyContext)
return (
<Container>
...
</Container>
)
}
useReducer
- it helps to keep states together
- the changes can be a bit more self-descriptive
- no need to call different setState (from useState) to update different states
function fetchAds(){
try{
setLoading(true)
...
}catch(err){
setLoading(false)
setError(err)
}
}
...
function fetchAds(){
try{
dispatch({type:'FETCH_ADS'}) // more self-descriptive
...
}catch(err){
dispatch({type:'FETCH_ADS_FAILED'}) // no multiple setState calls
}
}
- in fact, useState is implemented on top of useRedcer
const initialState = {
counter: 0
};
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { ...state, counter: state.counter + 1 };
...
default:
throw Error(`Unhandled action type: ${action.type}`);
}
}
function App(){
const [state, dispatch] = useReducer(reducer, initialState);
const { counter } = state;
function increment() {
dispatch({ type: "INCREMENT" });
}
...
return (
<div className="App">
<h1>Count: {counter}</h1>
<div>
<button onClick={increment}>Increment</button>
...
</div>
</div>
);
}
Note: in Redux reducers, we usually return the state as the default case inside the reducer switch. That is because we have the reducers separated as different files. When an action type is being dispatched all of the reducers get invoked but its handler resides inside the subset of global reducers. But for useReducer
s we need not do that. These are most of the time local to the components and are only invoked from it. We will have a clear idea about the action types. And practically we should be notified if any unhandled action types are dispatch
ed. So it is ok if we throw an error in the default case of these reducers.
- by using useReducer with the context we can implement our own Redux. Sample gist
Top comments (0)