DEV Community

Cover image for 🎣 [Hookable] βš›οΈ[React]
Shihabudheen US
Shihabudheen US

Posted on β€’ Edited on

1

🎣 [Hookable] βš›οΈ[React]

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')
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
  • 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

CodeSandbox

  • 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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Note:

handler(e){
 e.preventDefault()
}

<button onClick={(e)=>this.handler(e)}>Click Me</button>

// same as
<button onClick={this.handler}>Click Me</button>
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode
  • 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)
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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
 }
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Codesand box

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 useReducers 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 dispatched. 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)

The best way to debug slow web pages cover image

The best way to debug slow web pages

Tools like Page Speed Insights and Google Lighthouse are great for providing advice for front end performance issues. But what these tools can’t do, is evaluate performance across your entire stack of distributed services and applications.

Watch video

πŸ‘‹ Kindness is contagious

Please leave a ❀️ or a friendly comment on this post if you found it helpful!

Okay