From db5cc9406c20c6165b752ff4c15a84f6f820e315 Mon Sep 17 00:00:00 2001 From: James Gillmore Date: Mon, 8 May 2017 07:27:13 -0700 Subject: [PATCH] fix($docs): add docs: client-only-api, low-level-api, react-native --- README.md | 2 + docs/client-only-api.md | 91 +++++++++++++++++++++++++++++ docs/low-level-api.md | 50 ++++++++++++++++ docs/react-native.md | 125 ++++++++++++++++++++++++++++++++++++++++ docs/secret-api.md | 70 ---------------------- package.json | 2 +- src/connectRoutes.js | 17 ++++++ src/index.js | 7 +++ 8 files changed, 293 insertions(+), 71 deletions(-) create mode 100644 docs/client-only-api.md create mode 100644 docs/low-level-api.md create mode 100644 docs/react-native.md delete mode 100644 docs/secret-api.md diff --git a/README.md b/README.md index 6e2de0ed..bb699983 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/client-only-api.md b/docs/client-only-api.md new file mode 100644 index 00000000..1dc02386 --- /dev/null +++ b/docs/client-only-api.md @@ -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 [ 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 `` package so that real `` 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, // 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 ?
last
:
{path}
+ +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. + + + + + + diff --git a/docs/low-level-api.md b/docs/low-level-api.md new file mode 100644 index 00000000..a6026c20 --- /dev/null +++ b/docs/low-level-api.md @@ -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 `` 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. diff --git a/docs/react-native.md b/docs/react-native.md new file mode 100644 index 00000000..db62c4c5 --- /dev/null +++ b/docs/react-native.md @@ -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 }) => + + + {showModal && } + + +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 ;) diff --git a/docs/secret-api.md b/docs/secret-api.md deleted file mode 100644 index 8a84c22d..00000000 --- a/docs/secret-api.md +++ /dev/null @@ -1,70 +0,0 @@ -# Secret API -The following are basically advanced features you should only use if you have a really good reason to do so. - -## Declarative & Imperative History Functionality -The `location` state and the `action.meta.location` object *on the server or in environments where you used `createMemoryHistory` -to create your history* 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, // array of paths obviously -} -``` - -Funnily enough, 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. - -In addition to the declarative state above, here's the ***imperative*** methods we used to make that fake browser (these methods are available on *both the client and the server*): - -```javascript -import { push, replace, back, next } from 'redux-first-router' -``` -* **push:** (path) => void -* **replace:** (path) => void -* **back:** () => void -* **next:** () => void - -*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`). See the [history package's docs](https://github.com/ReactTraining/history) -for more info.* - - -## `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. We plan to offer it 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. - diff --git a/package.json b/package.json index 6a85baa4..a4190f7e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/connectRoutes.js b/src/connectRoutes.js index e314aeb3..91e5b1a1 100644 --- a/src/connectRoutes.js +++ b/src/connectRoutes.js @@ -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() diff --git a/src/index.js b/src/index.js index 776f0581..4a03b6f2 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,13 @@ export { replace, back, next, + go, + canGo, + canGoBack, + canGoForward, + prevPath, + nextPath, + history, scrollBehavior, updateScroll } from './connectRoutes'