Skip to content

Commit

Permalink
feat: mainly preventing unwanted loading indicators for table pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
qinsong77 committed Oct 25, 2024
1 parent 5c39d3c commit ee1ec25
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 96 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,27 @@ This is a [Next.js](https://nextjs.org/) 14 Boilerplate project base on [`create
- Update to Next.js 15
- How to test, the test strategy/architecture with RSC
- in [table pagination demo](./app/pagination-demo/page.tsx), Suspense fallback will cover table pagination and header when paginate on client, how to show them when request on client
- Fixed by using [useTransition](https://19.react.dev/reference/react/useTransition), refer: [Preventing unwanted loading indicators ](https://19.react.dev/reference/react/useTransition#preventing-unwanted-loading-indicators)

## Best Practices

- `server-only`, [Keeping Server-only Code out of the Client Environment](https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment)

## Libraries

- [nuqs](https://github.com/47ng/nuqs) Type-safe search params state manager for Next.js - Like React.useState, but stored in the URL query string.
- [next-safe-action](https://github.com/TheEdoRan/next-safe-action) Type safe and validated Server Actions in your Next.js project.

## Practices Refers

- [next authentication](https://www.robinwieruch.de/next-authentication/)

## Know issues

- [x] `eslint-plugin-vitest` can't updated, otherwise eslint will be broken. => `'plugin:prettier/recommended',`
- Standalone building output can't run if copy it's folder, cause pnpm `symlink`, node_module cant resolve correctly. It can be avoided by installing the package with `node-linker=hoisted` in the pnpm configuration before standalone output.

## Refers:
## Refers

- [Next.js App Router Playground](https://github.com/vercel/app-playground)
- [nodejs.org doc web repo](https://github.com/nodejs/nodejs.org/tree/main)
Expand All @@ -64,7 +78,6 @@ This is a [Next.js](https://nextjs.org/) 14 Boilerplate project base on [`create
First, run the development server:

```bash
# or
pnpm dev
# or
pnpm dev:turbo
Expand Down
174 changes: 93 additions & 81 deletions app/line-chart/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import { Separator } from '@/components/ui/separator'

const chartData = [
{ date: '2024-04-01', desktop: 222, mobile: 150 },
Expand Down Expand Up @@ -138,88 +139,99 @@ export default function Component() {
)

return (
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
<CardTitle>Line Chart - Interactive</CardTitle>
<CardDescription>
Showing total visitors for the last 3 months
</CardDescription>
</div>
<div className="flex">
{['desktop', 'mobile'].map((key) => {
const chart = key as keyof typeof chartConfig
return (
<button
key={chart}
data-active={activeChart === chart}
className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
onClick={() => setActiveChart(chart)}
>
<span className="text-xs text-muted-foreground">
{chartConfig[chart].label}
</span>
<span className="text-lg font-bold leading-none sm:text-3xl">
{total[key as keyof typeof total].toLocaleString()}
</span>
</button>
)
})}
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[450px] w-full"
>
<LineChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
<div>
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
<CardTitle>Line Chart - Interactive</CardTitle>
<CardDescription>
Showing total visitors for the last 3 months
</CardDescription>
</div>
<div className="flex">
{['desktop', 'mobile'].map((key) => {
const chart = key as keyof typeof chartConfig
return (
<button
key={chart}
data-active={activeChart === chart}
className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
onClick={() => setActiveChart(chart)}
>
<span className="text-xs text-muted-foreground">
{chartConfig[chart].label}
</span>
<span className="text-lg font-bold leading-none sm:text-3xl">
{total[key as keyof typeof total].toLocaleString()}
</span>
</button>
)
})}
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[450px] w-full"
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value)
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})
<LineChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
/>
<ChartTooltip
content={
<ChartTooltipContent
className="w-[150px]"
nameKey="views"
labelFormatter={(value) => {
return new Date(value).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
}}
/>
}
/>
<Line
dataKey={activeChart}
type="monotone"
stroke={`var(--color-${activeChart})`}
strokeWidth={2}
dot={false}
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value)
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})
}}
/>
<ChartTooltip
content={
<ChartTooltipContent
className="w-[150px]"
nameKey="views"
labelFormatter={(value) => {
return new Date(value).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
}}
/>
}
/>
<Line
dataKey={activeChart}
type="monotone"
stroke={`var(--color-${activeChart})`}
strokeWidth={2}
dot={false}
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
<Separator className="my-8" />
<div className="mb-8 grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-8">
<div className="min-h-20 bg-amber-100 p-4">01</div>
<div className="bg-amber-400 p-4 lg:col-start-2 lg:row-start-1">04</div>
<div className="bg-amber-200 p-4">02</div>
<div className="bg-amber-500 p-4 lg:col-start-2 lg:row-start-2">05</div>
<div className="min-h-48 bg-amber-300 p-4">03</div>
<div className="bg-amber-600 p-4 lg:col-start-2 lg:row-start-3">06</div>
</div>
</div>
)
}
26 changes: 18 additions & 8 deletions app/pagination-demo/_components/table-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
useReactTable,
VisibilityState,
} from '@tanstack/react-table'
import React, { use, useState } from 'react'
import React, { use, useState, useTransition } from 'react'

// todo how to show pagination component when start pagination on client
import { columns } from '@/app/pagination-demo/_components/columns'
Expand All @@ -27,6 +27,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table'
import { logger } from '@/lib/shared'

import { DataTablePagination } from './data-table-pagination'
import { DataTableToolbar } from './data-table-toolbar'
Expand All @@ -36,10 +37,12 @@ type TableDemoProps = {
}

export function TableDemo({ initGetTaskListPromise }: TableDemoProps) {
const [isPending, startTransition] = useTransition()
// todo make it as react context, update the promise in toolbar, eg: when inputting, re-fetch from server with useDeferredValue value?
const [getUserListPromise, setGetUserListPromise] = useState(
initGetTaskListPromise,
)
logger.info({ isPending }, 'TableDemo isPending')

const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
Expand Down Expand Up @@ -67,12 +70,15 @@ export function TableDemo({ initGetTaskListPromise }: TableDemoProps) {
// very important!
manualPagination: true, // turn off client-side pagination
onPaginationChange: (updater) => {
const newPagination =
typeof updater === 'function' ? updater(pagination) : updater
console.log(newPagination)
const { pageIndex, pageSize } = newPagination
setGetUserListPromise(getTaskList({ pageIndex, pageSize }))
setPagination(newPagination)
// !IMPORTANT using [useTransition](https://19.react.dev/reference/react/useTransition), refer: [Preventing unwanted loading indicators ](https://19.react.dev/reference/react/useTransition#preventing-unwanted-loading-indicators)
startTransition(() => {
const newPagination =
typeof updater === 'function' ? updater(pagination) : updater
console.log(newPagination)
const { pageIndex, pageSize } = newPagination
setGetUserListPromise(getTaskList({ pageIndex, pageSize }))
setPagination(newPagination)
})
// const newPagination = updater({ pageIndex: page - 1, pageSize })
// setPage(newPagination.pageIndex + 1)
// setPageSize(newPagination.pageSize)
Expand Down Expand Up @@ -124,7 +130,11 @@ export function TableDemo({ initGetTaskListPromise }: TableDemoProps) {
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
{isPending ? (
<div className="flex size-full min-h-32 items-center justify-center">
<p className="text-xs text-secondary-foreground">loading...</p>
</div>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
Expand Down
4 changes: 2 additions & 2 deletions app/pagination-demo/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export default async function Page() {
<div>
<h1>Table Demo</h1>
<h2>
user will see this immediately, if we didn&apos;t await any function
above
user will see this text immediately, if we didn&apos;t await any
function on Page component.
</h2>
<Separator className="my-4" />
<Suspense fallback={<TableLoading />}>
Expand Down
2 changes: 2 additions & 0 deletions app/task-sequence-progress/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { ExternalLink } from '@/components/site-layout'
import { StepComponent } from './_components/step-component'
import { unsafe_createSequentialProcesses } from './utils'

export const dynamic = 'force-dynamic'

async function firstProcess(id: string) {
return new Promise((resolve) =>
setTimeout(() => resolve(`First process done for ${id}`), 1000),
Expand Down
9 changes: 6 additions & 3 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
"inputs": ["**/*.{ts,tsx}"]
},
"build": {
"dependsOn": ["type-check", "lint", "format", "test"],
"dependsOn": ["type-check", "lint", "format", "test", "^build"],
"outputs": ["dist/**"],
"env": []
},
"lint": {},
"test": {},
"format": {},
"test": {
"inputs": ["**/*.test.ts", "**/*.test.tsx", "!e2e/**"]
},
"test:watch": {
"cache": false,
"persistent": true
Expand All @@ -19,7 +22,7 @@
"dependsOn": ["build"]
},
"e2e": {
"outputs": ["playwright-report/**"]
"inputs": ["e2e/**"]
}
}
}

0 comments on commit ee1ec25

Please sign in to comment.