Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added state to .push #215

Merged
merged 5 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading