-
Notifications
You must be signed in to change notification settings - Fork 143
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix($docs): add docs: client-only-api, low-level-api, react-native
- Loading branch information
1 parent
eb62042
commit db5cc94
Showing
8 changed files
with
293 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
||
|
||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ;) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters