skip to Main Content

I’m trying to understand memo and useCallback and I’ve made this minimal example:

import { memo, useCallback, useState } from "react";
import { Button, Text, TouchableOpacity, SafeAreaView } from "react-native";

const Item = memo(({ isSelected, index, onClicked }) => {
  return (<TouchableOpacity onPress={onClicked}>
    <Text style={{ backgroundColor: (isSelected ? "red" : "transparent"), paddingVertical: 10 }}>{index}</Text>
  </TouchableOpacity>)

})


export default function App() {

  const [selected, setSelected] = useState(0)
  const [count, setCount] = useState(10)

  let items = []
  for (let i = 0; i < count; i++)

    items.push(
      <Item
        key={i}
        index={i}
        isSelected={selected == i}
        onClicked={useCallback(() => setSelected(i), [])}

      />
    )

  return (<SafeAreaView>
    <Button title="Change to 5" onPress={() => { setCount(5); setSelected(1) }} />

    {items}

  </SafeAreaView>)
}

This works fine until I change the count by clicking on Button

and It gives the error:

Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

I don’t understand what I’m doing wrong?

Don’t I have to use useCallback() so that the <Item> don’t get re rendered due to a "new function" (onClicked) being passed in as props every time?

How do I avoid re rendering every time a new item is selected?


Edit

Based on Konrad’s comment I utilized useMemo like this:

import { memo, useCallback, useMemo, useState } from "react";
import { Button, Text, TouchableOpacity, SafeAreaView } from "react-native";

const Item = memo(({ isSelected, index, onClicked }) => {

  return (<TouchableOpacity onPress={onClicked}>
    <Text style={{ backgroundColor: (isSelected ? "red" : "transparent"), paddingVertical: 10 }}>{index}</Text>
  </TouchableOpacity>)

})


export default function App() {

  const [selected, setSelected] = useState(0)
  const [count, setCount] = useState(10)

  const func_arr = useMemo(() => {

    let arr = []
    for (let i = 0; i < count; i++) // loop 1
      arr.push(() => setSelected(i))

    return arr

  },[count])

  let items = []
  for (let i = 0; i < count; i++) // loop 2

    items.push(
      <Item
        key={i}
        index={i}
        isSelected={selected == i}
        onClicked={func_arr[i]} // How do I pass arguments to this?

      />
    )

  return (<SafeAreaView>
    <Button title="Change to 5" onPress={() => { setCount(5); setSelected(1) }} />

    {items}

  </SafeAreaView>)
}

But this raises more questions then it solves:

  1. How do I avoid using 2 loops? One for the <Item> & the other for useMemo

  2. How do I pass arguments to func_arr[i]?

2

Answers


  1. Chosen as BEST ANSWER

    Not exactly a solution (more of a workaround) but it's possible to pass the useCallback function itself as such:

    import { memo, useCallback, useMemo, useState } from "react";
    import { Button, Text, TouchableOpacity, SafeAreaView } from "react-native";
    
    const Item = memo(({ isSelected, index, onClicked }) => {
    
      console.log("Render Item", index)
    
      return (<TouchableOpacity onPress={() => onClicked(index)}>
        <Text style={{ backgroundColor: (isSelected ? "red" : "transparent"), paddingVertical: 10 }}>{index}</Text>
      </TouchableOpacity>)
    
    })
    
    
    export default function App() {
    
      const [selected, setSelected] = useState(0)
      const [count, setCount] = useState(10)
    
      const changeSelected = useCallback((index) => {
        setSelected(index)
      }, [])
    
    
      let items = []
      for (let i = 0; i < count; i++) {
    
        items.push(
          <Item
            key={i}
            index={i}
            isSelected={selected == i}
            onClicked={changeSelected}
    
          />
        )
      }
    
      return (<SafeAreaView>
        <Button title="Change to 5" onPress={() => { setCount(5); setSelected(1) }} />
    
        {items}
    
      </SafeAreaView>)
    }
    

  2. A couple of things here, reacts expects you to allways use the same hooks, hooks should not be conditionally used, nor used inside a loop that can change.

    In your case, when you change the value of "count", you will use "useCallback" one less time, that’s why you get that error message.

    The good news is your workaround is actually correct, let me explain, the "useCallback" hook is used to prevent Reat to re-compiling the function everytime the component gets updated.

    That being said, what you want to do is define a function that is going to always be the same, in your case:

    (index) => { setSelected(index) }
    

    That function will allways behave the same, so it does not need to be built over and over. Then, you use useCallback to be able to use that function everywhere you need it.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search