1

I'm making a notes app in React Native and trying to make it so I can click on a note in a FlatList to edit it. I'm using react-router-native for this. I get an Error when clicking on any FlatList item. I know that this error has been asked on stack overflow before but the answers are all for class components, whereas I'm using functional components.

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. 
import { FlatList, Pressable, StyleSheet, View } from "react-native"
import { useNavigate } from "react-router-native"
import theme from "../theme"
import Text from "./Text"

const styles = StyleSheet.create({
  separator: {
    height: 10,
    backgroundColor: theme.colors.background,
  },
  item: {
    padding: 8,
    backgroundColor: "white",
  },
})
const ItemSeparator = () => <View style={styles.separator} />

const renderItem = ({ item }) => (
  <View style={styles.item}>
    <Pressable onPress={() => useNavigate(`/${item.id}`)}>
      <Text fontWeight="bold" fontSize="subheading">
        {item.title}
      </Text>
      <Text>{item.body}</Text>
    </Pressable>
  </View>
)

const NoteList = ({ notes }) => {
  return (
    <FlatList
      data={notes}
      ItemSeparatorComponent={ItemSeparator}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
    />
  )
}
George Garman
  • 83
  • 1
  • 6

1 Answers1

0

useNavigate is a React hook and can only be called by a React function component or other custom React hook. It cannot be called in nested functions/callbacks.

Move the useNavigate hook call to the NoteList component and refactor the renderItem callback to curry a passed navigate function.

const ItemSeparator = () => <View style={styles.separator} />;

const renderItem = (navigate) => ({ item }) => (
  <View style={styles.item}>
    <Pressable onPress={() => navigate(`/${item.id}`)}>
      <Text fontWeight="bold" fontSize="subheading">
        {item.title}
      </Text>
      <Text>{item.body}</Text>
    </Pressable>
  </View>
);

const NoteList = ({ notes }) => {
  const navigate = useNavigate(); // <-- hook called in React function

  return (
    <FlatList
      data={notes}
      ItemSeparatorComponent={ItemSeparator}
      renderItem={renderItem(navigate)} // <-- pass navigate
      keyExtractor={(item) => item.id}
    />
  );
};

Alternatively you could move the renderItem function declaration into the NoteList component so the navigate function is just closed over in callback scope.

const ItemSeparator = () => <View style={styles.separator} />;

const NoteList = ({ notes }) => {
  const navigate = useNavigate();

  const renderItem = ({ item }) => (
    <View style={styles.item}>
      <Pressable onPress={() => navigate(`/${item.id}`)}>
        <Text fontWeight="bold" fontSize="subheading">
          {item.title}
        </Text>
        <Text>{item.body}</Text>
      </Pressable>
    </View>
  );

  return (
    <FlatList
      data={notes}
      ItemSeparatorComponent={ItemSeparator}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
    />
  );
};
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • I originally got this "Warning: Functions are not valid as a React child." but it seems to have disappeared now and your solution works, although eslint doesn't seem to like curried functions. Any idea why I got the warning? – George Garman Jan 13 '23 at 18:32
  • @GeorgeGarman What is the lint warning? I've just updated my answer to include an implementation that doesn't curry any arguments. – Drew Reese Jan 13 '23 at 18:40
  • "Component definition is missing display name", because the component ({item}) => ... doesn't have a name. Your second implementation takes away the error. Thank you! – George Garman Jan 13 '23 at 18:52
  • @GeorgeGarman Hmm, that might just be an Intellisense quirk between the linter and the IDE doing some static code analysis. Glad the second implementation is more usable for you. Cheers. – Drew Reese Jan 13 '23 at 19:02