Skip to content

Commit

Permalink
Added state to .push (#215)
Browse files Browse the repository at this point in the history
* Added state to .push

* Added state to replace and kept state in location

* Added state to Link, replaceTo and pushedTo

* Updated docs

---------

Co-authored-by: Liam Ma <[email protected]>
  • Loading branch information
AlexanderKaran and liamqma authored Sep 14, 2023
1 parent 2e3973f commit 4e1982b
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 74 deletions.
42 changes: 23 additions & 19 deletions docs/api/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Router

The `Router` component should ideally wrap your client app as high up in the tree as possible.
The `Router` component should ideally wrap your client app as high up in the tree as possible.

If you are planning to render your application on the server, we recommend creating a composition boundary between your router and the core of your application, including your `RouteComponent`.

Expand All @@ -25,21 +25,25 @@ import { appRoutes } from './routing';

const resourcesPlugin = createResourcesPlugin({});

<Router history={createBrowserHistory()} routes={appRoutes} plugins={[resourcesPlugin]}>
<Router
history={createBrowserHistory()}
routes={appRoutes}
plugins={[resourcesPlugin]}
>
<App />
</Router>;
```

### Router props

| prop | type | description |
| ----------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `routes` | `Routes[]` | Your application's routes |
| `history` | `History` | The history instance for the router, if omitted memory history will be used (optional but recommended) |
| `plugins` | `Plugin[]` | Plugin allows you to hook into Router API and extra login on route load/prefetch/etc |
| `basePath` | `string` | Base path string that will get prepended to all route paths (optional) |
| `initialRoute` | `Route` | The route your application is initially showing, it's a performance optimisation to avoid route matching cost on initial render(optional) |
| `onPrefetch` | `function(RouterContext)` | Called when prefetch is triggered from a Link (optional) |
| prop | type | description |
| -------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `routes` | `Routes[]` | Your application's routes |
| `history` | `History` | The history instance for the router, if omitted memory history will be used (optional but recommended) |
| `plugins` | `Plugin[]` | Plugin allows you to hook into Router API and extra login on route load/prefetch/etc |
| `basePath` | `string` | Base path string that will get prepended to all route paths (optional) |
| `initialRoute` | `Route` | The route your application is initially showing, it's a performance optimisation to avoid route matching cost on initial render(optional) |
| `onPrefetch` | `function(RouterContext)` | Called when prefetch is triggered from a Link (optional) |

## Resources plugin

Expand Down Expand Up @@ -77,12 +81,11 @@ export const routes = [

### Resources plugin props

| prop | type | description |
| ----------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `context` | `ResourceContext` | Custom contextual data that will be provided to all your resources' `getKey` and `getData` methods (optional) |
| `resourceData` | `ResourceData` | Pre-resolved resource data. When provided, the router will not request resources on mount (optional) |
| `timeout` | `number` | `timout` is used to prevent slow APIs from causing long renders on the server, If a route resource does not return within the specified time then its data and promise will be set to null.(optional) |

| prop | type | description |
| -------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `context` | `ResourceContext` | Custom contextual data that will be provided to all your resources' `getKey` and `getData` methods (optional) |
| `resourceData` | `ResourceData` | Pre-resolved resource data. When provided, the router will not request resources on mount (optional) |
| `timeout` | `number` | `timout` is used to prevent slow APIs from causing long renders on the server, If a route resource does not return within the specified time then its data and promise will be set to null.(optional) |

## MemoryRouter

Expand Down Expand Up @@ -133,6 +136,7 @@ export const LinkExample = ({ href = '/' }) => {
| `params` | `{ [key]: string }` | Used with `to` to generate correct path url |
| `query` | `{ [key]: string }` | Used with `to` to generate correct query string url |
| `prefetch` | `false` or `hover` or `mount` | Used to start prefetching router resources |
| `state` | `unknown` | Allows you to pass state via location |

## Redirect

Expand Down Expand Up @@ -254,10 +258,10 @@ Actions that communicate with the router's routing functionality are exposed saf
By using either of these you will gain access to the following actions

| prop | type | arguments | description |
| --------------- | ---------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `push` | `function` | `path: Href | Location, state?: any` | Calls `history.push` with the supplied args |
| --------------- | ---------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `push` | `function` | `path: Href | Location, state?: any` | Calls `history.push` with the supplied args |
| `pushTo` | `function` | `route: Route, attributes?: { params?: {}, query?: {} }` | Calls `history.push` generating the path from supplied route and attributes |
| `replace` | `function` | `path: Href | Location, state?: any` | Calls `history.replace` with the supplied args |
| `replace` | `function` | `path: Href | Location, state?: any` | Calls `history.replace` with the supplied args |
| `replaceTo` | `function` | `route: Route, attributes?: { params?: {}, query?: {} }` | Calls `history.replace` generating the path from supplied route and attributes |
| `goBack` | `function` | | Goes to the previous route in history |
| `goForward` | `function` | | Goes to the next route in history |
Expand Down
44 changes: 40 additions & 4 deletions docs/api/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { FeedRefresher } from './FeedRefresher';
import { FeedClearance } from './FeedCleaner';

export const Feed = () => {
const { data, loading, error, update, refresh, clear } = useResource(feedResource);
const { data, loading, error, update, refresh, clear } =
useResource(feedResource);

if (error) {
return <Error error={error} />;
Expand Down Expand Up @@ -48,9 +49,10 @@ As well as returning actions that act on the resource (i.e. update and refresh),

Where `-prev-` indicates the field will remain unchanged from any previous state, possibly the inital state.

It is important to note
* The timeout state is essentially a hung loading state, with the difference that `promise = null` and `error != null`. Developers should give priority to `loading` when deciding between loading or error states for their components. Promises/errors should only ever be thrown on the client.
* The `promise` reflects the last operation, either async or explicit update. Update will clear `error`, set `data`. It will also set a `promise` consistent with that `data` so long as no async is `loading`. When `loading` the `promise` will always reflect the future `data` or `error` from the pending async.
It is important to note

- The timeout state is essentially a hung loading state, with the difference that `promise = null` and `error != null`. Developers should give priority to `loading` when deciding between loading or error states for their components. Promises/errors should only ever be thrown on the client.
- The `promise` reflects the last operation, either async or explicit update. Update will clear `error`, set `data`. It will also set a `promise` consistent with that `data` so long as no async is `loading`. When `loading` the `promise` will always reflect the future `data` or `error` from the pending async.

Additionaly `useResource` accepts additional arguments to customise behaviour, like `routerContext`.
Check out [this section](../resources/usage.md) for more details on how to use the `useResource` hook.
Expand All @@ -71,6 +73,40 @@ export const MyRouteComponent = () => {
};
```

You can also use the `location` inside the `routerState` to access state passed via a `Link` or `push` from `routerActions`.

```js
const StartPage = () => {
const [routerState, routerActions] = useRouter();

const handleButtonClick = () => {
const url = '/destination';
const state = { referrer: 'StartPage' };
routerActions.push(url, state);
};

return (
<div>
<h1>Welcome to the Start Page</h1>
<button onClick={handleButtonClick}>Go to Destination</button>
</div>
);
};

const DestinationPage = () => {
const [routerState] = useRouter();

const referrer = routerState.location?.state?.referrer;

return (
<div>
<h1>Welcome to the Destination Page</h1>
{referrer && <p>You came from the {referrer}!</p>}
</div>
);
};
```
## createRouterSelector
If you are worried about `useRouter` re-rendering too much, you can create custom router hooks using selectors that will trigger a re-render only when the selector output changes.
Expand Down
6 changes: 4 additions & 2 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ export type Location = {
pathname: string;
search: string;
hash: string;
state?: unknown;
};

export type BrowserHistory = (
| Omit<History4, 'location' | 'go' | 'createHref' | 'push' | 'replace'>
| Omit<History5, 'location' | 'go' | 'createHref' | 'push' | 'replace'>
) & {
location: Location;
push: (path: string | Location) => void;
replace: (path: string | Location) => void;
push: (path: string | Location, state?: unknown) => void;
replace: (path: string | Location, state?: unknown) => void;
};

export type History = BrowserHistory;
Expand Down Expand Up @@ -135,6 +136,7 @@ export type LinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
params?: MatchParams;
query?: Query;
prefetch?: false | 'hover' | 'mount';
state?: unknown;
};

export type HistoryBlocker = (
Expand Down
10 changes: 5 additions & 5 deletions src/controllers/redirect/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ describe('<Redirect />', () => {
const to = '/cool-page';

expect(() => mountInRouter({ to })).not.toThrow();
expect(MockHistory.push).toHaveBeenCalledWith(to);
expect(MockHistory.push).toHaveBeenCalledWith(to, undefined);
});

it("doesn't break / throw when rendered with location `to` created from string", () => {
const to = '/go-out?search=foo#hash';

expect(() => mountInRouter({ to })).not.toThrow();
expect(MockHistory.push).toHaveBeenCalledWith(to);
expect(MockHistory.push).toHaveBeenCalledWith(to, undefined);
});

it.each([
Expand Down Expand Up @@ -121,7 +121,7 @@ describe('<Redirect />', () => {
"doesn't break / throw when rendered with `to` as a Route object, %s",
(_, to, params, query, expected) => {
expect(() => mountInRouter({ to, params, query })).not.toThrow();
expect(MockHistory.push).toHaveBeenCalledWith(expected);
expect(MockHistory.push).toHaveBeenCalledWith(expected, undefined);
}
);

Expand All @@ -132,7 +132,7 @@ describe('<Redirect />', () => {
'should navigate to given route %s correctly',
(_, to, params, query, expected) => {
mountInRouter({ to, query, params, push: false });
expect(MockHistory.replace).toHaveBeenCalledWith(expected);
expect(MockHistory.replace).toHaveBeenCalledWith(expected, undefined);
expect(MockHistory.push).not.toHaveBeenCalled();
}
);
Expand Down Expand Up @@ -168,7 +168,7 @@ describe('<Redirect />', () => {
'should use push history correctly with given route %s',
(_, to, params, expected) => {
mountInRouter({ to, params, push: true });
expect(MockHistory.push).toHaveBeenCalledWith(expected);
expect(MockHistory.push).toHaveBeenCalledWith(expected, undefined);
expect(MockHistory.replace).not.toHaveBeenCalled();
}
);
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/router-actions/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ describe('<RouterActions />', () => {
</Router>
);

expect(HistoryMock.push).toBeCalledWith('push');
expect(HistoryMock.replace).toBeCalledWith('replace');
expect(HistoryMock.push).toBeCalledWith('push', undefined);
expect(HistoryMock.replace).toBeCalledWith('replace', undefined);
expect(HistoryMock.goBack).toBeCalled();
expect(HistoryMock.goForward).toBeCalled();
expect(HistoryMock.block).toHaveBeenCalledWith(blockCallback);
Expand Down
12 changes: 6 additions & 6 deletions src/controllers/router-store/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,13 @@ const actions: AllRouterActions = {
},

push:
path =>
(path, state) =>
({ getState }) => {
const { history, basePath } = getState();
if (isExternalAbsolutePath(path)) {
window.location.assign(path as string);
} else {
history.push(getRelativePath(path, basePath));
history.push(getRelativePath(path, basePath), state);
}
},

Expand All @@ -198,17 +198,17 @@ const actions: AllRouterActions = {
attributes.query,
basePath
);
history.push(location as any);
history.push(location as any, attributes?.state);
},

replace:
path =>
(path, state) =>
({ getState }) => {
const { history, basePath } = getState();
if (isExternalAbsolutePath(path)) {
window.location.replace(path as string);
} else {
history.replace(getRelativePath(path, basePath) as any);
history.replace(getRelativePath(path, basePath) as any, state);
}
},

Expand All @@ -226,7 +226,7 @@ const actions: AllRouterActions = {
attributes.query,
basePath
);
history.replace(location as any);
history.replace(location as any, attributes?.state);
},

goBack:
Expand Down
Loading

0 comments on commit 4e1982b

Please sign in to comment.