react-router-dom의 useNavigate과 antd message.useMessage의 messageApi가 동시에 동작하지 않는 문제가 있었다. 새 게시글 등록이 완료되면 작성된 게시글 상세 페이지로 이동 시킨 후 "등록이 완료되었습니다"라는 메세지를 띄우려고 했는데, 메세지가 나오지 않았다. 페이지를 이동하면서 기존 페이지 안에 정의해둔 useMessage의 context holder의 범위를 벗어나면서 메세지가 뜨지 못하게 된 것으로 보였다.
Global 범위에 contextHolder를 배치하면 해결되겠지만, messageApi를 어떻게 전달해주면 효율적일지 고민이 되어 방법을 찾아보았다. 결론부터 말하자면
- 전체 route를 감싸는 layout 만들기
- layout에 contextHolder 붙이기, Outlet의 context props로 messageApi 내려주기.
- useOutletContext를 이용하여 messageApi 받아와 사용하기
// index.tsx, import 생략
const rootElement = document.getElementById("root")!;
const root = ReactDOM.createRoot(rootElement);
const router = createBrowserRouter([
{
element: <AppLayout />,
children: [
{ path: "/", element: <App /> },
{ path: "/create", element: <AnotherPage /> }
]
}
]);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
)
// AppLayout.tsx
import { Outlet, useOutletContext } from "react-router-dom";
import { MessageInstance } from "antd/lib/message/interface";
type ContextType = { messageApi: MessageInstance };
export const AppLayout = () => {
const [messageApi, contextHolder] = message.useMessage();
return (
<>
{contextHolder}
<Outlet context={{ messageApi } satisfies ContextType} />
</>
);
};
// for typescript
export const useMessageApi = () => useOutletContext<ContextType>();
// AnotherPage.tsx
import { useNavigate } from "react-router-dom";
import { Button } from "antd";
import { useMessageApi } from "./AppLayout.tsx";
export const AnotherPage = () => {
const { messageApi } = useMessageApi();
const navigate = useNavigate();
return (
<>
<h3>Create Page</h3>
<Button
onClick={() => {
messageApi.success("게시글이 등록되었습니다!");
navigate(-1);
}}
>
등록하기
</Button>
</>
);
};
useMessage가 route의 위치에 영향을 받지 않고 messageApi를 전송할 수 있도록, contextHolder를 모든 route를 감싸는 최상위 Layout route에 배치하였다. Layout route에서는 <Outlet/> 컴포넌트를 통해 자식 route를 Nested UI로 렌더하도록 한다.
이때 Outlet에 context props를 이용하여 원하는 값을 내려보내줄 수 있다. Outlet의 context로 내려준 값은 useOutletContext()를 이용하여 받아올 수 있다.
index.tsx
// ...생략
const router = createBrowserRouter([
{
element: <AppLayout />,
children: [
{ path: "/", element: <App /> },
{ path: "/create", element: <AnotherPage /> }
]
}
]);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
)
AppLayout.tsx
import { Outlet, useOutletContext } from "react-router-dom";
import { MessageInstance } from "antd/lib/message/interface";
export const AppLayout = () => {
const [messageApi, contextHolder] = message.useMessage();
return (
<>
{contextHolder}
<Outlet context={{ messageApi }} />
</>
);
};
AnotherPage.tsx
import { useNavigate, useOutletContext } from "react-router-dom";
import { Button } from "antd";
export const AnotherPage = () => {
const { messageApi } = useOutletContext();
const navigate = useNavigate();
return (
<>
<h3>Create Page</h3>
<Button
onClick={() => {
messageApi.success("게시글이 등록되었습니다!");
navigate(-1);
}}
>
등록하기
</Button>
</>
);
};
그러나 위와 같이 사용하면 useOutletContext를 사용하는 시점에서 타입 오류가 발생한다.

다행이 공식문서에 해당 케이스에 대한 해결방법이 잘 나와있다. context에 타입을 제한해주고, 타입을 포함한 custom hook을 만들어 useOutletContext를 사용하기를 권장하고 있다.
공식문서의 예시를 위의 코드에 적용해보았다.
AppLayout.tsx
import { Outlet, useOutletContext } from "react-router-dom";
import { MessageInstance } from "antd/lib/message/interface";
type ContextType = { messageApi: MessageInstance };
export const AppLayout = () => {
const [messageApi, contextHolder] = message.useMessage();
return (
<>
{contextHolder}
<Outlet context={{ messageApi } satisfies ContextType} />
</>
);
};
// for typescript
export const useMessageApi = () => useOutletContext<ContextType>();
AnotherPage.tsx
import { useNavigate } from "react-router-dom";
import { Button } from "antd";
import { useMessageApi } from "./AppLayout.tsx";
export const AnotherPage = () => {
const { messageApi } = useMessageApi();
const navigate = useNavigate();
return (
<>
<h3>Create Page</h3>
<Button
onClick={() => {
messageApi.success("게시글이 등록되었습니다!");
navigate(-1);
}}
>
등록하기
</Button>
</>
);
};
원래는 redux를 이용하여 messageApi를 전달해주어야 하나 고민했는데, useOutletContext를 이용해 쉽게 해결할 수 있어서 좋았다. 새삼 react-router-dom은 내가 생각하는 것보다 많은 기능을 제공해주는구나 싶었다. 공식문서를 좀 더 꼼꼼이 읽어보아야지.