XState 💜 NoNav

Taking back control of navigation

Yann LEFLOUR

VP of Engineering @ BAM

@yleflour

joyto.dev

A (quick) history of navigation

A (quick) history of navigation

A (quick) history of navigation

A (quick) history of navigation

A (quick) history of navigation

A (quick) history of navigation

The Digital Era

A (quick) history of navigation

The Digital Era

A (quick) history of navigation

The Digital Era

1965

Project Xanadu

Ted Nelson

MULTICS

P. G. Neumann

R.C Daley

1990

WorldWideWeb

Tim Berners-Lee

A (quick) history of navigation

A (quick) history of navigation

Display

Browser

Navigate

Nav stack

Router

Server

Dirs

Page

A (quick) history of navigation

3. (1997) The Cookies

D. Kristol, L. Montulli (html/rfc2109)

2. (1995) Query Strings

R. Fielding (html/rfc1080)

1. (1988) Model View Controller

Glenn E. Krasner, Stephen T. Pope

 

Display

Browser

Navigate

Nav stack

Router

Server

Page

Dirs

A (quick) history of navigation

3. (1997) The Cookies

D. Kristol, L. Montulli (html/rfc2109)

2. (1995) Query Strings

R. Fielding (html/rfc1080)

1. (1988) Model View Controller

Glenn E. Krasner, Stephen T. Pope

 

Display

Browser

Navigate

Nav stack

Router

Dirs

Server

Template

Page

Session

Data

Query

+

A (quick) history of navigation

Display

Browser

Navigate

Nav stack

Router

Dirs

Server

Template

Page

Session

Data

Query

+

Single

Page

Application

Marcio Galli, Roger Soares, Ian Oeschger (2003)

AngularJS

(2010)

A (quick) history of navigation

Display

Browser

Navigate

Nav stack

Router

Dirs

Server

Session

Data

Query

+

SPA

Single

Page

Application

Marcio Galli, Roger Soares, Ian Oeschger (2003)

AngularJS

(2010)

Template

A (quick) history of navigation

Browser

Display

Server

Router

Navigate

Nav stack

Query

+

Session

Data

Javascript

Template

SPA

A (quick) history of navigation

Browser

Display

Server

Router

Navigate

Nav stack

Query

+

Session

Data

Javascript

SPA

Nav stack

Template

Components

A (quick) history of navigation

Browser

Display

Server

Router

Navigate

Nav stack

Query

+

Session

Data

Javascript

SPA

Nav stack

Components

A (quick) history of navigation

Browser

Display

Server

Router

Navigate

Nav stack

Query

+

Session

Data

Javascript

SPA

Nav stack

Components

A (quick) history of navigation

Browser

Display

Server

Router

Navigate

Nav stack

Query

+

Session

Store

Data

Javascript

SPA

Nav stack

Components

A (quick) history of navigation

Browser

Display

Server

Router

Navigate

Nav stack

Query

+

Data

Javascript

SPA

Nav stack

Components

Store

A (quick) history of navigation

angular.
  module('phonecatApp').
  config(['$routeProvider',
    function config($routeProvider) {
      $routeProvider.
        when('/phones', {
          template: '<phone-list></phone-list>'
        }).
        when('/phones/:phoneId', {
          template: '<phone-detail></phone-detail>'
        }).
        otherwise('/phones');
    }
  ]);
<a ng-href="http://www.gravatar.com/avatar/{{hash}}">link1</a>
this.router.navigate(["../../parent"], {relativeTo: this.route, 
    queryParams: {p1: 'value', p2: 'v2'}, fragment: 'frag'});

AngularJS

<Switch>
  <Route path="/about">
    <About />
    </Route>
  <Route path="/users">
    <Users />
    </Route>
  <Route path="/">
    <Home />
    </Route>
</Switch>
<Link to="/users">Users</Link>
<Route exact path="/">
  {loggedIn ? <Redirect to="/dashboard" /> : <PublicHomePage />}
</Route>

ReactDOM

Browser

Display

Server

Router

Navigate

Nav stack

Query

+

Data

Javascript

SPA

Nav stack

Components

Store

A (quick) history of navigation

angular.
  module('phonecatApp').
  config(['$routeProvider',
    function config($routeProvider) {
      $routeProvider.
        when('/phones', {
          template: '<phone-list></phone-list>'
        }).
        when('/phones/:phoneId', {
          template: '<phone-detail></phone-detail>'
        }).
        otherwise('/phones');
    }
  ]);
<a ng-href="http://www.gravatar.com/avatar/{{hash}}">link1</a>
this.router.navigate(["../../parent"], {relativeTo: this.route, 
    queryParams: {p1: 'value', p2: 'v2'}, fragment: 'frag'});

AngularJS

<Switch>
  <Route path="/about">
    <About />
    </Route>
  <Route path="/users">
    <Users />
    </Route>
  <Route path="/">
    <Home />
    </Route>
</Switch>
<Link to="/users">Users</Link>
<Route exact path="/">
  {loggedIn ? <Redirect to="/dashboard" /> : <PublicHomePage />}
</Route>

ReactDOM

ReactNative

A (quick) history of navigation

ReactDOM

<Switch>
  <Route path="/about">
    <About />
    </Route>
  <Route path="/users">
    <Users />
    </Route>
  <Route path="/">
    <Home />
    </Route>
</Switch>
<Link to="/users">Users</Link>
<Route exact path="/">
  {loggedIn ? <Redirect to="/dashboard" /> : <PublicHomePage />}
</Route>

ReactNative

Navigation.setRoot({
    root: {
      bottomTabs: { children: [{
        stack: { children: [{
          component: {
            name: 'Home'
          }
        }]}
      },
      {
        stack: { children: [{
          component: {
            name: 'Settings'
          }
        }]}
      }]}
    }});
Navigation.push(props.componentId, {
  component: {
    name: 'UserProfile',
    passProps: { name: 'John Doe' }
  }
});
<NavigationContainer>
  <Stack.Navigator>
    <Stack.Screen
      name="Home"
      component={Home}
    />
    <Stack.Screen
      name="Settings"
      component={Settings}
    />
  </Stack.Navigator>
</NavigationContainer>
navigation.navigate(
  'UserProfile',
  { name: 'John Doe' }
);

react-native-navigation

react-navigation

A (quick) history of navigation

ReactNative

Navigation.setRoot({
    root: {
      bottomTabs: { children: [{
        stack: { children: [{
          component: {
            name: 'Home'
          }
        }]}
      },
      {
        stack: { children: [{
          component: {
            name: 'Settings'
          }
        }]}
      }]}
    }});
Navigation.push(props.componentId, {
  component: {
    name: 'UserProfile',
    passProps: { name: 'John Doe' }
  }
});
<NavigationContainer>
  <Stack.Navigator>
    <Stack.Screen
      name="Home"
      component={Home}
    />
    <Stack.Screen
      name="Settings"
      component={Settings}
    />
  </Stack.Navigator>
</NavigationContainer>
navigation.navigate(
  'UserProfile',
  { name: 'John Doe' }
);

react-native-navigation

react-navigation

From imperative to declarative

A (quick) history of navigation

ReactNative

Navigation.setRoot({
    root: {
      bottomTabs: { children: [{
        stack: { children: [{
          component: {
            name: 'Home'
          }
        }]}
      },
      {
        stack: { children: [{
          component: {
            name: 'Settings'
          }
        }]}
      }]}
    }});
Navigation.push(props.componentId, {
  component: {
    name: 'UserProfile',
    passProps: { name: 'John Doe' }
  }
});
<NavigationContainer>
  <Stack.Navigator>
    <Stack.Screen
      name="Home"
      component={Home}
    />
    <Stack.Screen
      name="Settings"
      component={Settings}
    />
  </Stack.Navigator>
</NavigationContainer>
navigation.navigate(
  'UserProfile',
  { name: 'John Doe' }
);

react-native-navigation

react-navigation

From imperative to declarative

Components

Store

State

Action

Browser

Display

Server

Router

Navigate

Nav stack

Query

+

Data

Javascript

SPA

Nav stack

Components

Store

From imperative to declarative

Components

Store

State

Action

Browser

Display

Server

Router

Navigate

Nav stack

Query

+

Data

Javascript

SPA

Nav stack

Components

Store

From imperative to declarative

Components

Store

State

Action

Nav stack

Navigate

Query

+

Components

Display

Nav. Action

Nav. Store

Nav. State

From imperative to declarative

Components

Store

State

Action

Components

Nav. Action

Nav. Store

Nav. State

From imperative to declarative

#1 - Page name as a parameter

Components

Store

State

Action

Components

Nav. Action

Nav. Store

Nav. State

props.navigation.push('Profile');
profilePage.show();

=

Login Page

Profile Page

Nav. Service

From imperative to declarative

#1 - Page name as a parameter

props.navigation.push('Profile');
profilePage.show();

=

Login Page

Profile Page

Nav. Store

State

Components

Action

Store

Components

Nav. Action

Nav. State

Side Effect

Nav. Service

From imperative to declarative

#1 - Page name as a parameter

props.navigation.push('Profile');
profilePage.show();

=

Login Page

Profile Page

Nav. Store

State

Components

Action

Store

Components

Nav. State

Side Effect

Nav. Service

From imperative to declarative

#2 - It's your main dependency

Pick folder

Login

#3 You don't need it

#3 You don't need it

Page

Navigate

Page

Browser

Display

Router

Navigate

Nav stack

Query

+

Store

Javascript

Nav stack

Components

#3 You don't need it

Page

Navigate

Page

Browser

Display

Router

Nav stack

Store

Javascript

Nav stack

Components

Navigate

Query

+

App

#3 You don't need it

<Modal
  animationType="slide"
  transparent={true}
  visible={modalVisible}
  onRequestClose={() => {
    Alert.alert(
      "Modal has been closed."
    );
  }}
>

Display

Router

Store

Javascript

Nav stack

Components

App

The Modal

The concept of NoNav

The concept of NoNav

Caroline Besnard

Head of Design - BAM

"Navigation should be your concern #2 after the concept of your product because a bad navigation :

  • is a break for adoption
  • decreases the product perceived value"

The concept of NoNav

Caroline Besnard

Head of Design - BAM

"Navigation should be your concern #2 after the concept of your product because a bad navigation :

  • is a break for adoption
  • decreases the product perceived value"

Smaller Screen

Same Information

Mobility

Short Attention Span

More Navigation

+

=>

+

Effiency

<=

The concept of NoNav

The concept of NoNav

The concept of NoNav

Position

Deep

Linking

Bluetooth

Sharing

Push

Notifications

Network

status

Context

The Solution

Thomas Pucci

The Solution

Thomas Pucci

Software Architect - BAM

yarn add react-nonav

" React Native declarative and reactive navigation "

import React from 'react';
import { Canal, Screen } from 'react-nonav';
import { FirstName } from './FirstName';
import { LastName } from './LastName';

const SignInCanal = () => (
  <Canal>
    <Screen name="firstname" Component={FirstName} visible />
    <Screen name="lastname" Component={LastName} visible={false} />
  </Canal>
);

The Solution

Thomas Pucci

Software Architect - BAM

yarn add react-nonav

" React Native declarative and reactive navigation "

import React from 'react';
import { Canal, Screen } from 'react-nonav';
import { FirstName } from './FirstName';
import { LastName } from './LastName';

const SignInCanal = () => (
  <Canal>
    <Screen name="firstname" Component={FirstName} visible />
    <Screen name="lastname" Component={LastName} visible={false} />
  </Canal>
);

The Solution

import React from 'react';
import { Canal, Screen } from 'react-nonav';
import { FirstName } from './FirstName';
import { LastName } from './LastName';

const SignInCanal = () => (
  <Canal>
    <Screen name="firstname" Component={FirstName} visible />
    <Screen name="lastname" Component={LastName} visible={false} />
  </Canal>
);

The Solution

import React from 'react';
import { Canal, Screen } from 'react-nonav';
import { FirstName } from './FirstName';
import { LastName } from './LastName';

const SignInCanal = () => (
  <Canal>
    <Screen name="firstname" Component={FirstName} visible />
    <Screen name="lastname" Component={LastName} visible={false} />
  </Canal>
);

Components

Store

State

Action

Components

Nav. Action

Nav. Store

Nav. State

The Solution

import React from 'react';
import { Canal, Screen } from 'react-nonav';
import { FirstName } from './FirstName';
import { LastName } from './LastName';

const SignInCanal = () => (
  <Canal>
    <Screen name="firstname" Component={FirstName} visible />
    <Screen name="lastname" Component={LastName} visible={false} />
  </Canal>
);

Components

Store

State

Action

Components

Nav. Action

Nav. Store

Nav. State

The Solution

Components

Store

State

Action

Components

Nav. Action

Nav. Store

Nav. State

Hold up a minute

currentPage

Event

Login

previousPage

Home

+

=

Home

Order

+

=

Confirm

currentPage

Confirm

isNetworkUp

displayConfirm

False

True

=

+

+

False

True

=

XState to the rescue

XState to the rescue

David Khourshid

Fully featured state machine

Adheres to W3C State Charts Specs

State 1

State 2

Transition

event

Context

Machine

start() 

send(event)

stop()

Service

action

action(Assign)

event

Context

action(Assign)

=

=

=

Reducer

State

Action

State 1

State 2

Transition

event

Context

Machine

start() 

send(event)

stop()

Service

action

action(Assign)

start() 

send(event)

stop()

Service

State 1

State 2

Transition

event

action

Analytics

Logs

start() 

send(event)

stop()

Service

State 1

State 2

Transition

event

action

Analytics

Logs

Order

Confirm

Login

action

State 1

State 2

Transition

event

Order

Confirm

Login

State 1

State 2

Transition

event

Order

Confirm

Login

const fetchMachine = Machine({
    id: 'root',
    initial: 'home',
    context: {
      token: null
    },
    states: {
      home: {
        on: {
          ACCOUNT_PRESS: [
            {target: 'logIn', cond: 'isTokenUnavailable' },
            {target: 'account', cond: 'isTokenAvailable'}
          ]
        }
      },
      logIn: {
        invoke: {
          src: 'backPressHandler',
          id: 'back-press-handler'
        },
        on: {
          BACK_PRESS: 'home',
          LOGIN: {
            target: 'account',
            actions: ['persistToken'],
          },
        }
      },
      account: {
      },
    }
  });
  
const fetchMachine = Machine({
    id: 'root',
    initial: 'home',
    context: {
      token: null
    },
    states: {
      home: {
        on: {
          ACCOUNT_PRESS: [
            {target: 'logIn', cond: 'isTokenUnavailable' },
            {target: 'account', cond: 'isTokenAvailable'}
          ]
        }
      },
      logIn: {
        invoke: {
          src: 'backPressHandler',
          id: 'back-press-handler'
        },
        on: {
          BACK_PRESS: 'home',
          LOGIN: {
            target: 'account',
            actions: ['persistToken'],
          },
        }
      },
      account: {
      },
    }
  });
  
const fetchMachine = Machine({
    id: 'root',
    initial: 'home',
    context: {
      token: null
    },
    states: {
      home: {
        on: {
          ACCOUNT_PRESS: [
            {target: 'logIn', cond: 'isTokenUnavailable' },
            {target: 'account', cond: 'isTokenAvailable'}
          ]
        }
      },
      logIn: {
        invoke: {
          src: 'backPressHandler',
          id: 'back-press-handler'
        },
        on: {
          BACK_PRESS: 'home',
          LOGIN: {
            target: 'account',
            actions: ['persistToken'],
          },
        }
      },
      account: {
      },
    }
  });
  
const fetchMachine = Machine({
    id: 'root',
    initial: 'home',
    context: {
      token: null
    },
    states: {
      home: {
        on: {
          ACCOUNT_PRESS: [
            {target: 'logIn', cond: 'isTokenUnavailable' },
            {target: 'account', cond: 'isTokenAvailable'}
          ]
        }
      },
      logIn: {
        invoke: {
          src: 'backPressHandler',
          id: 'back-press-handler'
        },
        on: {
          BACK_PRESS: 'home',
          LOGIN: {
            target: 'account',
            actions: ['persistToken'],
          },
        }
      },
      account: {
      },
    }
  });
  
const fetchMachine = Machine({
    id: 'root',
    initial: 'home',
    context: {
      token: null
    },
    states: {
      home: {
        on: {
          ACCOUNT_PRESS: [
            {target: 'logIn', cond: 'isTokenUnavailable' },
            {target: 'account', cond: 'isTokenAvailable'}
          ]
        }
      },
      logIn: {
        invoke: {
          src: 'backPressHandler',
          id: 'back-press-handler'
        },
        on: {
          BACK_PRESS: 'home',
          LOGIN: {
            target: 'account',
            actions: ['persistToken'],
          },
        }
      },
      account: {
      },
    }
  });
  
const fetchMachine = Machine({
    id: 'root',
    initial: 'home',
    context: {
      token: null
    },
    states: {
      home: {
        on: {
          ACCOUNT_PRESS: [
            {target: 'logIn', cond: 'isTokenUnavailable' },
            {target: 'account', cond: 'isTokenAvailable'}
          ]
        }
      },
      logIn: {
        invoke: {
          src: 'backPressHandler',
          id: 'back-press-handler'
        },
        on: {
          BACK_PRESS: 'home',
          LOGIN: {
            target: 'account',
            actions: ['persistToken'],
          },
        }
      },
      account: {
      },
    }
  });
  
const fetchMachine = Machine({
    id: 'root',
    initial: 'home',
    context: {
      token: null
    },
    states: {
      home: {
        on: {
          ACCOUNT_PRESS: [
            {target: 'logIn', cond: 'isTokenUnavailable' },
            {target: 'account', cond: 'isTokenAvailable'}
          ]
        }
      },
      logIn: {
        invoke: {
          src: 'backPressHandler',
          id: 'back-press-handler'
        },
        on: {
          BACK_PRESS: 'home',
          LOGIN: {
            target: 'account',
            actions: ['persistToken'],
          },
        }
      },
      account: {
      },
    }
  });
  

Pick folder

Login

Pick folder

Login

Pick folder

Login

Let's sum that up

Mobile Navigation is a mental model from the past

Our users want to avoid it

We need to use context

But context gets messy because of imperative

We need to control navigation

But it is complex to handle

Let's sum that up

react-nonav to transition in a declarative way

XState to handle context

and complex user journeys

XState 💜 NoNav

Taking back control of navigation

Thank you

joyto.dev/xstate-nonav

Slides

Notes

References

Example

XState ❤️ NoNav

By Yann Leflour

XState ❤️ NoNav

  • 1,771