Best practice for React routing

I have routing setup like this…

        <IonRouterOutlet id="main">
          <Route path="/home" component={Home} exact />
          <Route path="/birthday/new" component={AddBirthday} exact />
          <Route path="/birthday/edit/:id" component={EditBirthday} exact />
          <Route path="/birthday/:id" component={ViewBirthday} exact />
          <Route path="/account/create" component={CreateAccount} exact />
          <Route path="/login" component={Login} exact />
          <Route path="/about" component={About} exact />
          <Route path="/contact" component={Contact} exact />
          <Redirect exact from="/" to="/home" />
        </IonRouterOutlet>

When a page is in an IonRouterOutlet , the container controls the transition animation between the pages as well as controls when a page is created and destroyed, which helps maintain the state between the views when switching back and forth between them. – source

So if I’m editing a birthday (EditBirthday), and then go to the page for viewing a birthday (ViewBirthday), and then delete that same birthday… then I have an error thrown because the EditBirthday page’s data for that deleted birthday is no longer accessible.

A snippet of the EditBirthday page looks like this…

const UpdateBirthday = ({ history, match }) => {
  const { state } = useContext(AppContext);
  const birthday = state.birthdays[match.params.id];

  const [error, setError] = useState();
  const [name, setName] = useState(birthday.name);
  const [day, setDay] = useState(birthday.dob.day);
  const [month, setMonth] = useState(birthday.dob.month.toString());
  const [year, setYear] = useState(birthday.dob.year);
  const [notes, setNotes] = useState(birthday.notes);
  const [updateClicked, setUpdateClicked] = useState(false);

birthday becomes undefined, so accessing properties of it throws a TypeError.

So what I really need is for the EditBirthday page to not save state between views. Or how do I actually destroy a page like this one once done with it?

Thanks

Maybe you could use history.replace() instead of history.push(), but it depends on how the user can navigate from one page to another, if it’s a link in one page or tabs or menu or back button…

Incidentally, /birthday/:id also matches /birthday/new, it’s not an exclusive match.

How can we make the matches be exclusive i.e. only pick the first matching route like using a Switch in react-router? The docs explicitly say:

“Since IonRouterOutlet takes over the job in determining which routes get rendered, using a Switch from React Router has no effect when used inside of an IonRouterOutlet. Switches still function as expected when used outside an IonRouterOutlet.”

And what good is Switch outside IonRouterOutlet?

I’m using routerLink mostly…

    <IonApp>
      <IonReactRouter>
        <IonMenu side="start" contentId="main">
          <IonHeader>
            <IonToolbar color="primary">
              <IonTitle>Birthdays AI</IonTitle>
            </IonToolbar>
          </IonHeader>
          <IonContent>
            <IonList>
              <IonMenuToggle>
                <IonItem routerLink="/home">Birthdays</IonItem>
                <IonItem routerLink="/about">About</IonItem>
                <IonItem routerLink="/contact">Contact</IonItem>
                {state.user && (
                  <IonItem button onClick={() => firebase.auth().signOut()}>
                    Logout
                  </IonItem>
                )}
                {!state.user && <IonItem routerLink="/login">Login</IonItem>}
              </IonMenuToggle>
            </IonList>
          </IonContent>
        </IonMenu>

        <IonRouterOutlet id="main">
          <Route path="/home" component={Home} exact />
          <Route path="/birthday/new" component={AddBirthday} exact />
          <Route path="/birthday/edit/:id" component={EditBirthday} exact />
          <Route path="/birthday/:id" component={ViewBirthday} exact />
          <Route path="/account/create" component={CreateAccount} exact />
          <Route path="/login" component={Login} exact />
          <Route path="/about" component={About} exact />
          <Route path="/contact" component={Contact} exact />
          <Redirect exact from="/" to="/home" />
        </IonRouterOutlet>
      </IonReactRouter>
    </IonApp>

I could replace routerLink with onClick={() => history.push('...')} I guess?

Yes I thought that IonRouterOutlet did a switch as well… so the first match only would match?

But how does the user navigate between EditBirthday and ViewBirthday?

IonRouterOutlet is not like a Switch, if multiple components match the requested path they will all be rendered. You can easily verify this by adding a console.log('rendering ComponentName') at the top of each component function.

Make sure the various paths do not overlap with each other. I’m not aware of another way.

What IonRouterOutlet gives you is the transition effects when navigating between pages. If you don’t need those transitions for some routes you can use a regular Switch.

1 Like

Sometimes the simplest solution is the best solution? There is no rule that controls how you must name your routes. If fact if this is a mobile app

      <Route path="/birthday-add" component={AddBirthday} exact />
      <Route path="/birthday-edit/:id" component={EditBirthday} exact />
      <Route path="/birthday-view/:id" component={ViewBirthday} exact />

Once a user has edited a birthday (EditBirthday), and then clicks Update, once the updates have been made, I now call history.replace('/home'); per your earlier suggestion. I actually decided to replace all history.push() with history.replace()

On ViewBirthday I have

<IonButton
    fill="clear"
    color="primary"
    onClick={() =>
        history.replace(`/birthday-edit/${match.params.id}`)
    }
    >
    Edit
</IonButton>

@aaronksaunders simple is often best… so I have updated my app like so…

        <IonRouterOutlet id="main">
          <Route path="/home" component={Home} exact />
          <Route path="/birthday-add" component={AddBirthday} exact />
          <Route path="/birthday-edit/:id" component={EditBirthday} exact />
          <Route path="/birthday-view/:id" component={ViewBirthday} exact />
          <Route path="/account/create" component={CreateAccount} exact />
          <Route path="/login" component={Login} exact />
          <Route path="/about" component={About} exact />
          <Route path="/contact" component={Contact} exact />
          <Redirect exact from="/" to="/home" />
        </IonRouterOutlet>

@mirkobalzarini I did a console.log at the top of every component, then ran a little test.

  1. On the Home screen where I view all birthdays, I clicked “add birthday”
  2. On clicking “save” I’m taken back to the home screen where I click on a birthday to view it
  3. Then I click “edit birthday”

Some notes… before I clicked something, I explicitly wrote out console.log('the action') in the console to show intent just before I did it.

This is what happened…

Notice how when I clicked “edit”, the AddBirthday, ViewBirthday, and EditBirthday components are all still being rendered. This doesn’t seem right?

So when I go back to view a birthday, and click “delete”, this happens…

… which is the core problem that crashes the app as it seems the EditBirthday component is still being rendered.

Seems like it only unmounts a component if you also pass direction: 'back'.

just call history.back()

i think the bigger question might be how you are architecting your user experience, this is a pretty standard CRUD UI around adding data, it should be list click push to detail, back, list push to add, back

Yes! You are exactly right @mirkonasato

I updated my history.replace statements with history.replace('/home', { direction: 'back' }); where I needed to do it programmatically.

For buttons I could do this…

<IonButton
    routerLink="/home"
    routerDirection="back"
    fill="outline"
>
    Cancel
</IonButton> 

FYI @aaronksaunders TypeError: history.back is not a function

Try history.goBack(). But @aaronksaunders is right, after adding an entry you should just goBack to the Home page.

You’ll need replace if you navigate from ViewBirthday to EditBirthday, which frankly I’m still not sure exactly how you’ve designed.

But it wouldn’t be an unreasonable flow to have e.g. List > View Item > Edit Item and the edit page also allow the user to delete the item. In that case you’d want to replace the View Item with the Edit Item, otherwise the View Item will be kept in the navigation stack.

2 Likes

@mirkobalzarini

The design is… view a list of birthdays, then clicking a birthday, you can either edit or delete that birthday. In EditBirthday once changes are saved, it goes back to ViewBirthday.

So from what I can tell, just using history.goBack() does the job, and I don’t actually need to use history.replace anymore.

Also I use

      <IonHeader>
        <IonToolbar>
          <IonButtons>
            <IonBackButton
              text={name}
              defaultHref={`/birthday-view/${match.params.id}`}
            />
          </IonButtons>
        </IonToolbar>
      </IonHeader>

in the toolbar for the EditBirthday component.

And in AddBirthday, there is a cancel button…

<IonButton
    routerLink="/home"
    routerDirection="back"
    fill="outline"
    >
    Cancel
</IonButton>

@magician1111 & @mirkonasato : We had a similar problem with this behaviour of IonRouterOutlet but since it uses react-router-dom's Route you can use regex in the path prop if you prefere the /birthday/new and birthday/:id paths, which would look like this (if :id is a number, otherwise you have to figure out a regex for new):
<Route path="/birthday/:id(\d+)" component={ViewBirthday} exact />