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

feat: add "context" prop to Outlet #8461

Merged
merged 5 commits into from Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 53 additions & 1 deletion docs/api.md
Expand Up @@ -509,7 +509,12 @@ class LoginForm extends React.Component {
<summary>Type declaration</summary>

```tsx
declare function Outlet(): React.ReactElement | null;
interface OutletProps {
context?: unknown;
}
declare function Outlet(
props: OutletProps
): React.ReactElement | null;
```

</details>
Expand Down Expand Up @@ -545,6 +550,53 @@ function App() {
}
```

### `useOutletContext`

<details>
<summary>Type declaration</summary>

```tsx
declare function useOutletContext<
Context = unknown
>(): Context;
```

</details>

Often parent routes manage state or other values you want shared with child routes. You can create your own [context provider](https://reactjs.org/docs/context.html) if you like, but this is such a common situation that it's built-into `<Outlet />`:

```tsx filename=src/routes/dashboard.tsx lines=[7]
type ContextType = { user: User | null };
function Dashboard() {
const [user, setUser] = React.useState<User | null>(null);

return (
<div>
<h1>Dashboard</h1>
<Outlet context={user} />
</div>
);
}

export function useUser() {
return useOutletContext<ContextType>();
}
```
kentcdodds marked this conversation as resolved.
Show resolved Hide resolved

```tsx filename=src/routes/dashboard/messages.tsx lines=[1,4]
import { useUser } from "../dashboard";

function DashboardMessages() {
const user = useUser();
return (
<div>
<h2>Messages</h2>
<p>Hello, {user.name}!</p>
</div>
);
}
```

### `<Router>`

<details>
Expand Down
6 changes: 4 additions & 2 deletions packages/react-router-dom/index.tsx
Expand Up @@ -23,7 +23,8 @@ import {
useOutlet,
useParams,
useResolvedPath,
useRoutes
useRoutes,
useOutletContext
} from "react-router";
import type { To } from "react-router";

Expand Down Expand Up @@ -71,7 +72,8 @@ export {
useOutlet,
useParams,
useResolvedPath,
useRoutes
useRoutes,
useOutletContext
};

export type {
Expand Down
6 changes: 4 additions & 2 deletions packages/react-router-native/index.tsx
Expand Up @@ -30,7 +30,8 @@ import {
useOutlet,
useParams,
useResolvedPath,
useRoutes
useRoutes,
useOutletContext
} from "react-router";
import type { To } from "react-router";

Expand Down Expand Up @@ -63,7 +64,8 @@ export {
useOutlet,
useParams,
useResolvedPath,
useRoutes
useRoutes,
useOutletContext
};

export type {
Expand Down
120 changes: 119 additions & 1 deletion packages/react-router/__tests__/useOutlet-test.tsx
@@ -1,6 +1,12 @@
import * as React from "react";
import * as TestRenderer from "react-test-renderer";
import { MemoryRouter, Routes, Route, useOutlet } from "react-router";
import {
MemoryRouter,
Routes,
Route,
useOutlet,
useOutletContext
} from "react-router";

describe("useOutlet", () => {
describe("when there is no child route", () => {
Expand Down Expand Up @@ -50,4 +56,116 @@ describe("useOutlet", () => {
`);
});
});

describe("when there is no context", () => {
it("returns null", () => {
function Users() {
return useOutlet();
}

function Profile() {
let outletContext = useOutletContext();

return (
<div>
<h1>Profile</h1>
<pre>{outletContext}</pre>
</div>
);
}

let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/users/profile"]}>
<Routes>
<Route path="users" element={<Users />}>
<Route path="profile" element={<Profile />} />
</Route>
</Routes>
</MemoryRouter>
);
});

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
<h1>
Profile
</h1>
<pre />
</div>
`);
});
});

describe("when there is context", () => {
it("returns the context", () => {
function Users() {
return useOutlet([
"Chance",
"Jacob",
"Kent",
"Logan",
"Michael",
"Ryan"
]);
}

function Profile() {
let outletContext = useOutletContext<string[]>();

return (
<div>
<h1>Profile</h1>
<ul>
{outletContext.map(name => (
<li key={name}>{name}</li>
))}
</ul>
</div>
);
}

let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/users/profile"]}>
<Routes>
<Route path="users" element={<Users />}>
<Route path="profile" element={<Profile />} />
</Route>
</Routes>
</MemoryRouter>
);
});

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
<h1>
Profile
</h1>
<ul>
<li>
Chance
</li>
<li>
Jacob
</li>
<li>
Kent
</li>
<li>
Logan
</li>
<li>
Michael
</li>
<li>
Ryan
</li>
</ul>
</div>
`);
});
});
});
26 changes: 21 additions & 5 deletions packages/react-router/index.tsx
Expand Up @@ -183,15 +183,17 @@ export function Navigate({ to, replace, state }: NavigateProps): null {
return null;
}

export interface OutletProps {}
export interface OutletProps {
context?: unknown;
}

/**
* Renders the child route's element, if there is one.
*
* @see https://reactrouter.com/docs/en/v6/api#outlet
*/
export function Outlet(_props: OutletProps): React.ReactElement | null {
return useOutlet();
export function Outlet(props: OutletProps): React.ReactElement | null {
return useOutlet(props.context);
}

export interface RouteProps {
Expand Down Expand Up @@ -510,14 +512,28 @@ export function useNavigate(): NavigateFunction {
return navigate;
}

const OutletContext = React.createContext<unknown>(null);

/**
* Returns the context (if provided) for the child route at this level of the route
* hierarchy.
* @see https://reactrouter.com/docs/en/v6/api#useoutletcontext
*/
export function useOutletContext<Context = unknown>(): Context {
return React.useContext(OutletContext) as Context;
}

/**
* Returns the element for the child route at this level of the route
* hierarchy. Used internally by <Outlet> to render child routes.
*
* @see https://reactrouter.com/docs/en/v6/api#useoutlet
*/
export function useOutlet(): React.ReactElement | null {
return React.useContext(RouteContext).outlet;
export function useOutlet(context?: unknown): React.ReactElement | null {
let outlet = React.useContext(RouteContext).outlet;
return (
<OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
);
}

/**
Expand Down