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 all 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
1 change: 1 addition & 0 deletions contributors.yml
Expand Up @@ -3,6 +3,7 @@
- hongji00
- JakubDrozd
- jonkoops
- kentcdodds
- kkirsche
- markivancho
- mcansh
Expand Down
75 changes: 74 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,74 @@ 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 lines=[3]
function Parent() {
const [count, setCount] = React.useState(0);
return <Outlet context={[count, setCount]} />;
}
```

```tsx lines=[2]
function Child() {
const [count, setCount] = useOutletContext();
const increment = () => setCount(c => c + 1);
return <button onClick={increment}>{count}</button>;
}
```

If you're using TypeScript, we recommend the parent component provide a custom hook for accessing the context value. This makes it easier for consumers to get nice typings, control consumers, and know who's consuming the context value. Here's a more realistic example:

```tsx filename=src/routes/dashboard.tsx lines=[12,17-19]
import * as React from "react";
import type { User } from "./types";

type ContextType = { user: User | null };

export default 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";

export default 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([
"Mary",
"Jane",
"Michael",
"Bert",
"Winifred",
"George"
]);
}

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>
Mary
</li>
<li>
Jane
</li>
<li>
Michael
</li>
<li>
Bert
</li>
<li>
Winifred
</li>
<li>
George
</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