typescriptfirebasereact-nativeerror-handlingreact-context

Uncaught Error: useNatureRecord must be used within a NatureRecordProvider in React Native TypeScript app


I'm developing a React Native app with TypeScript and using Firebase for data storage. In my app, I have a context provider (NatureRecordProvider) that should wrap two main screens, NatureRecordForm and RecordStorage. However, when I navigate to these screens, I encounter the following error in my web server:

Error Message:

enter image description here

The full error trace suggests that useNatureRecord is being used outside of the NatureRecordProvider, though I believe I've wrapped the necessary components with the provider.

Here’s a summary of my setup:

  1. Context Setup:
  1. Navigation Configuration:
  1. Component Usage:

Code Samples:

  1. RecordNavigation.tsx:
const RecordNavigation = () => {
  return (
    <NatureRecordProvider>
      <NavigationContainer>
        <Stack.Navigator initialRouteName="NatureRecordForm">
          <Stack.Screen name="NatureRecordForm" component={NatureRecordForm} />
          <Stack.Screen name="RecordStorage" component={RecordStorage} />
        </Stack.Navigator>
      </NavigationContainer>
    </NatureRecordProvider>
  );
};
  1. RecordStorage.tsx
import React, { useState, useEffect } from 'react';
import { ScrollView, Alert, View, Text, Image, StyleSheet } from 'react-native';
import { useNatureRecord } from '../../RecordLogic/RecordContext'; // Adjust the path as needed
import TopBanner from '../../RecordComponents/TopBanner';
import TabNavigation from '../../RecordComponents/TabNavigation';
import { NatureRecord as ImportedNatureRecord, NatureRecord } from '../../RecordLogic/NatureRecordInput';

interface LocalNatureRecord extends ImportedNatureRecord {
  sliders: any;
}

const RecordStorage: React.FC = () => {
  console.log("NatureRecordStorage component rendered");

  const { environmentalRecords: rawEnvironmentalRecords, biologicalRecords: rawBiologicalRecords, fetchRecords } = useNatureRecord();
  const [selectedTab, setSelectedTab] = useState<string>('environmental');

  useEffect(() => {
    fetchRecords();
  }, []);

  const environmentalRecords: LocalNatureRecord[] = rawEnvironmentalRecords.map(record => ({
    ...record,
    id: record.id || 0, // Ensure id is present
  }));
  
  const biologicalRecords: LocalNatureRecord[] = rawBiologicalRecords.map(record => ({
    ...record,
    id: record.id || 0, // Ensure id is present
  }));

  const handleClosePress = () => {
    Alert.alert(
      "닫기",
      "정말 닫으시겠습니까?",
      [
        { text: "취소", onPress: () => console.log("Close canceled"), style: "cancel" },
        { text: "확인", onPress: () => console.log("Close confirmed") }
      ]
    );
  };

  const handleTabPress = (tab: string) => {
    setSelectedTab(tab);
  };

  const { EnvironmentalRecordsList, BiologicalRecordsList }: { EnvironmentalRecordsList: React.FC<{ records: LocalNatureRecord[]; }>; BiologicalRecordsList: React.FC<{ records: LocalNatureRecord[]; }>; } = createRecordLists();

  return (
    <ScrollView style={{ flex: 1, padding: 0, margin: 0 }}>
      <TopBanner 
        source={require('C:\\app\\my-app\\NatureImg\\close.svg')} 
        text="도감"
        onClosePress={handleClosePress}
      />
      <TabNavigation onTabPress={handleTabPress} selectedTab={selectedTab} />
      <View style={{ padding: 20 }}>
        {selectedTab === 'environmental' && (
          <EnvironmentalRecordsList records={environmentalRecords} />
        )}
        {selectedTab === 'biological' && (
          <BiologicalRecordsList records={biologicalRecords} />
        )}
      </View>
    </ScrollView>
  );

  function createRecordLists() {
    const EnvironmentalRecordsList: React.FC<{ records: NatureRecord[]; }> = ({ records }) => (
      <View>
        {records.map((record, index) => (
          <View key={index} style={styles.recordContainer}>
            <Text style={styles.reviewText}>{record.review}</Text>
            <Image source={{ uri: record.photo }} style={styles.photo} />
            {record.sliders.map((slider: { name: string; value: number; }, sliderIndex: number) => (
              <Text key={sliderIndex} style={styles.sliderText}>{slider.name}: {slider.value}</Text>
            ))}
          </View>
        ))}
      </View>
    );

    const BiologicalRecordsList: React.FC<{ records: NatureRecord[]; }> = ({ records }) => (
      <View>
        {records.map((record, index) => (
          <View key={index} style={styles.recordContainer}>
            <Text style={styles.reviewText}>{record.review}</Text>
            <Image source={{ uri: record.photo }} style={styles.photo} />
            {record.sliders.map((slider: { name: string; value: number; }, sliderIndex: number) => (
              <Text key={sliderIndex} style={styles.sliderText}>{slider.name}: {slider.value}</Text>
            ))}
          </View>
        ))}
      </View>
    );
    return { EnvironmentalRecordsList, BiologicalRecordsList };
  }
};

const styles = StyleSheet.create({
  recordContainer: {
    marginBottom: 20,
    padding: 10,
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 5,
  },
  reviewText: {
    fontSize: 16,
    fontWeight: 'bold',
  },
  photo: {
    width: 100,
    height: 100,
    marginVertical: 10,
  },
  sliderText: {
    fontSize: 14,
  },
});

export default RecordStorage;
  1. NatureRecordForm.tsx
import React, { useState, useMemo, useCallback } from 'react';
import { ScrollView, View, TextInput, StyleSheet, Alert, Text } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useNatureRecord } from '@/RecordLogic/RecordContext';

import SaveButton from '@/RecordComponents/SaveButton'; 
import ReviewComponentStyle from '@/RecordStyle/ReviewComponentStyle'; 
import TopBanner from '@/RecordComponents/TopBanner';
import SliderComponent from '@/RecordComponents/SliderComponent';
import PhotoDisplay from '@/RecordComponents/PhotoDisplay';
import BigQuestionStyle from '@/RecordStyle/BigQuestionStyle';

const initialSliders = [
  { name: '탐험한 곳은 얼마나 아름다웁니까?', value: 50 },
  { name: '다른사람에게 이곳을 탐험하는 걸 추천하고 싶으면 어느정도?', value: 50 },
  { name: '탐험하는 동안 얼마나 즐거웠나요?', value: 50 },
  { name: '탐험하는 동안 얼마나 가치있었나요?', value: 50 },
  { name: '탐험 난이도는 어느정도 였나요?', value: 50 },
];

const labels = [
  { text: '아름다움' },
  { text: '추천 수준' },
  { text: '즐거움' },
  { text: '가치' },
  { text: '난이도' },
];

const NatureRecordForm: React.FC = () => {
  const [review, setReview] = useState('');
  const [photo, setPhoto] = useState('');
  const [sliders, setSliders] = useState(initialSliders);
  const [inputHeight, setInputHeight] = useState(40); // Initial height for the TextInput
  const navigation = useNavigation<any>();
  const { addEnvironmentalRecord } = useNatureRecord();

  const memoizedSliders = useMemo(() => sliders, [sliders]);

  const handleAddRecord = useCallback(async () => {
    try {
      const newRecord = {
        id: Date.now(), // Generate a unique id
        review,
        photo,
        sliders: memoizedSliders,
        timestamp: new Date().toISOString(), // Ensure timestamp is in a consistent format
      };

      await addEnvironmentalRecord(newRecord);

      Alert.alert('Record saved successfully!');
      navigation.navigate('RecordStorage'); // Navigate to RecordStorage page
    } catch (error) {
      console.error('Error saving record: ', error);
      Alert.alert('Failed to save record.');
    }
  }, [review, photo, memoizedSliders, addEnvironmentalRecord, navigation]);

  const handleSave = useCallback((data: { name: string; value: number }[]) => {
    setSliders(data);
  }, []);

  const handleClosePress = useCallback(() => {
    Alert.alert(
      "뒤로 넘어가면 기록이 삭제되는데 괜찮습니까?",
      "",
      [
        { text: "취소", style: "cancel" },
        { text: "확인", onPress: () => console.log("Close confirmed") }
      ]
    );
  }, []);

  return (
    <ScrollView style={{ flex: 1, padding: 0, margin: 0 }}>
      <TopBanner 
       source={require('C:\\app\\my-app\\NatureImg\\close.svg')} 
       text="자연환경 도감 등록"
       onClosePress={handleClosePress}
      />
      {photo ? <PhotoDisplay photoUri={photo} /> : null}
      <View style={BigQuestionStyle.container}>
        <Text style={BigQuestionStyle.howWouldYou}>탐험을 하셨군요!{'\n'}{'\n'}다음을 평가해주세요!</Text>
      </View>
      <SliderComponent initialSliders={memoizedSliders} onSave={handleSave} labels={labels} />
      <View style={{ marginBottom: 20 }} />
      <View style={ReviewComponentStyle.inputContainer}>
        <View style={ReviewComponentStyle.inputWrapper}>
          <TextInput
            placeholder="한 줄 입력"
            value={review}
            onChangeText={setReview}
            style={[ReviewComponentStyle.input, { height: inputHeight }]}
            multiline
            onContentSizeChange={(e) => setInputHeight(e.nativeEvent.contentSize.height)}
          />
        </View>
      </View>
      <TextInput
        placeholder="사진 URL 입력"
        value={photo}
        onChangeText={setPhoto}
        style={ReviewComponentStyle.photo}
      />
      <View style={styles.buttonWrapper}>
        <SaveButton title="기록 저장" onPress={handleAddRecord} />
      </View>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  buttonWrapper: {
    marginVertical: 20,
    alignItems: 'center',
  },
});

export default React.memo(NatureRecordForm);
  1. RecordContext.tsx
import React, { createContext, useState, useContext, ReactNode, useEffect } from "react";
import { collection, addDoc, getDocs } from 'firebase/firestore';
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
import { db, storage } from '@/firebase';

interface NatureRecord {
  id: number;
  photo: string;
  review: string;
  sliders: { name: string; value: number }[];
}

interface NatureRecordContextProps {
  environmentalRecords: NatureRecord[];
  biologicalRecords: NatureRecord[];
  addEnvironmentalRecord: (record: NatureRecord) => Promise<void>;
  addBiologicalRecord: (record: NatureRecord) => Promise<void>;
  fetchRecords: () => Promise<void>;
}

const NatureRecordContext = createContext<NatureRecordContextProps | undefined>(undefined);

export const NatureRecordProvider = ({ children }: { children: ReactNode }) => {
  console.log("Provider rendering"); // Add this line to check if the provider is rendering

  const [environmentalRecords, setEnvironmentalRecords] = useState<NatureRecord[]>([]);
  const [biologicalRecords, setBiologicalRecords] = useState<NatureRecord[]>([]);

  const addEnvironmentalRecord = async (record: NatureRecord) => {
    console.log("Adding environmental record", record); // Debugging log
    try {
      let photoURL = '';
      if (record.photo) {
        const storageReference = ref(storage, `photos/${Date.now()}_${record.photo}`);
        const response = await fetch(record.photo);
        const blob = await response.blob();
        await uploadBytes(storageReference, blob);
        photoURL = await getDownloadURL(storageReference);
      }

      const newRecord = {
        ...record,
        photo: photoURL,
        timestamp: new Date().toISOString(),
      };

      await addDoc(collection(db, 'environmentalRecords'), newRecord);
      setEnvironmentalRecords([...environmentalRecords, newRecord]);
    } catch (error) {
      console.error('Error saving record to Firebase: ', error);
    }
  };

  const addBiologicalRecord = async (record: NatureRecord) => {
    console.log("Adding biological record", record); // Debugging log
    try {
      let photoURL = '';
      if (record.photo) {
        const storageReference = ref(storage, `photos/${Date.now()}_${record.photo}`);
        const response = await fetch(record.photo);
        const blob = await response.blob();
        await uploadBytes(storageReference, blob);
        photoURL = await getDownloadURL(storageReference);
      }

      const newRecord = {
        ...record,
        photo: photoURL,
        timestamp: new Date().toISOString(),
      };

      await addDoc(collection(db, 'biologicalRecords'), newRecord);
      setBiologicalRecords([...biologicalRecords, newRecord]);
    } catch (error) {
      console.error('Error saving record to Firebase: ', error);
    }
  };

  const fetchRecords = async () => {
    console.log("Fetching records"); // Debugging log
    try {
      const environmentalSnapshot = await getDocs(collection(db, 'environmentalRecords'));
      const biologicalSnapshot = await getDocs(collection(db, 'biologicalRecords'));

      const environmentalData = environmentalSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) as unknown as NatureRecord[];
      const biologicalData = biologicalSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) as unknown as NatureRecord[];

      setEnvironmentalRecords(environmentalData);
      setBiologicalRecords(biologicalData);
    } catch (error) {
      console.error('Error fetching records from Firebase: ', error);
    }
  };

  useEffect(() => {
    fetchRecords();
  }, []);

  return (
    <NatureRecordContext.Provider value={{ environmentalRecords, biologicalRecords, addEnvironmentalRecord, addBiologicalRecord, fetchRecords }}>
      {children}
    </NatureRecordContext.Provider>
  );
};

export const useNatureRecord = () => {
  const context = useContext(NatureRecordContext);
  if (!context) {
    throw new Error("useNatureRecord must be used within a NatureRecordProvider");
  }
  return context;
};

Troubleshooting Steps:

Questions:

Any guidance on resolving this error would be greatly appreciated!

I expected the NatureRecordProvider context to provide data to all components within the navigation stack, as I wrapped the navigation stack in NatureRecordProvider. I anticipated that useNatureRecord would work within both NatureRecordForm and RecordStorage without error. However, instead, I encountered the error message: “useNatureRecord must be used within a NatureRecordProvider.”


Solution

  • Here are a couple of things you could consider:

    First, your import statements are different:

    NatureRecordForm: import { useNatureRecord } from '@/RecordLogic/RecordContext'; RecordStorage: import { useNatureRecord } from '../../RecordLogic/RecordContext';

    Ensure they are pointing to the same useNatureRecord function within the same Context.

    Secondly, try moving Navigator and Provider into App.tsx to ensure all components are wrapped and re-validate. This way you will also debug if there are any components missing the ProviderWrapper

    import React from 'react';
    import { NavigationContainer } from '@react-navigation/native';
    import RecordNavigation from './RecordNavigation';
    import { NatureRecordProvider } from './RecordContext';
    
    const App = () => {
      return (
        <NatureRecordProvider>
          <NavigationContainer>
            <RecordNavigation />
          </NavigationContainer>
        </NatureRecordProvider>
      );
    };
    
    export default App;