Search code examples
reactjsreact-nativereact-native-flatlistreact-router-native

Invalid hook call React Native FlatList Navigation


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}
    />
  )
}

Solution

  • 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}
        />
      );
    };