React Native – How to use Context API?

Structure an app Context

Create Context and Provider

// ContextProvider.tsx

export const AppContext = createContext({});

export const AppProvider: React.FC = ({ children, initData }) => {
  const [user, setUser] = useState<IUser>({
    accessToken: initData.accessToken || undefined,
    idToken: initData.idToken || undefined,
    refreshToken: initData.refreshToken || undefined,
  });

  return (
    <AppContext.Provider
      value={{
        user,
        updateUser: (data: IUser) => {
          setUser(data);
        },
      }}
    >
      {children}
    </AppContext.Provider>
  );
};

export default AppProvider;
// useAppContext.ts (custom hook)

const useAppContext = () => {
    const appContext = useContext(AppContext);

    const user = appContext?.user;
    const updateUser = appContext?.updateUser;

    // TODO: Continue to add context elements here

    return { user, updateUser };
}

export default useAppContext;

Usage

parent component

const App = () => {
  return (
    <ContextProvider initData={initData}>
      <Routing />
    </ContextProvider>
  );
};

export default App;

child component

const LOGOUT_STATUS = {
    accessToken: '',
    idToken: '',
    refreshToken: '',
}

const Routing: React.FC = () => {
  const { user, updateUser } = useAppContext();

  useEffect(() => {
    const checkAuth = async () => {
      try {
        const authUser = await getCurrentAuthUser();
        if (authUser) {
          updateUser?.(authUser);
        } else {
          updateUser?.(LOGOUT_STATUS);
        }
      } catch (error) {
        updateUser?.(LOGOUT_STATUS);
      }
    };
    checkAuth();
  }, []);

  return (
    <NavigationContainer>
      {user?.accessToken === undefined ? (
        <StyledContainer>
          <StyledSpinner />
        </StyledContainer>
      ) : user?.accessToken ? (
        <MainStack />
      ) : (
        <LoginStack />
      )}
    </NavigationContainer>
  );
};

export default Routing;

How wrap just a few Screens with a context in React-navigation?

import React from 'react';
import {createStackNavigator} from '@react-navigation/stack';
import Screen1 from '../pages/Screen1';
import Screen2 from '../pages/Screen2';
import { MyContextProvider } from '../providers/myContext';

const StackB = createStackNavigator();

export default function StackB() {
  return (
    <MyContextProvider>
      <StackB.Navigator initialRouteName="Screen1">
        <StackB.Screen name="Screen1" component={Screen1} />
        <StackB.Screen name="Screen2" component={Screen2} />
      </StackB.Navigator>
    </MyContextProvider>
  );
}
import React from 'react';
import {createStackNavigator} from '@react-navigation/stack';
import Screen3 from '../pages/Screen3';
import Screen4 from '../pages/Screen4';
import StackB from './StackB';

const Stack = createStackNavigator();

export default function MainRoutes() {
  return (
    <Stack.Navigator initialRouteName="StackB">
      <Stack.Screen
        name="StackB"
        component={StackB}
        options={{
          headerShown: false,
        }}
      />
      <Stack.Screen name="Screen3" component={Screen3} />
      <Stack.Screen name="Screen4" component={Screen4} />
    </Stack.Navigator>
  );
}

Issues

Value is updated multiple times in Context Provider

const TabSettingsContent: React.FC<Props> = () => {
  const context = useContext(SettingsContext)

  ...

  // Fetch preference options
  let preferenceItems: Array<TSettingsItem> = []
  const { data, isLoading } = useQuery(
    ['fetchNotificationPreferences', email],
    () => fetchNotificationPreferences(email),
    {
      enabled: !!email,
    }
  )

  useEffect(() => {
    if (!data) {
      return
    }

    // Save necessary info into parent state
    context.setSettings(data)
  }, [context, data]) // !!!! ISSUE IS HERE: when new value is saved into context provider anywhere in the child component, it will trigger this useEffect to be run again. This makes old value "data" is saved into context again.

  ...

  return ...
}

export default TabSettings
const SettingsItem: React.FC<Props> = ({
  title,
  code,
  isEnabled,
  gridTemplateColumns,
}) => {
  ...

  // Parent state
  let input: TSettingsDataItem = undefined
  const context = useContext(SettingsContext)
  const settings = context?.settings

  // Server state
  const { refetch } = useQuery(
    ['updateNotificationPreferences', input],
    () => updateNotificationPreferences(input),
    {
      enabled: false,
    }
  )

  const onPressToggle = () => {
    ...
    
    // Update parent state
    input = getUpdateParam(settings, !isToggleOn, code)
    context.setSettings(input) // THIS CHANGES "CONTEXT" WHICH TRIGGER useEffect IN PARENT COMPONENT

    refetch()
  }

  return <>
    <ToggleSwitch onPress={onPressToggle} />
  </>
}

export default SettingsItem

Update value 5 times by loop “for”, UI is updated with first time value only

...
const [count, updateCount] = React.useState(0);
...
return (
    <AppContext.Provider
      value={{
        count: count,
        updateCount: () => {
          updateCount(count + 1);
        },
      }}
    >
      {children}
    </AppContext.Provider>
  );
...
const moduleState = React.useContext(AppContext);
...
for (let i = 1; i <= 5; i++) {
  moduleState.updateCount();
}

// Expect UI to be displayed with 5 but UI displays 1
...

This may be because “moduleState” is still old, so previous value is always the same, not to be incremented

Be the first to comment

Leave a Reply

Your email address will not be published.


*