Skip to content

Commit

Permalink
docs(suspensive.org): code transition
Browse files Browse the repository at this point in the history
  • Loading branch information
manudeli committed Nov 22, 2024
1 parent 800ba8f commit b5eea21
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 5 deletions.
11 changes: 10 additions & 1 deletion docs/suspensive.org/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { recmaCodeHike, remarkCodeHike } from 'codehike/mdx'
import nextra from 'nextra'
import { remarkSandpack } from 'remark-sandpack'

/** @type {import('codehike/mdx').CodeHikeConfig} */
const chConfig = {
syntaxHighlighting: {
theme: 'github-dark',
},
}

const withNextra = nextra({
autoImportThemeStyle: true,
theme: 'nextra-theme-docs',
themeConfig: './theme.config.tsx',
defaultShowCopyCode: true,
latex: true,
mdxOptions: {
remarkPlugins: [remarkSandpack],
remarkPlugins: [[remarkCodeHike, chConfig], remarkSandpack],
recmaPlugins: [[recmaCodeHike, chConfig]],
rehypePlugins: [],
rehypePrettyCodeOptions: {
theme: 'github-dark-default',
Expand Down
4 changes: 3 additions & 1 deletion docs/suspensive.org/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@suspensive/react-query-4": "workspace:*",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-devtools": "^4.36.1",
"codehike": "^1.0.4",
"d3": "^7.9.0",
"framer-motion": "^11.11.8",
"next": "catalog:",
Expand All @@ -35,7 +36,8 @@
"react": "catalog:react18",
"react-dom": "catalog:react18",
"remark-sandpack": "^0.0.5",
"sharp": "catalog:"
"sharp": "catalog:",
"zod": "^3.23.8"
},
"devDependencies": {
"@suspensive/eslint-config": "workspace:*",
Expand Down
136 changes: 136 additions & 0 deletions docs/suspensive.org/src/components/Scrollycoding.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Block, HighlightedCodeBlock, parseProps } from 'codehike/blocks'
import {
type AnnotationHandler,
type CustomPreProps,
type HighlightedCode,
InnerLine,
InnerPre,
InnerToken,
Pre,
getPreRef,
} from 'codehike/code'
import {
Selectable,
Selection,
SelectionProvider,
} from 'codehike/utils/selection'
import {
type TokenTransitionsSnapshot,
calculateTransitions,
getStartingSnapshot,
} from 'codehike/utils/token-transitions'
import { Component, type RefObject } from 'react'
import { z } from 'zod'

const MAX_TRANSITION_DURATION = 900 // milliseconds

export class SmoothPre extends Component<CustomPreProps> {
ref: RefObject<HTMLPreElement>
constructor(props: CustomPreProps) {
super(props)
this.ref = getPreRef(this.props)
}

render() {
return <InnerPre merge={this.props} style={{ position: 'relative' }} />
}

getSnapshotBeforeUpdate() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return getStartingSnapshot(this.ref.current!)
}

componentDidUpdate(
prevProps: never,
prevState: never,
snapshot: TokenTransitionsSnapshot
) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const transitions = calculateTransitions(this.ref.current!, snapshot)
transitions.forEach(({ element, keyframes, options }) => {
const { translateX, translateY, ...kf } = keyframes as any
if (translateX && translateY) {
kf.translate = [
`${translateX[0]}px ${translateY[0]}px`,
`${translateX[1]}px ${translateY[1]}px`,
]
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
element.animate(kf, {
duration: options.duration * MAX_TRANSITION_DURATION,
delay: options.delay * MAX_TRANSITION_DURATION,
easing: options.easing,
fill: 'both',
})
})
}
}

const Schema = Block.extend({
steps: z.array(Block.extend({ code: HighlightedCodeBlock })),
})

export function Scrollycoding(props: unknown) {
const { steps } = parseProps(props, Schema)
return (
<SelectionProvider className="my-4 flex gap-4">
<div className="mb-[40vh] ml-2 max-w-xl flex-1">
{steps.map((step, i) => (
<Selectable
key={i}
index={i}
selectOn={['click', 'scroll']}
className="mb-56 cursor-pointer px-5 py-2 opacity-30 blur-lg transition data-[selected=true]:opacity-100 data-[selected=true]:blur-none"
>
<h2 className="mb-4 mt-4 text-2xl font-bold">
{i + 1}. {step.title}
</h2>
<div className="opacity-90">{step.children}</div>
</Selectable>
))}
</div>

<div className="flex-1 rounded-xl bg-[#191919]">
<div className="sticky top-16 overflow-auto">
<Selection
from={steps.map((step) => (
// eslint-disable-next-line @eslint-react/no-duplicate-key
<Code key="this key should be same" codeblock={step.code} />
))}
/>
</div>
</div>
</SelectionProvider>
)
}

const tokenTransitions: AnnotationHandler = {
name: 'token-transitions',
PreWithRef: SmoothPre,
Token: (props) => (
<InnerToken merge={props} style={{ display: 'inline-block' }} />
),
}
const wordWrap: AnnotationHandler = {
name: 'word-wrap',
Pre: (props) => <InnerPre merge={props} className="whitespace-pre-wrap" />,
Line: (props) => (
<InnerLine
merge={props}
style={{
textIndent: `${-props.indentation}ch`,
marginLeft: `${props.indentation}ch`,
}}
/>
),
Token: (props) => <InnerToken merge={props} style={{ textIndent: 0 }} />,
}
function Code({ codeblock }: { codeblock: HighlightedCode }) {
return (
<Pre
code={codeblock}
handlers={[tokenTransitions, wordWrap]}
className="min-h-[40rem] p-6"
/>
)
}
1 change: 1 addition & 0 deletions docs/suspensive.org/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { Callout } from './Callout'
export { HomePage } from './HomePage'
export { Sandpack } from './Sandpack'
export { BubbleChart } from './BubbleChart'
export { Scrollycoding } from './Scrollycoding'
170 changes: 168 additions & 2 deletions docs/suspensive.org/src/pages/en/index.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { HomePage } from '@/components'
import { HomePage, Scrollycoding } from '@/components'

<HomePage
title="Suspensive"
version={2}
description="Packages to use React Suspense easily"
description="All in one for React Suspense"
buttonText="Get Started"
items={[
{
Expand All @@ -20,3 +20,169 @@ import { HomePage } from '@/components'
},
]}
/>

<Scrollycoding>

# !!steps 대표적인 라이브러리인 TanStack Query로 Suspense 없이 코드를 작성한다면 이렇게 작성합니다.

이 경우 isLoading과 isError를 체크하여 로딩과 에러 상태를 처리하고 타입스크립트적으로 data에서 undefined를 제거할 수 있습니다.

```jsx ! Page.jsx
const Page = () => {
const userQuery = useQuery(userQueryOptions())
const postsQuery = useQuery({
...postsQueryOptions(),
select: (posts) => posts.filter(({ isPublic }) => isPublic),
})
const promotionsQuery = useQuery(promotionsQueryOptions())

if (
userQuery.isLoading ||
postsQuery.isLoading ||
promotionsQuery.isLoading
) {
return 'loading...'
}

if (userQuery.isError || postsQuery.isError || promotionsQuery.isError) {
return 'error'
}

return (
<Fragment>
<UserProfile {...userQuery.data} />
{postsQuery.data.map((post) => (
<PostListItem key={post.id} {...post} />
))}
{promotionsQuery.data.map((promotion) => (
<Promotion key={promotion.id} {...promotion} />
))}
</Fragment>
)
}
```

# !!steps 그런데 만약 조회해야 할 api가 더 많아진다고 가정해봅시다.

조회해야 하는 api가 더 많아진다면 이 로딩상태와 에러상태를 처리하는 코드가 더욱 복잡해집니다.

```jsx ! Page.jsx
const Page = () => {
const userQuery = useQuery(userQueryOptions())
const postsQuery = useQuery({
...postsQueryOptions(),
select: (posts) => posts.filter(({ isPublic }) => isPublic),
})
const promotionsQuery = useQuery(promotionsQueryOptions())
// 이 부분이 늘어날수록 코드가 복잡해집니다.

if (
userQuery.isLoading ||
postsQuery.isLoading ||
promotionsQuery.isLoading // 이 부분에서 매번 추가해야 합니다.
) {
return 'loading...'
}

if (
userQuery.isError ||
postsQuery.isError ||
promotionsQuery.isError // 이 부분에서 매번 추가해야 합니다.
) {
return 'error'
}

return (
<Fragment>
<UserProfile {...userQuery.data} />
{postsQuery.data.map((post) => (
<PostListItem key={post.id} {...post} />
))}
{promotionsQuery.data.map((promotion) => (
<Promotion key={promotion.id} {...promotion} />
))}
{/* 이 부분에서 매번 추가해야 합니다. */}
</Fragment>
)
}
```

# !!steps Suspense를 사용하면 타입적으로 코드가 간결해집니다. 하지만 컴포넌트의 깊이는 깊어질 수 밖에 없습니다.

useSuspenseQuery는 Suspense와 ErrorBoundary를 사용하여 외부에서 로딩과 에러 상태를 처리할 수 있습니다.
하지만 useSuspenseQuery는 hook이기 때문에 부모에 Suspense와 ErrorBoundary를 두기 위해 컴포넌트가 분리되어야만 하기 때문에 뎁스가 깊어지는 문제가 있습니다.

```jsx ! Page.jsx
const Page = () => (
<ErrorBoundary fallback="error">
<Suspense fallback="loading...">
<UserInfo userId={userId} />
<PostList userId={userId} />
<PromotionList userId={userId} />
</Suspense>
</ErrorBoundary>
)

const UserInfo = ({ userId }) => {
const { data: user } = useSuspenseQuery(userQueryOptions())
return <UserProfile {...user} />
}

const PostList = ({ userId }) => {
const { data: posts } = useSuspenseQuery({
...postsQueryOptions(),
select: (posts) => posts.filter(({ isPublic }) => isPublic),
})
return posts.map((post) => <PostListItem key={post.id} {...post} />)
}

const PromotionList = ({ userId }) => {
const { data: promotions } = useSuspenseQuery(promotionsQueryOptions())
return promotions.map((promotion) => (
<PromotionListItem key={promotion.id} {...promotion} />
))
}
```

# !!steps 이 경우 Suspensive에서 제공하는 SuspenseQuery를 사용하면 이렇게 같은 뎁스에서 hook의 제약을 피해 쉽게 코드를 작성할 수 있습니다.

1. SuspenseQuery를 사용하면 depth를 제거할 수 있습니다.
2. UserInfo라는 컴포넌트를 제거하고 UserProfile과 같은 Presentational 컴포넌트만 남으므로 테스트하기 쉬워집니다.

```jsx ! Page.jsx
const Page = () => (
<ErrorBoundary fallback="error">
<Suspense fallback="loading...">
<SuspenseQuery {...userQueryOptions()}>
{({ data: user }) => <UserProfile key={user.id} {...user} />}
</SuspenseQuery>
<SuspenseQuery
{...postsQueryOptions()}
select={(posts) => posts.filter(({ isPublic }) => isPublic)}
>
{({ data: posts }) =>
posts.map((post) => <PostListItem key={post.id} {...post} />)
}
</SuspenseQuery>
<SuspenseQuery
{...promotionsQueryOptions()}
select={(promotions) => promotions.filter(({ isPublic }) => isPublic)}
>
{({ data: promotions }) =>
promotions.map((promotion) => (
<PromotionListItem key={promotion.id} {...promotion} />
))
}
</SuspenseQuery>
</Suspense>
</ErrorBoundary>
)
```

</Scrollycoding>

# 이것이 우리가 Suspensive를 만드는 이유입니다.

더 쉬운 React Suspense를 사용하세요

## ErrorBoundaryGroup
Loading

0 comments on commit b5eea21

Please sign in to comment.