4

In React I can execute code every time a location change using use-react-router which gives me access to the history, location, and match properties. What I do is to use a useEffect and run code every time location property change, something like:

import useRouter from "use-react-router";

const { location } = useRouter();

React.useEffect(() => {
  // run some code every time `location` change.
}, [location]);

There is a way to achieve this same behaviour with react-navigation in react-native?

Update 1 - Temporary solution using useNavigationState hook

I'm tryng to check if navigation state change and run code when that happend, to achieve this I've used useNavigationState from react-navigation:

import {useNavigationState} from '@react-navigation/native';

const navigationState = useNavigationState(state => state);

React.useEffect(() => {
  // run some code every time `location` change.
}, [navigationState]);

There is a problem with this approach, if you try to use this outside Navigation root component, an error will be thrown: couldn't find a navigation object.Is your component inside a screen in a navigator? This is obvious because you are trying to access the react-navigation state out of its context.

What if I'm creating a component that encloses all the other components of my application including navigation component? something like:

AppStateProvider

type AppStateProviderProps = {
  children: React.ReactNode;
};

// some context that u want to pass to componet tree
export const AppStateContext = createContext();

function AppStateProvider({children}: AppStateProviderProps) {
  const navigationState = useNavigationState(state => state);

  React.useEffect(() => {
     // run code every time navigation state change
  }, [navigationState]);

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

App.tsx - my root component

function App(): JSX.Element{
  return (    
    {/* this will throw the error mentioned above */}
    <AppStateProvider> 
      <NavigationContainer>
        {/* ...my navigators and screens */} 
      </NavigationContainer>
    </AppStateProvider>
  );
}

Is there a more generic solution outside of navigation something that I can use in any situation withouth depend of react-navigation.

Update 2

I want to achieve is to track the global aplication state, i.e if there is any error or is loading any resouce, display a loading screen or an error screen respectively. To achieve this, I'm trying to track when the user change of screen and check if there is any error or if any resource is been loading, to show the current screen or loading/error screen.

if there are no errors or if a resource is not being loaded when the screen changes, the application state will be set to undefined and the screen to which the user is navigating will be displayed.

What I do in react

// Root component App.tsx
import { BrowserRouter } from "react-router-dom";

const App: React.FC = () => {
  return (
    <BrowserRouter basename={APP_MOUNT_URI}>
      {* This pass the current app state to all components, and its updated every time location change *}
      <AppStateProvider>
      {* other componets *} 
      </AppStateProvider>
    </BrowserRouter>
  );
};
// Aplication state with context AppStateProvider.tsx
import React from "react";
import useRouter from "use-react-router";

type AppStateProviderProps = {
  children: React.ReactNode;
};

function reduceAppState(
  prevState,
  action,
) {
  switch (action.type) {
    case 'displayError':
      return displayError(
        prevState,
        action.payload.error,
        action.payload.errorId,
      );
    case 'displayLoader':
      return displayLoader(prevState, action.payload.value);
    default:
      return prevState;
  }
}

const initialAppState: AppStateType = {
  error: null,
  loading: false,
};

// some context that u want to pass to componet tree
export const AppStateContext = createContext();

function AppStateProvider({children}: AppStateProviderProps) {
  const {location} = useRouter();
  const stateAndDispatch = React.useReducer(appStateReducer, initialAppState);
  const [state, dispatch] = stateAndDispatch;

  React.useEffect(() => {
     // if there is no error in the application, when you change the 
     // route, the status will be undefined and the current screen will 
     // be displayed.
     if (state.error) {
      dispatch({
        payload: {
          error: undefined,
        },
        type: 'displayError',
      });
    }
  }, [location]);

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

Cristian Flórez
  • 2,277
  • 5
  • 29
  • 51
  • Ok, now that I better understand what you're going for, I don't think our thread was helping you on your path. It feels to me like you're aiming more for an application error screen that overrides or interrupts normal app flow with important info. For my own App, I have a solution centered around using react native's Modal widget. I place that alongside my main/root components and it pops up loading messages, for example. You could go this way or a hundred other ways. I don't know that there is one perfect solution I can communicate, so I'll leave you with that. – Atmas May 18 '21 at 05:24

1 Answers1

1

For prior to your update 2:

One good approach might be to use an event listener on a navigation ref that is attached to your main container. See docs here: https://reactnavigation.org/docs/navigation-events/

The navigationRef can be exported and accessed most anywhere. If you wanted to check navigation state from a parent of the navigation container, it could look something like this:

import { useEffect } from 'react'
import {
    createNavigationContainerRef,
    EventListenerCallback,
    NavigationContainerEventMap,
    NavigationContainer,
} from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'

type StackParamList = {
    Screen1: undefined
    Screen2: undefined
}
const Screen1 = () => <></>
const Screen2 = () => <></>

const navigationRef = createNavigationContainerRef<StackParamList>()
const Stack = createNativeStackNavigator<StackParamList>()

const Navigation = () => (
    <NavigationContainer ref={navigationRef}>
        <Stack.Navigator>
            <Stack.Screen name="Screen1" component={Screen1} />
            <Stack.Screen name="Screen2" component={Screen2} />
        </Stack.Navigator>
    </NavigationContainer>
)

const App = () => {
    useEffect(() => {
        const handleStateChange: EventListenerCallback<
            NavigationContainerEventMap,
            'state'
        > = (event) => {
            const state = event.data.state

            // do stuff with state
            console.log(state)
        }

        // Add listener on mount
        navigationRef.addListener('state', handleStateChange)

        // Remove listener on unmount
        return () => navigationRef.removeListener('state', handleStateChange)
    }, [navigationRef])

    return <Navigation />
}

export default App
Dallin Romney
  • 2,276
  • 1
  • 6
  • 5