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