DEV Community

Pierre
Pierre

Posted on

useContext does not update or show the value

I need to save the ID of a field from the settings.tsx view. That value need to be used with other view, like stations.tsx, alarms.tsx, map.tsx.

If later, I need to watch another field, I can change the field from a drop menu and the new value must be used from the other field. It meens, I should be able to print the select ID filed from the other View, but it does not!

First I renamed /hook/fieldContext.ts to /hook/filedProvider.tsx. I needed to add ts*x* to avoid error message at FieldContext.Provider.

I copied a suggestion as the following

import React, { createContext, useState, ReactNode } from 'react';

type FieldContextType = {
  field: string;
  setField: (value: string) => void;
};

export const FieldContext = createContext<FieldContextType | undefined>(undefined);

export const FieldProvider = ({ children }: { children: ReactNode }) => {
  const [field, setField] = useState("-1");

  return (<FieldContext.Provider value={{ field, setField }}>
    {children}
  </FieldContext.Provider>);

};
Enter fullscreen mode Exit fullscreen mode

Q1: I suppose, the above code is not a hook any more because of tsx. Should I move it to constants or components ?

Secondly: It has been asked to add <FieldProvider></FieldProvider> at the root of my application, where is the navigation. I am confuse.

Let me first introduce to my structure to better understand my problem

app
- (tab)
-- map
--- layout.tsx
--- [id].tsx
-- settings
--- layout.tsx
--- setting.tsx
--- info.tsx
-- stations.tsx
-- index.tsx
-- alarms.tsx
-- layout.tsx (add here)
- layout.tsx (or add here?)
Enter fullscreen mode Exit fullscreen mode

My navigation look like

app/layout.tsx

import { Stack } from "expo-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient

export default function Layout() {      
  return (
    <QueryClientProvider client={queryClient}>
      <Stack>
        <Stack.Screen 
          name="(tabs)" 
          options={{ 
            headerShown: false
          }} 
        />
        <Stack.Screen 
          name="+not-found"
          /*
          options={{
            headerShown: false,
          }} 
          */
        />
      </Stack>
    </QueryClientProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

and app/(tab)/layout.tsx

import FontAwesome from '@expo/vector-icons/FontAwesome6';
import { Tabs} from 'expo-router';
import { StyleSheet } from "react-native";
import { useThemeColors } from '@/hooks/useThemeColors';
import { useNavigation } from '@react-navigation/native';
import { useState } from "react";
import { FieldProvider } from '@/hooks/fieldProvider';


export default function TabLayout() {
  const colors= useThemeColors()
  const navigation = useNavigation();

  const [sortKey, setSortKey] = useState<"id_station" | "station_name">("id_station")

  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: 'green'}}>
      <Tabs.Screen
        name="index"
        options={{
          headerShown: false,
          title: 'Home',
          headerStyle: {backgroundColor: colors.appHeadBackgroundColor, borderColor:'red'},
          headerTintColor: '#000',
          headerTitleStyle: {
            fontWeight: 'normal',
          },
          tabBarIcon: ({ color }) => <FontAwesome size={28} name="arrows-to-circle" color={color} />,  
          tabBarStyle: {
            display:'none',
            backgroundColor: colors.appTabBarBackgroundColor,
            paddingTop:8,
            height:70,
          }
        }}
      />
      <Tabs.Screen
        name="map"
        options={{
          title: 'Map',
          headerShown: false,
          headerStyle: { backgroundColor: colors.appHeadBackgroundColor},
          headerTintColor: '#000',
          headerTitleStyle: {
            fontWeight: 'normal',
          },
          tabBarIcon: ({ color }) => <FontAwesome size={28} name="map-location-dot" color={color} />,
          //headerTitle: props => <LogoTitle />,
          tabBarStyle: {
            backgroundColor: colors.appTabBarBackgroundColor,
            //paddingTop:8,
            height:70,
          }
        }}
      />
      <Tabs.Screen
        name="station"
        options={{
          headerStyle: { 
            backgroundColor: colors.appHeadBackgroundColor
          },
          headerTintColor: '#000',
          headerTitleStyle: {
            fontWeight: 'normal',
          },
          headerShown: false,
          href: null,
          tabBarIcon: ({ color }) => <FontAwesome size={28} name="chart-line" color={color} />,
          tabBarStyle: {
            //backgroundColor: colors.appTabBarBackgroundColor,
            //paddingTop:8,
            height:70,
          }
        }}
      />  
      <Tabs.Screen
        name="alarms"
        options={{
          title: 'Alarmes',
          headerShown: false,
          headerStyle: { backgroundColor: colors.appHeadBackgroundColor},
          headerTintColor: '#000',
          headerTitleStyle: {
            fontWeight: 'normal',
          },
          /*
          headerRight: () => (
            <Button onPress={() => router.back()}>Back</Button>
          ),
          */
          tabBarIcon: ({ color }) => <FontAwesome size={28} name="bell" color={color} />,
          //headerTitle: props => <LogoTitle />,
          tabBarStyle: {
            //backgroundColor: colors.appTabBarBackgroundColor,
            //paddingTop:8,
            height:70,
            //borderWidth:30,
          }
        }}
      />
      <Tabs.Screen
        name="stations"
        options={{
          title: 'Stations',
          headerShown: false,
          headerStyle: { backgroundColor: colors.appHeadBackgroundColor},
          headerTintColor: '#000',
          headerTitleStyle: {
            fontWeight: 'normal',
          },
          tabBarIcon: ({ color }) => <FontAwesome size={28} name="map-pin" color={color} />,
          tabBarStyle: {
            //backgroundColor: colors.appTabBarBackgroundColor,
            height:70,
          }
        }}
      />
      <Tabs.Screen
        name="settings"
        options={{
          title: 'Paramètres',
          headerShown: false,
          headerStyle: { backgroundColor: colors.appHeadBackgroundColor },
          headerTintColor: '#000',
          headerTitleStyle: {
            fontWeight: 'normal',
          },
          /*
          headerRight: () => (
            <Button onPress={() => router.back()}>Back</Button>
          ),
          */
          tabBarIcon: ({ color }) => <FontAwesome size={28} name="gear" color={color} />,
          //headerTitle: props => <LogoTitle />,
          tabBarStyle: {
              backgroundColor: colors.appTabBarBackgroundColor,
              //paddingTop:8,
              height:70,
            }
          }}
        />
    </Tabs>
  );
}
Enter fullscreen mode Exit fullscreen mode

I was not really sure if <FieldProvider> should go to at app/layout.tsx or at app/(tab)/layout.tsx (I tried both)

Problem: How can I get the value in my view settings.tsx and map.tsx. I tried the following in my view alarms.tsx (it need it as well) as that view is very simple at the moment.

As <FieldProvider> is in app/layout.tsx, I tried the following, but the selected field is not printed, {filed} shows an error

import { Image, View } from "react-native";
import { useContext, useEffect, useState } from "react";
import { RootView } from "@/components/RootView";
import { ThemedText } from "@/components/ThemedText";
import { Row } from "@/components/Row";
import css from "@/hooks/styles";
import { useThemeColors } from "@/hooks/useThemeColors";
import AsyncStorage from '@react-native-async-storage/async-storage';
import { imagesHeader } from "@/components/imagesHeader";
import { FieldContext, FieldProvider } from "@/hooks/fieldProvider";

export default function Alarms() {

    const colors = useThemeColors()

    const field = useContext(FieldContext)

    return(
    <RootView>
      <View style={[{backgroundColor: colors.appHeadBackgroundColor }]}>
        <View style={css.jumbotronContainer}>
          <Image
            style={css.jumbotronImage}
            source={imagesHeader.alarms.uri}
          />       
        </View>
      </View>

      <View style={css.container}>
        <Row>
          <ThemedText>Terrain sélectionner: </ThemedText> 
          <ThemedText>{field}</ThemedText>
        </Row>
        <ThemedText style={[css.text,css.paragraphe]}>Aucune station en alarme</ThemedText>
      </View>
    </RootView>
    )
}
Enter fullscreen mode Exit fullscreen mode

As I am a novice, I sure I missed some steps, but which one? :)

Thanks for your help!

Top comments (1)

Collapse
 
pierrot10 profile image
Pierre

I found the solution and I am going to post my solution.
As I am a beginner, I am not sure if it's the best practice. At least there is a solution.

Résumé of the scenario
Form my setting.tsx, I can select a field to monitor some stations registered for the selected field. If I move to stations.tsx, map.tsx, alarms.tsx, the stations must be filtered with the selected field. If I return to settings.tsx and I select another field, the selected field must taken in consideration in map.tsx, stations.tsx and alarms.tsx. If I close the application, and I later open it, the selected field must be the same. (memory not lost)

Solution
I have created (thank to Khurshid) a hook (I still does know if it should be a hook, a constats or a components)

fieldProvider.tsx

import React, { createContext, useState, ReactNode } from 'react';
import { Text } from "react-native";

type FieldContextType = {
  idField: string;
  setIdField: (value: string) => void;
};

export const FieldContext = createContext<FieldContextType | undefined>(undefined);

export const FieldProvider = ({ children }: { children: ReactNode }) => {
  const [idField, setIdField] = useState("-1");

  return (<FieldContext.Provider value={{ idField, setIdField }}>
    {children}
  </FieldContext.Provider>);

};
Enter fullscreen mode Exit fullscreen mode

I wrapped my menu between <FieldProvider> in app/layout.tsx

import { Stack } from "expo-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { FieldProvider } from "@/hooks/fieldProvider";

const queryClient = new QueryClient

export default function Layout() {

  return (
    <FieldProvider>
      <QueryClientProvider client={queryClient}>
        <Stack>
          <Stack.Screen 
            name="(tabs)" 
            options={{ 
              headerShown: false
            }} 
          />
          <Stack.Screen 
            name="+not-found"
            /*
            options={{
              headerShown: false,
            }} 
            */
          />
        </Stack>
      </QueryClientProvider>
    </FieldProvider>
    );
}
Enter fullscreen mode Exit fullscreen mode

In my alarms.tsx, I added const field = useContext(FieldContext) and <ThemedText>{field?.idField}</ThemedText> to print the recorded field

import { Image, View } from "react-native";
import { useContext, useEffect, useState } from "react";
import { RootView } from "@/components/RootView";
import { ThemedText } from "@/components/ThemedText";
import { Row } from "@/components/Row";
import css from "@/hooks/styles";
import { useThemeColors } from "@/hooks/useThemeColors";
import AsyncStorage from '@react-native-async-storage/async-storage';
import { imagesHeader } from "@/components/imagesHeader";
import { FieldContext, FieldProvider } from "@/hooks/fieldProvider";

export default function Alarms() {

    const colors = useThemeColors()

    const field = useContext(FieldContext)

    return(
    <RootView>
      <FieldContext.Provider value={field}>
      <View style={[{backgroundColor: colors.appHeadBackgroundColor }]}>
        <View style={css.jumbotronContainer}>
          <Image
            style={css.jumbotronImage}
            source={imagesHeader.alarms.uri}
          />       
        </View>
      </View>

      <View style={css.container}>
        <Row>
          <ThemedText>Terrain sélectionner: </ThemedText> 

          <ThemedText>{field?.idField}</ThemedText>
        </Row>
        <ThemedText style={[css.text,css.paragraphe]}>Aucune station en alarme</ThemedText>
      </View>
      </FieldContext.Provider>
    </RootView>
    )
}
Enter fullscreen mode Exit fullscreen mode

At this point, it prints

-1

in setting.tsx I added const field = useContext(FieldContext) and field?.setIdField(value). Here is the full code

import { useEffect, useState, useContext } from "react";
import { ActivityIndicator, StyleSheet, Image, Text, View, Pressable, ScrollView } from "react-native";
import { RootView } from "@/components/RootView";
import { Card } from "@/components/Card";
import { ThemedText } from "@/components/ThemedText";
import { useFetchQuery, useInfiniteFetchQuery } from "@/hooks/useFetchQuery";
import { useThemeColors } from "@/hooks/useThemeColors";
import Dropdown from 'react-native-input-select';
import AsyncStorage from '@react-native-async-storage/async-storage';
import css from "@/hooks/styles";
import { imagesHeader } from "@/components/imagesHeader";
import { FieldContext, FieldProvider } from "@/hooks/fieldProvider";


type Fields = {
  index: number,
  value: string,
  label: string,
}

export default function Setting() {

  const {data, isFetching} = useFetchQuery("/getstation/0/[id]", {id: 0}) // Get all active fields
  const colors = useThemeColors()

  const field = useContext(FieldContext)

  // https://stackoverflow.com/questions/62380874/what-is-the-right-place-to-call-a-function-before-render-in-react
  useEffect(()=>{

  }, [])

  function onFieldChange(value:string) {    
    field?.setIdField(value)
  }


  const fields:Fields[] = []
  fields.push({
    index: -1,
    value: "-1",
    label: "Aucun",
    //field_city: f.field_city,
  })

  fields.push({
    index: 0,
    value: "0",
    label: "Tous",
    //field_city: f.field_city,
  })
  data?.fields.map((f, index) =>{
    fields.push({
      index: index,
      value: f.id_field.toString(),
      label: f.field_longname + "("+ f.id_field.toString() +")",
      //field_city: f.field_city,
    })
  })

    return (
      <RootView>
        <View style={[{backgroundColor: colors.appHeadBackgroundColor }]}>
          <View style={css.jumbotronContainer}>
            <Image
              style={css.jumbotronImage}
              source={imagesHeader.alarms.uri}
            />       
          </View>
        </View>

        {isFetching ? <ActivityIndicator style={css.activityIndicator} color={colors.greenDark} />:null}

        <View style={css.container}>
          <Card style={{flex:1, borderWidth:0}}>

              <ThemedText style={{marginVertical:10}} variant={"subtitle2"} >Selectionner un terrain à surveiller</ThemedText>
              <Dropdown
                label="Vous recevrez des alarmes uniquement pour le terrain sélectionné."
                placeholder="Select an option..."
                options = {fields}
                selectedValue={field?.idField}
                onValueChange={(value:any) => onFieldChange(value)}
                primaryColor={'green'}
                dropdownStyle={{
                  borderWidth: 1, // To remove border, set borderWidth to 0
                  borderColor: colors.grayLight,
                  alignItems: "center",
                }}
              />
              <ThemedText>Field (useContext): {field?.idField}</ThemedText>
          </Card>
        </View>
      </RootView>
    );
  }
Enter fullscreen mode Exit fullscreen mode

As you can see above, I used field?.idField to select the value of drop menu.
When you change the value of the drop menu, to select another field to be used while filtering the station, the function onFieldChange is called and I save the select field into the context

function onFieldChange(value:string) {    
  field?.setIdField(value)
}
Enter fullscreen mode Exit fullscreen mode

Now, if I navigate from settings.tsx, stations.tsx or alarms.tsx the field ID is the same as the latest selection of the drop menu.

It works fine for now.

Could you let me know

  1. Is it a good practive? Can we improve? is there mistake?
  2. Does the setIdField if saved, if I close/open the app
  3. Does files.tsx can be a hook, or shoild be a componants or a cosntants?

I hope, it helps. Enjoy!