diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c4892dfa85..75f66f3dcb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -11,18 +11,21 @@ body: Do you need some help? ====================== - The issue tracker is meant for feature requests and bug reports only. This isn't the best place for - support or usage questions. Questions here don't have as much visibility as they do elsewhere. Before - you ask a question, here are some resources to get help first: + The issue tracker is meant for bug reports only. This isn't the best + place for support or usage questions. Questions here don't have as much + visibility as they do elsewhere. Before you ask a question, here are + some resources to get help first: - Read the docs: https://reactrouter.com - Check out the list of frequently asked questions: https://reactrouter.com/start/faq - Explore examples: https://reactrouter.com/start/examples + - Ask in chat: https://rmx.as/discord - Look for/ask questions on Stack Overflow: https://stackoverflow.com/questions/tagged/react-router - - Ask in chat: https://discord.gg/6RyV8n8yyM - ### Test Case Starter: - https://stackblitz.com/github/remix-run/react-router/tree/main/examples/basic?file=src/App.tsx + ### Test Case Starters: + + * (Using ``)[https://stackblitz.com/github/remix-run/react-router/tree/main/examples/data-router?file=src/App.tsx] + * (Using ``)[https://stackblitz.com/github/remix-run/react-router/tree/main/examples/basic?file=src/App.tsx] - type: input attributes: label: What version of React Router are you using? diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d1bc9ec6a6..30c6cfa323 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,20 @@ blank_issues_enabled: false contact_links: - - name: 🤔 Support/Usage Question + - name: 💡 Feature Request + url: https://github.com/remix-run/react-router/discussions/new?category=proposals + about: + If you've got an idea for a new feature in React Router, please open a new + Discussion with the `Proposals` label. We'll review the most popular + proposals on a regular basis. Read more about our Open Development process + [here](https://remix.run/blog/open-development). + - name: 🤔 Usage Question (Stack Overflow) url: https://stackoverflow.com/questions/tagged/react-router - about: This is a bug tracker, not a support system. For usage questions, please use Stack Overflow where there are a lot more people ready to help you out. Thanks! + about: Open a question in Stack Overflow with the react-router label + - name: 🤔 Usage Question (Github Discussions) + url: https://github.com/remix-run/remix/discussions/new?category=q-a + about: Open a Discussion in GitHub wih the `Q&A` label + - name: 💬 Remix Discord Channel + url: https://rmx.as/discord + about: Interact with other people using React Router and Remix 📀. There's + plenty of channels for general discussions as well as a threaded `#help` + channel in here where you can ask for help with you issue. diff --git a/.github/ISSUE_TEMPLATE/documentation_isse.yml b/.github/ISSUE_TEMPLATE/documentation_isse.yml new file mode 100644 index 0000000000..c9473a021e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_isse.yml @@ -0,0 +1,21 @@ +name: 📚 Documentation Issue +description: Something is wrong with the React Router docs. +title: "[Docs]: " +labels: + - docs +body: + - type: markdown + attributes: + value: | + Thank you for contributing! + + For documentation updates - we would happily accept PRs, so feel free + to update and open a PR to the `main` branch. Otherwise let us know + in this issue what you felt was missing or incorrect. + + - type: textarea + attributes: + label: Describe what's incorrect/missing in the documentation + description: A concise description of what you expected to see in the docs + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index 960c34af47..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: 💡 Feature Request -description: I want to add something to React Router. -title: "[Feature]: " -labels: - - feature -body: - - type: markdown - attributes: - value: | - Thank you for contributing! - - Do you need some help? - ====================== - The issue tracker is meant for feature requests and bug reports only. This isn't the best place for - support or usage questions. Questions here don't have as much visibility as they do elsewhere. Before - you ask a question, here are some resources to get help first: - - - Read the docs: https://reactrouter.com - - Check out the list of frequently asked questions: https://reactrouter.com/start/faq - - Explore examples: https://reactrouter.com/start/examples - - Look for/ask questions on Stack Overflow: https://stackoverflow.com/questions/tagged/react-router - - Ask in chat: https://discord.gg/6RyV8n8yyM - - type: textarea - attributes: - label: What is the new or updated feature that you are suggesting? - validations: - required: true - - type: textarea - attributes: - label: Why should this feature be included? - validations: - required: true diff --git a/examples/data-router/src/app.tsx b/examples/data-router/src/app.tsx index 93c4f399c9..d6e9a89534 100644 --- a/examples/data-router/src/app.tsx +++ b/examples/data-router/src/app.tsx @@ -1,50 +1,35 @@ import React from "react"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router-dom"; import { + Await, createBrowserRouter, createRoutesFromElements, + defer, + Form, + Link, + Outlet, Route, RouterProvider, + useAsyncError, + useAsyncValue, + useFetcher, + useFetchers, + useLoaderData, + useNavigation, + useParams, + useRevalidator, + useRouteError, } from "react-router-dom"; -import { - Fallback, - Layout, - homeLoader, - Home, - deferredLoader, - DeferredPage, - deferredChildLoader, - deferredChildAction, - DeferredChild, - todosAction, - todosLoader, - TodosList, - TodosBoundary, - todoLoader, - Todo, - sleep, - AwaitPage, -} from "./routes"; +import type { Todos } from "./todos"; +import { addTodo, deleteTodo, getTodos } from "./todos"; + import "./index.css"; let router = createBrowserRouter( createRoutesFromElements( }> } /> - }> - } - /> - - } /> - sleep(3000)} - element={

👋

} - /> } /> + } + /> ) ); @@ -65,3 +55,335 @@ if (import.meta.hot) { export default function App() { return } />; } + +export function sleep(n: number = 500) { + return new Promise((r) => setTimeout(r, n)); +} + +export function Fallback() { + return

Performing initial data load

; +} + +// Layout +export function Layout() { + let navigation = useNavigation(); + let revalidator = useRevalidator(); + let fetchers = useFetchers(); + let fetcherInProgress = fetchers.some((f) => + ["loading", "submitting"].includes(f.state) + ); + return ( + <> +

Data Router Example

+ +

+ This example demonstrates some of the core features of React Router + including nested <Route>s, <Outlet>s, <Link>s, and + using a "*" route (aka "splat route") to render a "not found" page when + someone visits an unrecognized URL. +

+ + +
+ {navigation.state !== "idle" &&

Navigation in progress...

} + {revalidator.state !== "idle" &&

Revalidation in progress...

} + {fetcherInProgress &&

Fetcher in progress...

} +
+

+ Click on over to /todos and check out these + data loading APIs! +

+

+ Or, checkout /deferred to see how to + separate critical and lazily loaded data in your loaders. +

+

+ We've introduced some fake async-aspects of routing here, so Keep an eye + on the top-right hand corner to see when we're actively navigating. +

+
+ + + ); +} + +// Home +interface HomeLoaderData { + date: string; +} + +export async function homeLoader(): Promise { + await sleep(); + return { + date: new Date().toISOString(), + }; +} + +export function Home() { + let data = useLoaderData() as HomeLoaderData; + return ( + <> +

Home

+

Date from loader: {data.date}

+ + ); +} + +// Todos +export async function todosAction({ request }: ActionFunctionArgs) { + await sleep(); + + let formData = await request.formData(); + + // Deletion via fetcher + if (formData.get("action") === "delete") { + let id = formData.get("todoId"); + if (typeof id === "string") { + deleteTodo(id); + return { ok: true }; + } + } + + // Addition via
+ let todo = formData.get("todo"); + if (typeof todo === "string") { + addTodo(todo); + } + + return new Response(null, { + status: 302, + headers: { Location: "/todos" }, + }); +} + +export async function todosLoader(): Promise { + await sleep(); + return getTodos(); +} + +export function TodosList() { + let todos = useLoaderData() as Todos; + let navigation = useNavigation(); + let formRef = React.useRef(null); + + // If we add and then we delete - this will keep isAdding=true until the + // fetcher completes it's revalidation + let [isAdding, setIsAdding] = React.useState(false); + React.useEffect(() => { + if (navigation.formData?.get("action") === "add") { + setIsAdding(true); + } else if (navigation.state === "idle") { + setIsAdding(false); + formRef.current?.reset(); + } + }, [navigation]); + + return ( + <> +

Todos

+

+ This todo app uses a <Form> to submit new todos and a + <fetcher.form> to delete todos. Click on a todo item to navigate + to the /todos/:id route. +

+
    +
  • + + Click this link to force an error in the loader + +
  • + {Object.entries(todos).map(([id, todo]) => ( +
  • + +
  • + ))} +
+ + + + + + + + ); +} + +export function TodosBoundary() { + let error = useRouteError() as Error; + return ( + <> +

Error 💥

+

{error.message}

+ + ); +} + +interface TodoItemProps { + id: string; + todo: string; +} + +export function TodoItem({ id, todo }: TodoItemProps) { + let fetcher = useFetcher(); + + let isDeleting = fetcher.formData != null; + return ( + <> + {todo} +   + + + + + + ); +} + +// Todo +export async function todoLoader({ + params, +}: LoaderFunctionArgs): Promise { + await sleep(); + let todos = getTodos(); + if (!params.id) { + throw new Error("Expected params.id"); + } + let todo = todos[params.id]; + if (!todo) { + throw new Error(`Uh oh, I couldn't find a todo with id "${params.id}"`); + } + return todo; +} + +export function Todo() { + let params = useParams(); + let todo = useLoaderData() as string; + return ( + <> +

Nested Todo Route:

+

id: {params.id}

+

todo: {todo}

+ + ); +} + +// Deferred Data +interface DeferredRouteLoaderData { + critical1: string; + critical2: string; + lazyResolved: Promise; + lazy1: Promise; + lazy2: Promise; + lazy3: Promise; + lazyError: Promise; +} + +const rand = () => Math.round(Math.random() * 100); +const resolve = (d: string, ms: number) => + new Promise((r) => setTimeout(() => r(`${d} - ${rand()}`), ms)); +const reject = (d: Error | string, ms: number) => + new Promise((_, r) => + setTimeout(() => { + if (d instanceof Error) { + d.message += ` - ${rand()}`; + } else { + d += ` - ${rand()}`; + } + r(d); + }, ms) + ); + +export async function deferredLoader() { + return defer({ + critical1: await resolve("Critical 1", 250), + critical2: await resolve("Critical 2", 500), + lazyResolved: Promise.resolve("Lazy Data immediately resolved - " + rand()), + lazy1: resolve("Lazy 1", 1000), + lazy2: resolve("Lazy 2", 1500), + lazy3: resolve("Lazy 3", 2000), + lazyError: reject(new Error("Kaboom!"), 2500), + }); +} + +export function DeferredPage() { + let data = useLoaderData() as DeferredRouteLoaderData; + return ( +
+ {/* Critical data renders immediately */} +

{data.critical1}

+

{data.critical2}

+ + {/* Pre-resolved deferred data never triggers the fallback */} + should not see me!

}> + + + +
+ + {/* Deferred data can be rendered using a component + the useAsyncValue() hook */} + loading 1...

}> + + + +
+ + loading 2...

}> + + + +
+ + {/* Or you can bypass the hook and use a render function */} + loading 3...

}> + {(data: string) =>

{data}

}
+
+ + {/* Deferred rejections render using the useAsyncError hook */} + loading (error)...

}> + }> + + +
+
+ ); +} + +function RenderAwaitedData() { + let data = useAsyncValue() as string; + return

{data}

; +} + +function RenderAwaitedError() { + let error = useAsyncError() as Error; + return ( +

+ Error (errorElement)! +
+ {error.message} {error.stack} +

+ ); +} diff --git a/examples/data-router/src/routes.tsx b/examples/data-router/src/routes.tsx deleted file mode 100644 index ce428df118..0000000000 --- a/examples/data-router/src/routes.tsx +++ /dev/null @@ -1,399 +0,0 @@ -import React from "react"; -import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router-dom"; -import { - Await, - Form, - Link, - Outlet, - defer, - useAsyncError, - useAsyncValue, - useFetcher, - useFetchers, - useLoaderData, - useNavigation, - useParams, - useRevalidator, - useRouteError, - json, - useActionData, -} from "react-router-dom"; - -import type { Todos } from "./todos"; -import { addTodo, deleteTodo, getTodos } from "./todos"; - -export function sleep(n: number = 500) { - return new Promise((r) => setTimeout(r, n)); -} - -export function Fallback() { - return

Performing initial data "load"

; -} - -// Layout -export function Layout() { - let navigation = useNavigation(); - let { revalidate } = useRevalidator(); - let fetchers = useFetchers(); - let fetcherInProgress = fetchers.some((f) => - ["loading", "submitting"].includes(f.state) - ); - return ( - <> - -
- {navigation.state !== "idle" &&

Navigation in progress...

} - {fetcherInProgress &&

Fetcher in progress...

} -
-

- Click on over to /todos and check out these - data loading APIs!{" "} -

-

- Or, checkout /deferred to see how to - separate critical and lazily loaded data in your loaders. -

-

- We've introduced some fake async-aspects of routing here, so Keep an eye - on the top-right hand corner to see when we're actively navigating. -

- - - ); -} - -// Home -interface HomeLoaderData { - date: string; -} - -export async function homeLoader(): Promise { - await sleep(); - return { - date: new Date().toISOString(), - }; -} - -export function Home() { - let data = useLoaderData() as HomeLoaderData; - return ( - <> -

Home

-

Last loaded at: {data.date}

- - ); -} - -// Todos -export async function todosAction({ request }: ActionFunctionArgs) { - await sleep(); - - let formData = await request.formData(); - - // Deletion via fetcher - if (formData.get("action") === "delete") { - let id = formData.get("todoId"); - if (typeof id === "string") { - deleteTodo(id); - return { ok: true }; - } - } - - // Addition via
- let todo = formData.get("todo"); - if (typeof todo === "string") { - addTodo(todo); - } - - return new Response(null, { - status: 302, - headers: { Location: "/todos" }, - }); -} - -export async function todosLoader(): Promise { - await sleep(); - return getTodos(); -} - -export function TodosList() { - let todos = useLoaderData() as Todos; - let navigation = useNavigation(); - let formRef = React.useRef(null); - - // If we add and then we delete - this will keep isAdding=true until the - // fetcher completes it's revalidation - let [isAdding, setIsAdding] = React.useState(false); - React.useEffect(() => { - if (navigation.formData?.get("action") === "add") { - setIsAdding(true); - } else if (navigation.state === "idle") { - setIsAdding(false); - formRef.current?.reset(); - } - }, [navigation]); - - return ( - <> -

Todos

-

- This todo app uses a <Form> to submit new todos and a - <fetcher.form> to delete todos. Click on a todo item to navigate - to the /todos/:id route. -

-
    -
  • - - Click this link to force an error in the loader - -
  • - {Object.entries(todos).map(([id, todo]) => ( -
  • - -
  • - ))} -
- - - - - - - - ); -} - -export function TodosBoundary() { - let error = useRouteError() as Error; - return ( - <> -

Error 💥

-

{error.message}

- - ); -} - -interface TodoItemProps { - id: string; - todo: string; -} - -export function TodoItem({ id, todo }: TodoItemProps) { - let fetcher = useFetcher(); - - let isDeleting = fetcher.formData != null; - return ( - <> - {todo} -   - - - - - - ); -} - -// Todo -export async function todoLoader({ - params, -}: LoaderFunctionArgs): Promise { - await sleep(); - let todos = getTodos(); - if (!params.id) { - throw new Error("Expected params.id"); - } - let todo = todos[params.id]; - if (!todo) { - throw new Error(`Uh oh, I couldn't find a todo with id "${params.id}"`); - } - return todo; -} - -export function Todo() { - let params = useParams(); - let todo = useLoaderData() as string; - return ( - <> -

Nested Todo Route:

-

id: {params.id}

-

todo: {todo}

- - ); -} - -interface DeferredRouteLoaderData { - critical1: string; - critical2: string; - lazyResolved: Promise; - lazy1: Promise; - lazy2: Promise; - lazy3: Promise; - lazyError: Promise; -} - -const rand = () => Math.round(Math.random() * 100); -const resolve = (d: string, ms: number) => - new Promise((r) => setTimeout(() => r(`${d} - ${rand()}`), ms)); -const reject = (d: Error | string, ms: number) => - new Promise((_, r) => - setTimeout(() => { - if (d instanceof Error) { - d.message += ` - ${rand()}`; - } else { - d += ` - ${rand()}`; - } - r(d); - }, ms) - ); - -export async function deferredLoader() { - return defer({ - critical1: await resolve("Critical 1", 250), - critical2: await resolve("Critical 2", 500), - lazyResolved: Promise.resolve("Lazy Data immediately resolved - " + rand()), - lazy1: resolve("Lazy 1", 1000), - lazy2: resolve("Lazy 2", 1500), - lazy3: resolve("Lazy 3", 2000), - lazyError: reject(new Error("Kaboom!"), 2500), - }); -} - -export function DeferredPage() { - let data = useLoaderData() as DeferredRouteLoaderData; - return ( -
-

{data.critical1}

-

{data.critical2}

- - should not see me!

}> - - - -
- - loading 1...

}> - - - -
- - loading 2...

}> - - - -
- - loading 3...

}> - {(data: string) =>

{data}

}
-
- - loading (error)...

}> - }> - - -
- - -
- ); -} - -interface DeferredChildLoaderData { - critical: string; - lazy: Promise; -} - -export async function deferredChildLoader() { - return defer({ - critical: await resolve("Critical Child Data", 500), - lazy: resolve("Lazy Child Data", 1000), - }); -} - -export async function deferredChildAction() { - return json({ ok: true }); -} - -export function DeferredChild() { - let data = useLoaderData() as DeferredChildLoaderData; - let actionData = useActionData(); - return ( -
-

{data.critical}

- loading child...

}> - - - -
-
- -
- {actionData ?

Action data:{JSON.stringify(actionData)}

: null} -
- ); -} - -let shouldResolve = true; -let rawPromiseResolver: ((value: unknown) => void) | null; -let rawPromiseRejecter: ((value: unknown) => void) | null; -let rawPromise: Promise = new Promise((r, j) => { - rawPromiseResolver = r; - rawPromiseRejecter = j; -}); - -export function AwaitPage() { - React.useEffect(() => { - setTimeout(() => { - if (shouldResolve) { - rawPromiseResolver?.("Resolved raw promise!"); - } else { - rawPromiseRejecter?.("Rejected raw promise!"); - } - }, 1000); - }, []); - - return ( - Awaiting raw promise

}> - {(data: string) =>

{data}

}
-
- ); -} - -function RenderAwaitedData() { - let data = useAsyncValue() as string; - return

{data}

; -} - -function RenderAwaitedError() { - let error = useAsyncError() as Error; - return ( -

- Error (errorElement)! -
- {error.message} {error.stack} -

- ); -}