Home > front end >  React Native (Expo) - Ability to update state of top component from any (nested) child component
React Native (Expo) - Ability to update state of top component from any (nested) child component

Time:05-03

I am working on an Expo app, that uses authentication with JWTs that are stored using SecureStore. The top-level component initially displays a Login screen but also checks if a JWT exists in SecureStore. If a JWT exists the app verifies that it is still valid and if it is the app takes the user to the Landing page, from which the user can navigate to many other pages that fetch and display all sorts of data.

I am looking for a way to handle an expired JWT, so that if the user is navigating to a page that tries to fetch some data and the API response returns e.g. 401 the app should take the user back to the login screen.

The top component uses this state to decide what page to show

  const [appState, setAppState] = useState(appStates.STARTUP_SETUP);

with valid values for appState being:

  const appStates = {
    STARTUP_SETUP: "StartupSetup", // Initial state, during which the app checks for an existing valid JWT
    SHOW_LOGIN_SCREEN: "ShowLoginScreen", // There is no stored JWT, app shows login screen
    SHOW_SIGNUP_SCREEN: "ShowSignupScreen", // There is no stored JWT, app shows signup screen
    SHOW_SIGNUP_CONFIRMATION_SCREEN: "ShowSignupConfirmationScreen", // There is no stored JWT, user just registered and is prompted to check their email for verification
    USER_LOGGED_IN: "UserLoggedIn", // user logged in, JWT is stored
  }

The component uses appState in the following way:

  if (appState === appStates.USER_LOGGED_IN) {
    comp = <Landing onLogout={logUserOut} />;
  } else if (appState === appStates.SHOW_LOGIN_SCREEN) {
    comp = <Login onSuccessfulLogin={updateUser} onSignup={() => setAppState(appStates.SHOW_SIGNUP_SCREEN)} />;
  } else if (appState === appStates.SHOW_SIGNUP_SCREEN) {
    comp = <Signup onSuccessfulSignup={() => setAppState(appStates.SHOW_SIGNUP_CONFIRMATION_SCREEN)} onLogin={() => setAppState(appStates.SHOW_LOGIN_SCREEN)} />;
  } else if (appState === appStates.SHOW_SIGNUP_CONFIRMATION_SCREEN) {
    comp = <SignupConfirmation onLogin={() => setAppState(appStates.SHOW_LOGIN_SCREEN)} />
  }

With Landing having its own tree of child components.

I am basically looking for a way to be able to do

setAppState(appStates.SHOW_LOGIN_SCREEN)

from anywhere in my app.

One possibility would be to pass that hook from the top component to Landing and every child that has but I feel there should be an easier way.

Edit - Solution

In my top component I created a method that deletes the token and sets the appState to SHOW_LOGIN_SCREEN

  const deleteStoredToken = () => {
    deleteToken();
    setAppState(appStates.SHOW_LOGIN_SCREEN);
  }
  const appStateValue = { deleteStoredToken };

I then created a context (with a default value)

export const AppContext = React.createContext({
    deleteStoredToken: () => { }
});

I used this context to wrap the children of the top component by providing appStateValue as its value

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

And now in any child component I can do

const { deleteStoredToken } = useContext(AppContext);

and use deleteStoredToken()

CodePudding user response:

It sounds like you're looking for a state management solution. A React Context is a low-effort solution to this - be sure to read the caveats in the documentation though.

There are tens if not hundreds of libraries that offer different ways to do this. The most popular are probably Redux and Mobx; these offer dedicated ways to share complex state between components. However, if it's only for one value, and it won't be updated frequently, a Context is perfectly fine.

  • Related