Passing state into child components with Router

Apologies in advance if this is a newbie question, but I’m struggling with passing a state variable down to a child component if I’m using a router.

I’m using ionic 7 and capacitor android 5.1.1

  "dependencies": {
    "@capacitor/android": "5.1.1",
    "@capacitor/app": "5.0.5",
    "@capacitor/core": "5.1.1",
    "@capacitor/haptics": "5.0.5",
    "@capacitor/keyboard": "5.0.5",
    "@capacitor/status-bar": "5.0.5",
    "@ionic/react": "^7.0.0",
    "@ionic/react-router": "^7.0.0",
    "@transistorsoft/capacitor-background-fetch": "^5.0.0",
    "@transistorsoft/capacitor-background-geolocation": "^5.0.0",
    "@types/react-router": "^5.1.20",
    "@types/react-router-dom": "^5.3.3",
    "axios": "^1.4.0",
    "ionicons": "^7.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router": "^5.3.4",
    "react-router-dom": "^5.3.4"
  }

Problem: I am building an app which contains multiple pages but I need them to all share a global state and I want updates whenever that state changes. My simplified solution has a boolean state variable in App, and a child component which uses and can update this state variable (via a callback)

App:

setupIonicReact();

const App: React.FC = () => {
  console.log('App: Rendering App');

  const [booleanValue, setBooleanValue] = useState(false);
  console.log('App:BooleanValue', booleanValue);

  const handleToggle = () => {
    console.log('App: handleToggle');
    setBooleanValue((prevValue) => !prevValue);
  };

  return (
    <IonApp>
            <ChildComponent booleanValue={booleanValue} onToggle={handleToggle} />
    </IonApp>
  )
};

export default App;

Child Component:

interface ChildComponentProps {
  booleanValue: boolean;
  onToggle: () => void;
}

const ChildComponent: React.FC<ChildComponentProps> = ({ booleanValue, onToggle }) => {
  console.log('ChildComponent: Rendering ChildComponent with booleanValue: ', booleanValue);
  return (
    <IonPage>
    <div>
      <p>Boolean Value: {booleanValue.toString()}</p>
      <button onClick={onToggle}>Toggle</button>
    </div>
    </IonPage>
  );
};

export default ChildComponent;

This works fine and my output when clicking the button is as desired:
3

Now, as I want multiple pages which can update this state I build in a router:

const App: React.FC = () => {
  console.log('App: Rendering App');

  const [booleanValue, setBooleanValue] = useState(false);
  console.log('App:BooleanValue', booleanValue);

  const handleToggle = () => {
    console.log('App: handleToggle');
    setBooleanValue((prevValue) => !prevValue);
  };

  return (
    <IonApp>
      <IonReactRouter>
        <IonRouterOutlet>
          <Route path="/OperativePage">
            <ChildComponent booleanValue={booleanValue} onToggle={handleToggle} />
          </Route>
          <Redirect to="/OperativePage" />
        </IonRouterOutlet>
      </IonReactRouter>
    </IonApp>
  )
};

export default App;

Now my state is not getting updated in the child page (the child page is not getting re-rendered):

My third test was to use the render method, which does cause the re-render of the child page, but does not change the state in the child:

setupIonicReact();

const App: React.FC = () => {
  console.log('App: Rendering App');

  const [booleanValue, setBooleanValue] = useState(false);
  console.log('App:BooleanValue', booleanValue);

  const handleToggle = () => {
    console.log('App: handleToggle');
    setBooleanValue((prevValue) => !prevValue);
  };

  return (
    <IonApp>
      <IonReactRouter>
        <IonRouterOutlet>
          <Route path="/OperativePage"
            render={(routeProps) => (
              <ChildComponent
                booleanValue={booleanValue}
                onToggle={handleToggle}
                {...routeProps}
              />
            )}
          />
          <Redirect to="/OperativePage" />
        </IonRouterOutlet>
      </IonReactRouter>
    </IonApp>
  )
};

export default App;

1

Can anyone explain what I’m doing wrong here and the reason for the different behaviours when attempting to use the router?

Thanks

Not sure what you are expecting to see… I recreated your project and the toggle is working fine

Here is the project

Thanks for taking a look aaronksaunders,

It’s given me a bit more information as to the nature of the issue but still a bit confusing:

When I load the StackBlitz example, it builds and I see the same behaviour as I am getting. Clicking the toggle button is not updating the value:

image

However after clicking the reload icon:
image

The app reloads and suddenly all is working correctly again:
image

After a retest, this is the same behaviour as my sample app locally - after a F5 reload on the browser page it starts working. When i do a
ionic cap sync
ionic cap run android - in my emulator the app never works (maybe the same issue on first load but as I’m now in an app in the webview I can’t easily do a F5 refresh)

i dont know what you are doing but the code i provided you works fine. I would suggest you restart your process using the code i provided you.

The output u are showing in the first image where you say it is not working is not from my application it is from your original code.

This is a video of the code running fine.
localhost:3000 - 16 July 2023 (loom.com)

Your issue with running on device is a separate issue and I suggest you open a new issue with that.

My final advice is to you the VS code plugin when setting up your projects. Good Luck

Hi aaron

The problem is occurring on the initial load/serve of the app (both my app and your stackblitz example).
Video of the problem: Loom | Free Screen & Video Recording Software | Loom

Appreciate the help so far.

Think I’ve narrowed a potential cause of the issue. Seems to be something around the redirect on the initial load (redirecting back to the OperativePage).

If I replace:

  return (
    <IonApp>
      <IonReactRouter>
        <IonRouterOutlet>
          <Route path="/OperativePage">
            <ChildComponent booleanValue={booleanValue} onToggle={handleToggle} />
          </Route>
          <Redirect to="/OperativePage" />
        </IonRouterOutlet>
      </IonReactRouter>
    </IonApp>
  )
};

with this:

  return (
    <IonApp>
      <IonReactRouter>
        <IonRouterOutlet>
          <Route path="/OperativePage">
            <ChildComponent
              booleanValue={booleanValue}
              onToggle={handleToggle}
            />
          </Route>
          <Route path="/">
            <ChildComponent
              booleanValue={booleanValue}
              onToggle={handleToggle}
            />
          </Route>
        </IonRouterOutlet>
      </IonReactRouter>
    </IonApp>
  );

The behaviour is working on the initial load without a refresh.

1 Like

Although you got this to work, I would not recommend the approach. There are better was to manage state in a react application than passing it around as properties

Thanks for the help aaron,

Although the last change worked, I’m still not happy with it as it seems more of a workaround than anything else, and I’m still none the wiser as to why the redirect has different behaviour in the child component on initial load compared to direct navigation (unless there is a good reason for it that I don’t know, it seems inconsistent).

In terms of the approach, do you not think this is advisable in general?

From what I was reading I thought this was a standard way of implementing using the ‘Controlled Components’ pattern (Difference Between Controlled and Uncontrolled Components in React — A Guide) having the child state managed by the parent?

I don’t think that is the correct scalable approach. here are some alternate ways to manage state in your react application

2 Likes

These are all great recommendations. I would just add that I think Context is the simplest to get started with because it’s built into modern React.

1 Like