Search code examples
javascriptreactjsreact-nativereact-hooksfrontend

Preventing re-render without using useCallback(), useMemo() or any custom hooks


In the following code I have a Button component and a Todo component which when rendered looks like this:

enter image description here

Clicking any button gives the corresponding todo using an API call.

App.jsx:

import { useEffect, useState } from 'react'
import './App.css'
import Button from './Button'
import Todo from './Todo'
import axios from 'axios'

export default function App() {
  const arr = [1, 2, 3, 4];
  const [todo, setTodo] = useState({title:"",description:""});
  useEffect(()=>eventFunc(1),[]);

  function eventFunc(id) {
    (async () => {
      const res = await axios.get(`https://sum-server.100xdevs.com/todo?id=${id}`);
      setTodo(res.data.todo);
    })();
  }
  
  return (
  <>
  <h1>Hello CodeSandbox</h1>
      {arr.map((id) => (
        <Button key={id} eventFunc={eventFunc} id={id} />
      ))}

      <Todo todo={todo} />
      </>
  )
}

Button.jsx:

export default function Button({ id, eventFunc }) {
    return <button onClick= {()=>eventFunc(id)}key={id}> Button {id}</button>;
  }

Todo.jsx:

export default function Todo({todo}){
    return (
        <>
        <h1>{todo.title}</h1>
        <h3>{todo.description}</h3>
        </>
        
    )
}

Question 1: How can I prevent the re-rendering of the Button component without using custom hooks, useCallback() or useMemo()?

Question 2: Is there any possible way to set the initial state value to be the first todo (using the api call) and not an empty todo object?

I tried the following solution: Keeping the eventFunc() outside thge scope of App() function and then wrapping the button component inside the memo() function. But it didn't work and gave me the error: "Uncaught TypeError: Cannot read properties of undefined (reading 'title') at Todo (Todo.jsx:4:19)"
Question3: Why am I getting this error?

Failed solution:

App.jsx:

import { useEffect, useState } from 'react'
import './App.css'
import Button from './Button'
import Todo from './Todo'
import axios from 'axios'

function eventFunc(id) {
  (async () => {
    const res = await axios.get(`https://sum-server.100xdevs.com/todo?id=${id}`);
    return res.data.todo;
  })();
}

export default function App() {
  const arr = [1, 2, 3, 4];
  const [todo, setTodo] = useState({title:"",description:""});
  useEffect(()=>setTodo(eventFunc(1)),[]);

  return (
  <>
    <h1>Hello CodeSandbox</h1>
      {arr.map((id) => (
        <Button key={id} eventFunc={eventFunc} id={id} />
      ))}

    <Todo todo={todo} />
  </>
  )
}

Button.jsx:

import {memo} from 'react'

export default memo(function Button({ id, eventFunc }) {
    return <button onClick= {()=>eventFunc(id)}key={id}> Button {id}</button>;
  })

Solution

  • Question 1: How can I prevent the re-rendering of the Button component without using custom hooks, useCallback() or useMemo()?

    React.memo is part way there, but it relies on the props not changing. eventFunc is recreated on every render, which breaks the memoization. The typical solution for this is to then wrap eventFunc in a useCallback. I'm not sure why you've excluded that as an option; if you give some more details on why you have that restriction, maybe i can come up with something.

    Question 2: Is there any possible way to set the initial state value to be the first todo (using the api call) and not an empty todo object?

    Some component will have to do a render before the data is loaded, and then rerender once it is loaded. But it doesn't need to be this component. You can split the code into two components, one of which is responsible for loading the data, and another which only gets rendered once the data exists.

    const Loader = () => {
      const [data, setData] = useState(null)
      const [loading, setLoading] = useState(true);
      useEffect(() => {
        // load the data, then update the states
      }, []);
    
      if (loading) {
        return null; // or some placeholder
      } else {
        return <App data={data} />
      }
    }
    
    const App = ({ data }) = >{
      const [todo, setTodo] = useState(data);
    
      // ...
    }
    

    Question3: Why am I getting this error?

    Because eventFunc returns undefined. That in turn means setTodo(eventFunc(1)) sets the state to undefined.

    eventFunc is currently creating an anonymous asynchronous function, and immediately invoking it. The anonymous function runs until the await, then returns a promise to eventFunc. eventFunc ignores this promise and finishes running. It has no explicit return statement, so it's implicitly returning undefined. Some time later, axios.get finishes and the async function resumes, but that won't retroactively change the fact that eventFunc returned undefined.

    Instead, make eventFunc be the async function itself:

    async function eventFunc(id) {
      const res = await axios.get(`https://sum-server.100xdevs.com/todo?id=${id}`);
      return res.data.todo;
    }
    

    And then await the promise it returns:

    useEffect(()=>{
      const load = async () => {
        const todo = await eventFunc(1);
        setTodo(todo);
      }
      load();
    },[]);