Skip to content

Commit

Permalink
fix($docs): add docs: client-only-api, low-level-api, react-native
Browse files Browse the repository at this point in the history
  • Loading branch information
faceyspacey committed May 8, 2017
1 parent eb62042 commit db5cc94
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 71 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ That's all folks! :+1:
* [server side rendering](./docs/server-rendering.md)
* [scroll restoration](./docs/scroll-restoration.md)
* [redirects](./docs/server-rendering.md#redirects-example)
* [React Native](./docs/react-native.md)
* [client-only API](./docs/client-only-api.md)

## FAQ

Expand Down
91 changes: 91 additions & 0 deletions docs/client-only-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Client-Only API
The following are features you should avoid unless you have a reason that makes sense to use them. These features revolve around the [history package's](npmjs.com/package/history) API. They make the most sense in React Native--for things like back button handling. On web, you'll rarely need it as you'll want to use our [<Link /> component](https://github.com/faceyspacey/redux-first-router-link) to create real links embedded in the page for SEO/SSR instead.

One case for web though--if you're curious--is the fake address bar you've probably seen in one our examples. If you have such needs, go for it.

*Takeaway:* On web, force yourself to use our `<Link />` package so that real `<a>` tags get embedded in the page for SEO and link-sharing benefits; beware of using the below methods.



## Imperative Methods

* **push:** (path) => void
* **replace:** (path) => void
* **back:** () => void
* **next:** () => void
* **go:** (number) => void
* **canGoBack:** (path) => boolean
* **canGoForward:** () => boolean
* **prevPath:** () => ?string
* **nextPath:** () => ?string

**You can import them like so:**

```javascript
import { back, canGoBack } from 'redux-first-router'
```
> For a complete example, see the [React Native Android BackHandler Example](./react-native.md#android-backhandler).
Keep in mind these methods should not be called until you call `connectRoutes`. This is almost always fine, as your store configuration typically happens before your app even renders once.

*Note: do NOT rely on these methods on the server, as they do not make use of enclosed* ***per request*** *state. If you must, use the corresponding
methods on the `history` object you create per request which you pass to `connectRoutes(history`). Some of our methods are convenience methods for what you can do with `history`, so don't expect `history` to have all the above methods, but you can achieve the same. See the [history package's docs](https://github.com/ReactTraining/history)
for more info.*



## Declarative History API

The `location` state and the `action.meta.location` object *on the server or in environments where you used `createMemoryHistory`
to create your history (such as React Native)* will also maintain the ***declarative*** information about the history stack. It can be found within the `history` key, and this
is its shape:

```javascript
history: {
index: number, // index of focused entry/path
length: number, // total # of entries/paths
entries: Array<string>, // array of paths obviously
}
```

This is different from what the `history` package maintains in that you can use Redux to reactively respond to its changes. Here's an example:

```js
import React from 'react'
import { connect } from 'react-redux'

const MyComponent = ({ isLast, path }) =>
isLast ? <div>last</div> : <div>{path}</div>

const mapStateToProps = ({ location: { history } }) => ({
isLast: history.index === history.length - 1,
path: history.entries[history.index].pathname
})

expoort default connect(mapStateToProps)(MyComponent)
```
> By the way, this example also showcases the ultimate goal of **Redux First Router:** *to stay within the "intuitive" workflow of standard Redux patterns*.

If you're wondering why such state is limited to `createMemoryHistory`, it's because it can't be consistently maintained in the browser. Here's why:

[would it be possible for createBrowserHistory to also have entries and index? #441](https://github.com/ReactTraining/history/issues/441)

In short, the browser will maintain the history for your website even if you refresh the page, whereas from our app's perspective,
if that happens, we'll lose awareness of the history stack. `sessionStorage` almost can solve the issue, but because of various
browser inconsitencies (e.g. when cookies are blocked, you can't recall `sessionStorage`), it becomes unreliable and therefore
not worth it.


***When might I have use for it though?***

Well, you see the fake browser we made in our playground on *webpackbin*, right? We emulate the browser's back/next buttons
using it. If you have the need to make such a demo or something similar, totally use it--we plan to maintain the secret API.

*Redux First Router's* [React Navigation implementation](./react-native#react-navigation) also relies heavily on `history` state.






50 changes: 50 additions & 0 deletions docs/low-level-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Low-level API

Below are some additional methods we export. The target user is package authors. Application developers will rarely need this.

## `actionToPath` and `pathToAction`
These methods are also exported:

```javascript
import { actionToPath, pathToAction } from 'redux-first-router'

const { routesMap } = store.getState().location

const path = actionToPath(action, routesMap)
const action = pathToAction(path, routesMap)
```

You will need the `routesMap` you made, which you can import from where you created it or you can
get any time from your store.

[Redux First Router Link](https://github.com/faceyspacey/redux-first-router-link)
generates your links using these methods. It does so using the `store` Redux makes available via `context` in
order for all your links not to need to subscribe to the `store` and become unnecessarilly reactive.

Unlike *React Router* we do not offer a [NavLink](https://reacttraining.com/react-router/#navlink) component
as that leads to unnecessary renders. That's why we using your store `context` instead. The `routesMap` does not change, so we can get it once without responding to reactive updates from your `location` reducer state.

We will however likely create a `<NavLink />` component in the future. Until then, it's extremely easy
to make yourself. You can do so in an ad hoc way *without* using `actionToPath` or `pathToAction` (just by using your app-specific state),
but if you'd like to abstract it, analyze the **Redux First Router Link** code. Feel free to make a PR; we'd welcome
a second export in that package.


## History

You can get access to the `history` object that you initially created, but from anywhere in your code without having to pass it down:

```js
import { history } from 'redux-first-router'

// notice that you must call it as a function
history().entries.map(entry => entry.pathname)
history().index
history().length
history().action
// etc
```

Keep in mind `history()` will return undefined until you call `connectRoutes`. This is usually fine, as your store configuration typically happens before your app even renders once.

View the [history package](https://www.npmjs.com/package/history) for more info.
125 changes: 125 additions & 0 deletions docs/react-native.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# React Native
**Redux First Router** has been thought up from the ground up with React Native (and Server Environments) in mind. They both make use of
the [history package's](https://www.npmjs.com/package/history):



## Linking
This is really where the magic of **Redux First Router** shines. Everything is supposed to be URL-based, right. So handling incoming links from *React Native's* `Linking` API should be as easy as it comes. Here's how you kick off your app from now on:

*src/linking.js:*
```js
import { push } from 'redux-first-router'
import startApp from './startApp'

Linking.getInitialURL().then(startApp)
Linking.addEventListener('url', ({ url }) => push(url))
```

*startApp.js:*
```js
import createMemoryHistory from 'history/createMemoryHistory'
import configureStore from './configureStore'
import renderApp from './renderApp'

export default url => {
const initialPath = url.substr(url.indexOf('.com') + 5)
const history = createMemoryHistory({
initialEntries: [ initialPath ],
})
const store = configureStore(history)
const App = renderApp(store)

AppRegistry.registerComponent('ReduxFirstRouterBoilerplateNative', () => App)
}
```


## Android `BackHandler`
Implementing back button handling in React Native is as easy as it comes with *Redux First Router*. It's as follows:


```js
import { BackHandler } from 'react-native'
import { back, canGoBack } from 'redux-first-router'

BackHandler.addEventListener('hardwareBackPress', () => {
if (canGoBack()) {
back()
return true
}

return false
})
```

## First Class React Navigation Support!
This perhaps is the crowning feature of **Redux First Router**, we have a lot to share about it.

First off, all the above setup continues to be exactly how you setup your *React Navigation*-based app. However, if you've used or studied *React Navigation*,
you know that there isn't one linear history of path "entries." There in fact is a tree of them. You may have read what the *React Navigation* team had to see about this:

>A common navigation structure in iOS is to have an independent navigation stack for each tab, where all tabs can be covered by a modal. This is three layers of router: a card stack, within tabs, all within a modal stack. So unlike our experience on web apps, the navigation state of mobile apps is too complex to encode into a single URI.
https://github.com/react-community/react-navigation/blob/master/docs/blog/2017-01-Introducing-React-Navigation.md#routers-for-every-platform

That was the key realization that enabled the *React Navigation* team to finally solve the *"navigation" problem* for React. However, now that it's done, we've had a realization of our own:

**It's possible to reconcile a linear "time track" (stack) of history entries with the tree form React Navigation maintains in your Redux store.**

And that's exactly what we've done. All you need to do is pass an Array of the navigators you're using like so:


```js
connectRoutes(history, routesMap, {
navigators: [
stackNav,
drawerNav,
tabsNav
]
})
```
> note: custom navigators must have a `router` key
and voila!

Our middleware we'll handle any actions dispatched by the default navigators (such as the `back` buttons). And we'll replace the `router.getActionForState` methods on your navigators for you with ones that will respond to the typical actions **Redux First Router** dispatches. From your point of view, nothing will have changed when it comes to configuring *React Navigation* reducers. What will have changed is you now get access to the seamless *Redux First Router* API you're perhaps familiar with from web.

That's not all though. You can now use `back` and `next` across all your Navigators, and it will automatically infer what to do. This makes Android's `BackHandler` a breeze.

Most importantly though it solves several key issues that *React Navigation* will never solve: the fact that not all your states will be Navivgator-based. You now **DON'T have to setup a Navigator** just to properly respond to a URL. For example, if you want to trigger a Modal by simply doing:

```js
const App ({ showModal }) =>
<View>
<RestOfApp />
{showModal && <MyModal />}
</View>

const mapStateToProps ({ location }) => ({
showModal: location.type === 'MODAL'
})

export default connect(mapStateToProps)(App)
```

you can.

In large apps the reality is there will be endless cases where you want paths associated with states that your Navigator can't represent. *React Navigation* is going to get a lot more animation power, but just imagine right now you want the screen to flip around in response to **URL-driven state** (such as from clicking a coupon ad on Facebook). It's not something *React Navigation* is made for. I.e. one-off fancy state changes. I.e. the precise ones you may want to be URL-driven.

So by using *Redux First Router* as your ***"master routing controller"***, you're never left in the dust when it comes to URL-driven state.

A final issue it solves is: when you have multiple disconnected Navigators. Perhaps you have a custom drawer with a *StackNavigator* in it, which appears with a *TabNavigator* underneath it partially visible. Now you have 2 separate tracks/routers essentially. You need a *master router* to control the two if you want to respond to incoming URLs consistently. **Redux First Router** fits in a perfect place when it comes to **React Navigation**.


## How React Navigation Integration Works

*You want to know how we managed to pull this off?* The following is our tree-to-stack history reconcialiation algorithm:

* **foo bar:** bla bla bla lore ipsum
* **foo bar:** bla bla bla lore ipsum
* **foo bar:** bla bla bla lore ipsum
* **foo bar:** bla bla bla lore ipsum
* **foo bar:** bla bla bla lore ipsum
* **foo bar:** bla bla bla lore ipsum
* **foo bar:** bla bla bla lore ipsum
* ...we haven't figured it out yet if you're still seeing this, but we're working on it ;)
70 changes: 0 additions & 70 deletions docs/secret-api.md

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "redux-first-router",
"version": "0.0.0-development",
"description": "think of your app in states not routes (and, yes, while keeping the address bar in sync)",
"main": "dist/index.js",
"main": "src/index.js",
"scripts": {
"build": "babel src -d dist",
"build:umd": "BABEL_ENV=commonjs NODE_ENV=development webpack src/index.js dist/redux-first-router.js",
Expand Down
17 changes: 17 additions & 0 deletions src/connectRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,23 @@ export const go = (n: number) => _history.go(n)

export const canGo = (n: number) => _history.canGo(n)

export const canGoBack = (): boolean => !!_history.entries[_history.index - 1]

export const canGoForward = (): boolean =>
!!_history.entries[_history.index + 1]

export const prevPath = (): ?string => {
const entry = _history.entries[_history.index - 1]
return entry && entry.pathname
}

export const nextPath = (): ?string => {
const entry = _history.entries[_history.index + 1]
return entry && entry.pathname
}

export const history = () => _history

export const scrollBehavior = () => _scrollBehavior

export const updateScroll = () => _updateScroll && _updateScroll()
7 changes: 7 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ export {
replace,
back,
next,
go,
canGo,
canGoBack,
canGoForward,
prevPath,
nextPath,
history,
scrollBehavior,
updateScroll
} from './connectRoutes'
Expand Down

0 comments on commit db5cc94

Please sign in to comment.