Skip to content

Commit

Permalink
feat: Add useRouter and RouterContext (#307)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Pass router object through our own context rather than Redux store
BREAKING CHANGE: Remove custom connector helpers
  • Loading branch information
taion committed Apr 5, 2019
1 parent 8064ec4 commit 2a90dcb
Show file tree
Hide file tree
Showing 19 changed files with 221 additions and 159 deletions.
41 changes: 28 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -767,11 +767,9 @@ A link will navigate per its `to` location descriptor when clicked. You can prev
Otherwise, `<Link>` forwards additional props to the child element.
If you have your own store with `foundReducer` installed on a key other than `found`, use `createConnectedLink` with a options object with a `getFound` function to create a custom link component class, as with `createConnectedRouter` above.
#### Programmatic navigation
The `withRouter` HOC wraps an existing component class or function and injects `match` and `router` props, as on route components above, but with only the `location` and `params` properties on `match`. You can use this HOC to create components that navigate programmatically in event handlers.
The `withRouter` HOC wraps an existing component class or function and injects `match` and `router` props, as on route components above. You can use this HOC to create components that navigate programmatically in event handlers.
```js
const propTypes = {
Expand All @@ -798,7 +796,21 @@ MyButton.propTypes = propTypes;
export default withRouter(MyButton);
```
If you have your own store with `foundReducer` installed on a key other than `found`, use `createWithRouter` with a options object with a `getFound` function to create a custom HOC, as with `createConnectedLink` above.
The `useRouter` Hook provides the same capabilities.
```js
function MyButton() {
const { match, router } = useRouter();

const onClick = useCallback(() => {
router.replace('/widgets');
}, [router]);

return (
<button onClick={onClick}>Current widget: {match.params.widgetId}</button>
);
}
```
#### Blocking navigation
Expand Down Expand Up @@ -949,10 +961,11 @@ These behave similarly to their counterparts above, except that the options obje
#### Server-side rendering with custom Redux store
Found exposes lower-level functionality for doing server-side rendering for use with your own Redux store, as with `createConnectedRouter` above. On the server, use `getStoreRenderArgs` to get a promise for the arguments to your `render` function.
Found exposes lower-level functionality for doing server-side rendering for use with your own Redux store, as with `createConnectedRouter` above. On the server, use `getStoreRenderArgs` to get a promise for the arguments to your `render` function, then wrap the rendered elements with a `<RouterProvider>`.
```js
import { getStoreRenderArgs } from 'found';
import { RouterProvider } from 'found/lib/server';

/* ... */

Expand All @@ -976,14 +989,16 @@ app.use(async (req, res) => {
throw e;
}

res
.status(renderArgs.error ? renderArgs.error.status : 200)
.send(
renderPageToString(
<Provider store={store}>{render(renderArgs)}</Provider>,
store.getState(),
),
);
res.status(renderArgs.error ? renderArgs.error.status : 200).send(
renderPageToString(
<Provider store={store}>
<RouterProvider renderArgs={renderArgs}>
{render(renderArgs)}
</RouterProvider>
</Provider>,
store.getState(),
),
);
});
```
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"homepage": "https://github.com/4Catalyzer/found#readme",
"dependencies": {
"@babel/runtime-corejs2": "^7.4.0",
"@restart/context": "^2.1.4",
"farce": "^0.2.8",
"is-promise": "^2.1.0",
"lodash": "^4.17.11",
Expand Down
5 changes: 3 additions & 2 deletions src/Link.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import createConnectedLink from './createConnectedLink';
import BaseLink from './BaseLink';
import withRouter from './withRouter';

const Link = createConnectedLink();
const Link = withRouter(BaseLink);
Link.displayName = 'Link';

export default Link;
3 changes: 3 additions & 0 deletions src/RouterContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from 'react';

export default React.createContext(null);
52 changes: 38 additions & 14 deletions src/createBaseRouter.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';
import React from 'react';
import { ReactReduxContext } from 'react-redux';
import StaticContainer from 'react-static-container';
import warning from 'warning';
import mapContextToProps from '@restart/context/mapContextToProps';

import { routerShape } from './PropTypes';
import createRender from './createRender';
import RouterContext from './RouterContext';
import createStoreRouterObject from './utils/createStoreRouterObject';
import resolveRenderArgs from './utils/resolveRenderArgs';

export default function createBaseRouter({
Expand All @@ -24,13 +27,13 @@ export default function createBaseRouter({
});

const propTypes = {
store: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
resolvedMatch: PropTypes.object.isRequired,
matchContext: PropTypes.any,
resolver: PropTypes.shape({
resolveElements: PropTypes.func.isRequired,
}).isRequired,
router: routerShape.isRequired,
onResolveMatch: PropTypes.func.isRequired,
initialRenderArgs: PropTypes.object,
};
Expand All @@ -39,9 +42,12 @@ export default function createBaseRouter({
constructor(props) {
super(props);

const { initialRenderArgs } = props;
const { store, initialRenderArgs } = props;

this.router = createStoreRouterObject(store);

this.state = {
renderArgs: initialRenderArgs || null,
element: initialRenderArgs ? render(initialRenderArgs) : null,
};

Expand Down Expand Up @@ -71,19 +77,14 @@ export default function createBaseRouter({
'one router instance when using hot reloading.',
);

window.__FOUND_REPLACE_ROUTE_CONFIG__ = this.props.router.replaceRouteConfig;
window.__FOUND_REPLACE_ROUTE_CONFIG__ = this.router.replaceRouteConfig;
}
/* eslint-enable no-underscore-dangle */
/* eslint-env browser: false */
}
}

componentWillReceiveProps(nextProps) {
warning(
nextProps.router === this.props.router,
'<BaseRouter> does not support changing the router object.',
);

if (
nextProps.match !== this.props.match ||
nextProps.resolver !== this.props.resolver ||
Expand Down Expand Up @@ -118,7 +119,10 @@ export default function createBaseRouter({
const pendingMatch = this.props.match;

try {
for await (const renderArgs of resolveRenderArgs(this.props)) {
for await (const renderArgs of resolveRenderArgs(
this.router,
this.props,
)) {
if (!this.mounted || this.props.match !== pendingMatch) {
return;
}
Expand All @@ -130,7 +134,10 @@ export default function createBaseRouter({
this.props.resolvedMatch !== pendingMatch
);

this.setState({ element: render(renderArgs) });
this.setState({
renderArgs,
element: render(renderArgs),
});

if (this.pendingResolvedMatch) {
// If this is a new match, update the store, so we can rerender at
Expand All @@ -142,7 +149,7 @@ export default function createBaseRouter({
}
} catch (e) {
if (e.isFoundRedirectException) {
this.props.router.replace(e.location);
this.router.replace(e.location);
return;
}

Expand All @@ -151,20 +158,37 @@ export default function createBaseRouter({
}

render() {
const { renderArgs, element } = this.state;

// Don't rerender synchronously if we have another rerender coming. Just
// memoizing the element here doesn't do anything because we're using
// context.
return (
<StaticContainer
shouldUpdate={!this.shouldResolveMatch && !this.pendingResolvedMatch}
>
{this.state.element}
<RouterContext.Provider
value={{
router: this.router,
match: renderArgs,
}}
>
{element}
</RouterContext.Provider>
</StaticContainer>
);
}
}

BaseRouter.propTypes = propTypes;

return BaseRouter;
// FIXME: For some reason, using contextType doesn't work here.
return mapContextToProps(
{
consumers: ReactReduxContext,
mapToProps: ({ store }) => ({ store }),
displayName: 'withStore(BaseRouter)',
},
BaseRouter,
);
}
12 changes: 0 additions & 12 deletions src/createConnectedLink.js

This file was deleted.

35 changes: 16 additions & 19 deletions src/createConnectedRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { connect } from 'react-redux';

import ActionTypes from './ActionTypes';
import createBaseRouter from './createBaseRouter';
import injectRouterProp from './utils/injectRouterProp';

function resolveMatch(match) {
return {
Expand All @@ -15,22 +14,20 @@ export default function createConnectedRouter({
getFound = ({ found }) => found,
...options
}) {
return injectRouterProp(
connect(
state => {
const { match, resolvedMatch } = getFound(state);
return { match, resolvedMatch };
},
{
onResolveMatch: resolveMatch,
},
null,
{
// Don't block context propagation from above. The router should seldom
// be unnecessarily rerendering anyway.
pure: false,
getDisplayName: () => 'ConnectedRouter',
},
)(createBaseRouter(options)),
);
return connect(
state => {
const { match, resolvedMatch } = getFound(state);
return { match, resolvedMatch };
},
{
onResolveMatch: resolveMatch,
},
null,
{
// Don't block context propagation from above. The router should seldom
// be unnecessarily rerendering anyway.
pure: false,
getDisplayName: () => 'ConnectedRouter',
},
)(createBaseRouter(options));
}
27 changes: 0 additions & 27 deletions src/createWithRouter.js

This file was deleted.

4 changes: 2 additions & 2 deletions src/getRenderArgs.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import resolveRenderArgs from './utils/resolveRenderArgs';

export default async function getRenderArgs(props) {
export default async function getRenderArgs(router, props) {
let elements;

for await (elements of resolveRenderArgs(props)) {
for await (elements of resolveRenderArgs(router, props)) {
// Nothing to do here. We just need the last value from the iterable.
}

Expand Down
7 changes: 1 addition & 6 deletions src/getStoreRenderArgs.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,5 @@ export default function getStoreRenderArgs({
const router = createStoreRouterObject(store);
const match = getFound(store.getState()).resolvedMatch;

return getRenderArgs({
router,
match,
matchContext,
resolver,
});
return getRenderArgs(router, { match, matchContext, resolver });
}
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ export ActionTypes from './ActionTypes';
export BaseLink from './BaseLink';
export createBaseRouter from './createBaseRouter';
export createBrowserRouter from './createBrowserRouter';
export createConnectedLink from './createConnectedLink';
export createConnectedRouter from './createConnectedRouter';
export createElements from './createElements';
export createFarceRouter from './createFarceRouter';
export createInitialBrowserRouter from './createInitialBrowserRouter';
export createInitialFarceRouter from './createInitialFarceRouter';
export createMatchEnhancer from './createMatchEnhancer';
export createRender from './createRender';
export createWithRouter from './createWithRouter';
export ElementsRenderer from './ElementsRenderer';
export foundReducer from './foundReducer';
export getRenderArgs from './getRenderArgs';
Expand All @@ -26,4 +24,6 @@ export RedirectException from './RedirectException';
export resolver from './resolver';
export * as ResolverUtils from './ResolverUtils';
export Route from './Route';
export RouterContext from './RouterContext';
export useRouter from './useRouter';
export withRouter from './withRouter';
29 changes: 29 additions & 0 deletions src/server/RouterProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import PropTypes from 'prop-types';
import React from 'react';

import { routerShape } from '../PropTypes';
import RouterContext from '../RouterContext';

const propTypes = {
renderArgs: PropTypes.shape({
router: routerShape.isRequired,
}).isRequired,
children: PropTypes.node,
};

function RouterProvider({ renderArgs, children }) {
return (
<RouterContext.Provider
value={{
router: renderArgs.router,
match: renderArgs,
}}
>
{children}
</RouterContext.Provider>
);
}

RouterProvider.propTypes = propTypes;

export default RouterProvider;

0 comments on commit 2a90dcb

Please sign in to comment.