Wartości ustawione w kontekście nie są przekazywane między widokami

0

Cześć,

mam taki problem, że zrobiłem sobie globalny provider w którym trzymam stan używany w kilku widokach.
Problem polega na tym, że po zapisaniu jakiejś wartości do stanu i nawigacji do innego screena, kiedy dostaje się do tego stanu to dalej mam w nim starą wartość. Tej nowej nie ma. Nie wiem czy coś robie źle, czy jak. Chciałem w jednym miejscu sobie coś pobierać z API, zapisać w tym globalnym stanie i później móc się do tego dostać z innego screena.

Chodzi mi o coś takiego

Tu jest mój provider

const AppContext = createContext<IAppContext>(initialAppContext);
const { Provider } = AppContext;

const AppProvider = ({ children }: any) => {
    const [appState, setAppState] = useState(initialAppContext.appState)

    return (
        <Provider value={{ appState, setAppState }}>{children}</Provider >
    )
};

export { AppContext, AppProvider }

Tutaj mam mój komponent root a w nim tego providera używam

return (
  <AppProvider>
      <AppContainer>
        <RootStackScreen userToken={token} />
      </AppContainer>
  </AppProvider>
)

No i tutaj sobie coś w tym stanie ustawiam w jakimś screenie

export const MyScreen = ({ route, navigation }: NativeStackScreenProps<any>) => {
  const appContext = useContext(AppContext);

  appContext.setAppState({
      ...appContext.appState,
      showIntro: aclimatizationStatus.data.showIntro
  })
}

No i teraz jak gdzies w innym screenie chce to odczytać. Czyli muszę się tam nawigować to nie ma tej ustawionej wartości tylko jest stara..

export const InnyScreen = ({ route, navigation }: NativeStackScreenProps<any>) => {

const appContext = useContext(AppContext);

let showIntro = appContext.appState.showIntro; // <--- tutaj jest stara wartosc - nie pokazuje tej co ustawiłem :(

}
0

Mówiąc "stara wartość" masz namyśli tą początkową, którą przypisałeś na samym starcie?

Jeśli tak to musisz "użyć contextu przed routingiem"

const App = () => {
  return (
    <Context> {/* Globalny context */}
      <Router> {/* Routing */}
        {/* 
          Tutaj jakieś komponenty
          korzystające z contextu i routingu
        */}
      </Router>
    </Context>
  );
}

export default App;

Jeśli użyjesz tego odwrotnie, czyli "najpierw routing, a później context" to React przy każdej zmianie ścieżki będzie renderować ten komponent ponownie i wszystkie wartości będą się resetować.


EDIT:

Chociaż teraz jak patrzę to drugą przyczyną (nie jestem tego pewny na 100%) może być użycie destrukturyzacji w tym miejscu

const { Provider } = AppContext;

<Provider ...>...</Provider>

zamiast pełnego zapisu

<AppContext.Provider value={{ hello: "world" }}>
  {children}
</AppContext.Provider>

EDIT 2:

Ewentualnie można spróbować użyć hooka useMemo do naprawy problemu

const AppContext = ({ children }) => {
  const [appState, setAppState] = useContext(...);

  const value = useMemo(() => ({
    appState,
    setAppState,
  }), [appState]);

  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  )
};
0

@Xarviel: dzięki za odpowiedź. Spróbowałem z tym destruktorem ale nie pomogło.

Działa mi to tak że, dopiero jak zrobie reload aplikacji w konsoli (czyli załaduje mi się od nowa wszystko), to wtedy widzę te wartości które zapisałem do stanu.

Mój <AppProvider> jest najbardziej "na zewnątrz".
RootStackScreen wygląda tak mniej więcej

const RootStack = createStackNavigator<RootStackParamList>()

export default function RootStackScreen({ userToken = null }: any) {


(...)

    return (
        <RootStack.Navigator>
            {(() => {
                switch (screenName) {
                    case 'APP':
                        return <RootStack.Screen name="App" component={DrawerScreen} options={{ headerShown: false, animationEnabled: false }} />
                    case 'LOGIN':
                        return <RootStack.Screen name='Auth' component={AuthStackScreen} options={{ animationEnabled: false, headerShown: false }} />
                    default:
                        null;
                }
            })()}
        </RootStack.Navigator>
    )
}
0

Coś u mnie jest nie tak z tym providerem globalnym. Próbowałem na różne sposoby ale to nie działa. Dodałem sobie nowy provider testowy

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

export interface IUSerContext {
    userName: string;
    setUserName: (appState: string) => void;
}

const UserContext = createContext<IUSerContext>({ userName: '', setUserName: () => { } });

const UserProvider = ({ children }: any) => {

    const [userName, setUserName] = useState('John Smith');

    const myValue = useMemo(() => ({ userName, setUserName }),
        [userName]
    );

    return (
        <UserContext.Provider value={myValue}>{children}</UserContext.Provider>
    );
}

export { UserContext, UserProvider }

Również nie jestem w stanie wyświetlić na innych ekranach, które mają dostęp do tego providera wartości której w nim ustawiam.

Wydaje mi się że może ten routing który mam jest powodem tych problemów. Czyli to:

const RootStack = createStackNavigator<RootStackParamList>()

export default function RootStackScreen({ userToken = null }: any) {

      (...)

    return (
        <RootStack.Navigator>
            {(() => {
                switch (screenName) {
                    case 'APP':
                        return <RootStack.Screen name="App" component={DrawerScreen} options={{ headerShown: false, animationEnabled: false }} />
                    case 'LOGIN':
                        return <RootStack.Screen name='Auth' component={AuthStackScreen} options={{ animationEnabled: false, headerShown: false }} />
                    default:
                        null;
                }
            })()}
        </RootStack.Navigator>
    )
}
export default function App() {

  (...)

  return (


          <UserProvider>
            <AppContainer>
              <RootStackScreen userToken={token} />
            </AppContainer>
          </UserProvider>

  )
}

Tutaj przykład screena w którym to testuje


export const MainScreen = ({ route, navigation }: NativeStackScreenProps<TasksStackParamList>) => {

    const userContext = useContext(UserContext);

//1. Ustawiam sobie wartość w globalnym stanie przy nawigowaniu do ekranu
    useFocusEffect(
        React.useCallback(() => {
            userContext.setUserName('nowe imie');
        }, [])
    );

//2. A tutaj jakas funkacja ktora wykonywana jest po nacisnieciu przyciska

const myFunction = useCallback((item: any) => {

        console.error('IMIE PO: ' + userContext.userName); // <- nie pokazuje tego co ustawilem na początku
}

Może macie jakiś pomysł co mam zrąbane :(

0

Wyrenderuj w swoim MyScreen aktualny stan, i zobacz czy się zmienił:

export const MyScreen = ({ route, navigation }: NativeStackScreenProps<any>) => {
  const appContext = useContext(AppContext);

  function update() {
    appContext.setAppState({
        ...appContext.appState,
        showIntro: aclimatizationStatus.data.showIntro
    });
  }

  // odpal update() i zobacz czy wartość się zmienia bezpośrednio na tym samym screenie

  return <>
    {JSON.stringify(appContext.appState)}
  </>;
}
0

Czy zmieniając stan w kontekście powodujesz w ogóle rerendering komponentu głębiej? Ja bym wrzucił jakiś alert i zobaczył, czy dane komponenty się rerenderują.

0
LukeJL napisał(a):

Czy zmieniając stan w kontekście powodujesz w ogóle rerendering komponentu głębiej? Ja bym wrzucił jakiś alert i zobaczył, czy dane komponenty się rerenderują.

Rerenderuje się sam HTML - czyli jak bezpośrednio w htmlu mam odczyt wartości z kontekstu <View>title={userContext.userName}</View> to nowa wartość będzie widoczna na ekranie. Ale w przypadku jak w komponencie chce wywołać jakąś funkcję która dostaje się do tego stanu to już w tej funkcji wartość stanu będzie niepoprawna / stara

0
Riddle napisał(a):

Wyrenderuj w swoim MyScreen aktualny stan, i zobacz czy się zmienił:

Tak, nowa wartość będzie widoczna na ekranie. Ale w przypadku jak w komponencie chce wywołać jakąś funkcję która dostaje się do tego stanu to już w tej funkcji wartość stanu będzie niepoprawna / stara

0
RideorDie napisał(a):

Tak, nowa wartość będzie widoczna na ekranie. Ale w przypadku jak w komponencie chce wywołać jakąś funkcję która dostaje się do tego stanu to już w tej funkcji wartość stanu będzie niepoprawna / stara

Ale mówisz tak bo Ci się wydaje, czy uruchomiłeś aplikację i faktycznie sprawdziłeś że tak się dzieje?

To ma na celu sprawdzić, czy faktycznie Twoja zmiana updateuje stan w contexcie - bo może się okazać że wcale nie.

0

Przeczytałem, że użycie useEffect powinno pomóc


  let ekranBuserName = null;

 useEffect(() => {

   ekranBuserName = userContext.username; // w tym miejscu mam poprawną wartość

    }, [userContext.userName]);

  jakasFunkcja() {

    console.log(ekranBuserName) // wartość z kosmosu
  
  }

no i sytuacja wygląda tak. Mam dwa ekrany A i B. Na ekranie A pobieram jakąś wartość i zapisuje ją w stanie userContext.setUserName('costam');
W tym momencie jak to robie to wykonuje się ten useEffect co wstawiłem wyżej. Tyle że znajduje się on w ekranie B ( bo tam potrzebuje się do tej wartości dostać).
No i mogłoby się wydawać, że wszystko jest git, ale z jakiś niewiadomych mi przyczyn, kiedy nawiguje się do ekranu B i wyowłuje funkcję która korzysta z tej zmiennej ekranBuserName to jakimś cudem ma ona poprzednią wartość ( nie widzę żadnego loga kóry by do niej tą wartość przypisał..)

0

Zaraz, w jaki dokładny sposób masz tam funkcje zadeklarowane w tym komponencie? Bo mam wrażenie, że próbujesz na siłę robić z Reacta "normalny" JavaScript, zapominając o tym, w jaki sposób on działa i że zmiennych w komponentach się nie powinno używać normalnie, a jedynie pod kontrolą Reacta.

Zmienne w komponentach funkcyjnych są resetowane za każdym renderem.

0
LukeJL napisał(a):

Zaraz, w jaki dokładny sposób masz tam funkcje zadeklarowane w tym komponencie? Bo mam wrażenie, że próbujesz na siłę robić z Reacta "normalny" JavaScript, zapominając o tym, w jaki sposób on działa i że zmiennych w komponentach się nie powinno używać normalnie, a jedynie pod kontrolą Reacta.

Zmienne w komponentach funkcyjnych są resetowane za każdym renderem.

No ja ze świata Angulara jestem więc istnieje spore prawdopodobieństwo, że wszystko robię źle.

Tak to przykładowo wygląda


export const MainScreen = ({ route, navigation }: NativeStackScreenProps<TasksStackParamList>) => {

    const [data, setData] = useState<Dto>({ items: [] });

    useFocusEffect(
        React.useCallback(() => {
            getItems();
        }, [])
    );

    const getItems = () => {
        authAxios.get('/items').then(response => {
            let responseData = response.data as Dto
            setData(responseData)
        });

    }

    const handleRemoveItem = useCallback(async (item: ItemData) => {

        let resposne = await authAxios.delete('/item/delete', { data: item });

        if (resposne.status == 204) {
        
            setData(prevData => {
                const newData = prevData.items.filter(i => i !== item)
                const result = { ...prevData, items: SortItems(newData) };
                return result
            })
        }
    }, [])

}

0

Nie rozumiem tego kompletnie jak to działa. Wszystkie przykłady które są w internecie z tym co chce zrobić są tylko i wyłącznie z użyciem tego stanu w HTML czy jak to sie tam nazywa w react native - i to również mi działa. Jednak ja potrzebuje czegoś innego, a skoro to nie działa to zacząłem szukać czegoś podobnego do Reduxa, bo nie chce tego potwora ustawiać. Znalazłem bibliotekę o nazwie recoil do zarządzania stanem. No i działa dokładnie tak samo beznadziejnie jak ten provider mój..

Nie rozumiem, przecież to jest normalny case. Włączam aplikacje, ciągne jakieś dane z API i chce sobie je trzymać w apliakcji żeby mieć do niech dostęp, bo używam ich w innych miejscach w aplikacji do wysylania zapytan do API. Więc potrzebuje w funkcji w komponencie dostać się do wartości ze stanu i przesłać ją w zapytaniu do API.. i nie mogę :(

Jak się takie rzeczy ogarnia w react native?

1
RideorDie napisał(a):

Nie rozumiem tego kompletnie jak to działa. Wszystkie przykłady które są w internecie z tym co chce zrobić są tylko i wyłącznie z użyciem tego stanu w HTML czy jak to sie tam nazywa w react native - i to również mi działa. Jednak ja potrzebuje czegoś innego, a skoro to nie działa to zacząłem szukać czegoś podobnego do Reduxa, bo nie chce tego potwora ustawiać. Znalazłem bibliotekę o nazwie recoil do zarządzania stanem. No i działa dokładnie tak samo beznadziejnie jak ten provider mój..

Nie rozumiem, przecież to jest normalny case. Włączam aplikacje, ciągne jakieś dane z API i chce sobie je trzymać w apliakcji żeby mieć do niech dostęp, bo używam ich w innych miejscach w aplikacji do wysylania zapytan do API. Więc potrzebuje w funkcji w komponencie dostać się do wartości ze stanu i przesłać ją w zapytaniu do API.. i nie mogę :(

Pokaż cały kod jaki masz, bo wrzucasz jakieś skrawki.

RideorDie napisał(a):

Jak się takie rzeczy ogarnia w react native?

Tak samo

0



export const MainScreen = ({ route, navigation }: NativeStackScreenProps<TasksStackParamList>) => {

	const userContext = useContext(UserContext);
    const { authAxios } = useContext(AxiosContext);
    const [data, setData] = useState<Dto>({ items: [] });

	// DODAŁEM DLA TESTU - JEŻELI W INNYM EKRANIE USTAWIE USERNAME 
    // TO TO W TYM SAMYM MOMENCIE SIĘ TO WYWOŁA I ZALOGUJE POPRAWNĄ WARTOŚĆ
    useEffect(() => {
        console.log(userContext.userName);
    }, [userContext.userName]); // <- DZIEKI TEMU

	// ZA KAŻDYM RAZEM PO NAWIGOWANIU DO TEGO SCREENA ZACIĄGAM Z BAZY ITEMS
    useFocusEffect(
        React.useCallback(() => {
            getItems();
        }, [])
    );

    const getItems = () => {
        authAxios.get('/items').then(response => {
            let responseData = response.data as Dto
            setData(responseData)
        });

    }

	// TUTAJ ODBYWA SIĘ TEST. JEST TO METODA WYWOŁYWANA Z UI PRZEZ NACIŚNIĘCIE PRZYCISKU
	const handleToggleTaskItem = useCallback((item: any) => {

        console.log(userContext.userName); // WYPISUJE OBECNA WARTOSC ZE STANU

	// USTAWIAM NOWA WARTOSC DO STANU.
	// NIESTETY KIEDY NACISNE TEN PRZYCISK JESZCZE RAZ, TO NIE BEDZIE TAM TEJ USTAWIONEJ WARTOSCI
    // A WARTOSC KTORA BYLA POWYZEJ.
    
        userContext.setUserName(Date().toString()); // <- CZYLI TO W INNYM EKRANIE USTAWIAM, ALE TU TEZ NIE DZIALA.
        // NIE MA ZNACZENIA

        let command = {
            userName: userContext.userName // <- TUTAJ POTRZEBUJE UZYC WARTOSCI ZE STANU KTORA
            // ZOSTALA USTAWIONA W INNYM EKRANIE. 
            // TAM NIE DZIALA TAK SAMO JAK NIE DZIALA JAK W TYM EKRANIE USTAWIAM
        };

        authAxios.post('/item/toggle', command).then(response => {

            setData(prevData => {
                const result = { ...prevData };
                return result
            })
        }).catch((error: any) => {
            console.log(JSON.stringify(error));
        });
    }, [])

    return (
	        <Masthead title={userContext.userName}> // TO DZIALA ZAJEBISCIE SIE ODSWIEZA TAK JAK POWINNO W PRZECIWIENSTWIE DO KODU POWYZEJ.
                <NavBar />
            </Masthead>
    )
}



2

Problem nie leży w tym, że wartości jest w tym, że masz nieświeżą wartość appState, tylko że masz nieświeży obiekt kontekstu.

Jeśli renderujesz to tak:

const AppProvider = ({ children }: any) => {
    const [appState, setAppState] = useState(initialAppContext.appState)

    return (
        <Provider value={{ appState, setAppState }}>{children}</Provider >
    )
};

To jeśli zrobisz setAppState, komponent AppProvider się przerenderuje i stworzy się nowy obiekt kontekstu (w sensie mam na myśli wartość tego kontekstu, to co jest w value i co odbierasz przez useContext:

<Provider value={{ appState, setAppState }}>{children}</Provider >

(zwróć uwagę na { appState, setAppState } ---> czyli tworzy się nowy obiekt)

Z kolei w komponencie MainScreen odbierasz świeży obiekt kontekstu:

const userContext = useContext(UserContext);

dlatego jest możliwe wyrenderowanie go i na poziomie HTML działa ci to dobrze.

Jednakże podajesz również ten obiekt kontekstu do wewnętrznej funkcji w domknięciu:

const handleToggleTaskItem = useCallback((item: any) => {

console.log(userContext.userName); // WYPISUJE OBECNA WARTOSC ZE STANU

I to jest problematyczne, ponieważ jeśli ta funkcja* zostanie odpalona po tym, jak kontekst się zmienił, to będzie miała już nieaktualne wartości. I teraz, gdybyś zrobił to tak:

const handleToggleTaskItem = () => {
};

to przy każdym renderze tworzona byłaby nowa funkcja(nowy obiekt funkcji) handleToggleTaskItem z aktualnym kontekstem w środku.
Natomiast useCallback cache'uje funkcję: is a React Hook that lets you cache a function definition between re-renders.
Czyli to normalne, że masz nieświeżą funkcję.

Jednak useCallback przyjmuje również tablicę zależności: https://beta.reactjs.org/reference/react/useCallback
więc przypuszczalnie mógłbyś coś takiego zrobić, żeby podać [appState] jako zależność

const handleToggleTaskItem = useCallback((item: any) => {...}, [appState]);

albo też wywalić useCallback.

*(przez funkcję nie mam na myśli funkcji jako "kod w pliku", tylko konkretny obiekt funkcji w trakcie działania programu, domknięcie z konkretnymi wartościami używanych zmiennych z zewnętrznej funkcji)

1 użytkowników online, w tym zalogowanych: 0, gości: 1