add useInstance
This commit is contained in:
20
packages/reacord/library/core/instance-context.tsx
Normal file
20
packages/reacord/library/core/instance-context.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { raise } from "../../helpers/raise"
|
||||||
|
import type { ReacordInstance } from "./instance"
|
||||||
|
|
||||||
|
const Context = React.createContext<ReacordInstance | undefined>(undefined)
|
||||||
|
|
||||||
|
export const InstanceProvider = Context.Provider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the associated instance for the current component.
|
||||||
|
*
|
||||||
|
* @category Core
|
||||||
|
* @see https://reacord.fly.dev/guides/use-instance
|
||||||
|
*/
|
||||||
|
export function useInstance(): ReacordInstance {
|
||||||
|
return (
|
||||||
|
React.useContext(Context) ??
|
||||||
|
raise("Could not find instance, was this component rendered via Reacord?")
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
|
import React from "react"
|
||||||
import type { ComponentInteraction } from "../internal/interaction"
|
import type { ComponentInteraction } from "../internal/interaction"
|
||||||
import { reconciler } from "../internal/reconciler.js"
|
import { reconciler } from "../internal/reconciler.js"
|
||||||
import type { Renderer } from "../internal/renderers/renderer"
|
import type { Renderer } from "../internal/renderers/renderer"
|
||||||
import type { ReacordInstance } from "./instance"
|
import type { ReacordInstance } from "./instance"
|
||||||
|
import { InstanceProvider } from "./instance-context"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @category Core
|
* @category Core
|
||||||
@@ -47,13 +49,12 @@ export abstract class Reacord {
|
|||||||
|
|
||||||
const container = reconciler.createContainer(renderer, 0, false, {})
|
const container = reconciler.createContainer(renderer, 0, false, {})
|
||||||
|
|
||||||
if (initialContent !== undefined) {
|
const instance: ReacordInstance = {
|
||||||
reconciler.updateContainer(initialContent, container)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
render: (content: ReactNode) => {
|
render: (content: ReactNode) => {
|
||||||
reconciler.updateContainer(content, container)
|
reconciler.updateContainer(
|
||||||
|
<InstanceProvider value={instance}>{content}</InstanceProvider>,
|
||||||
|
container,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
deactivate: () => {
|
deactivate: () => {
|
||||||
this.deactivate(renderer)
|
this.deactivate(renderer)
|
||||||
@@ -63,6 +64,12 @@ export abstract class Reacord {
|
|||||||
renderer.destroy()
|
renderer.destroy()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (initialContent !== undefined) {
|
||||||
|
instance.render(initialContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
private deactivate(renderer: Renderer) {
|
private deactivate(renderer: Renderer) {
|
||||||
@@ -13,5 +13,6 @@ export * from "./core/components/link"
|
|||||||
export * from "./core/components/option"
|
export * from "./core/components/option"
|
||||||
export * from "./core/components/select"
|
export * from "./core/components/select"
|
||||||
export * from "./core/instance"
|
export * from "./core/instance"
|
||||||
|
export { useInstance } from "./core/instance-context"
|
||||||
export * from "./core/reacord"
|
export * from "./core/reacord"
|
||||||
export * from "./core/reacord-discord-js"
|
export * from "./core/reacord-discord-js"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"test": "vitest --coverage --no-watch",
|
"test": "vitest --coverage --no-watch",
|
||||||
"test-dev": "vitest",
|
"test-dev": "vitest",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"playground": "nodemon --exec esmo --ext ts,tsx ./playground/main.tsx",
|
"playground": "nodemon --exec esmo --ext ts,tsx --inspect=5858 --enable-source-maps ./playground/main.tsx",
|
||||||
"release": "release-it"
|
"release": "release-it"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Client } from "discord.js"
|
import { Client } from "discord.js"
|
||||||
import "dotenv/config"
|
import "dotenv/config"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Button, ReacordDiscordJs } from "../library/main"
|
import { Button, ReacordDiscordJs, useInstance } from "../library/main"
|
||||||
import { createCommandHandler } from "./command-handler"
|
import { createCommandHandler } from "./command-handler"
|
||||||
import { Counter } from "./counter"
|
import { Counter } from "./counter"
|
||||||
import { FruitSelect } from "./fruit-select"
|
import { FruitSelect } from "./fruit-select"
|
||||||
@@ -93,6 +93,17 @@ createCommandHandler(client, [
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "delete-this",
|
||||||
|
description: "delete this",
|
||||||
|
run: (interaction) => {
|
||||||
|
function DeleteThis() {
|
||||||
|
const instance = useInstance()
|
||||||
|
return <Button label="delete this" onClick={() => instance.destroy()} />
|
||||||
|
}
|
||||||
|
reacord.reply(interaction, <DeleteThis />)
|
||||||
|
},
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
await client.login(process.env.TEST_BOT_TOKEN)
|
await client.login(process.env.TEST_BOT_TOKEN)
|
||||||
|
|||||||
72
packages/reacord/test/use-instance.test.tsx
Normal file
72
packages/reacord/test/use-instance.test.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
import type { ReacordInstance } from "../library/main"
|
||||||
|
import { Button, useInstance } from "../library/main"
|
||||||
|
import type { MessageSample } from "./test-adapter"
|
||||||
|
import { ReacordTester } from "./test-adapter"
|
||||||
|
|
||||||
|
describe("useInstance", () => {
|
||||||
|
it("returns the instance of itself", async () => {
|
||||||
|
let instanceFromHook: ReacordInstance | undefined
|
||||||
|
|
||||||
|
function TestComponent({ name }: { name: string }) {
|
||||||
|
const instance = useInstance()
|
||||||
|
instanceFromHook ??= instance
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
label={`create ${name}`}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.reply(<TestComponent name="child" />)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label={`destroy ${name}`}
|
||||||
|
onClick={() => instance.destroy()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageOutput(name: string): MessageSample {
|
||||||
|
return {
|
||||||
|
content: "",
|
||||||
|
embeds: [],
|
||||||
|
actionRows: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
label: `create ${name}`,
|
||||||
|
style: "secondary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
label: `destroy ${name}`,
|
||||||
|
style: "secondary",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tester = new ReacordTester()
|
||||||
|
const instance = tester.send(<TestComponent name="parent" />)
|
||||||
|
|
||||||
|
await tester.assertMessages([messageOutput("parent")])
|
||||||
|
expect(instanceFromHook).toBe(instance)
|
||||||
|
|
||||||
|
tester.findButtonByLabel("create parent").click()
|
||||||
|
await tester.assertMessages([
|
||||||
|
messageOutput("parent"),
|
||||||
|
messageOutput("child"),
|
||||||
|
])
|
||||||
|
|
||||||
|
// this test ensures that the only the child instance is destroyed,
|
||||||
|
// and not the parent instance
|
||||||
|
tester.findButtonByLabel("destroy child").click()
|
||||||
|
await tester.assertMessages([messageOutput("parent")])
|
||||||
|
|
||||||
|
tester.findButtonByLabel("destroy parent").click()
|
||||||
|
await tester.assertMessages([])
|
||||||
|
})
|
||||||
|
})
|
||||||
26
packages/website/app/routes/guides/use-instance.md
Normal file
26
packages/website/app/routes/guides/use-instance.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
meta:
|
||||||
|
title: useInstance
|
||||||
|
description: Using useInstance to get the current instance within a component
|
||||||
|
---
|
||||||
|
|
||||||
|
# useInstance
|
||||||
|
|
||||||
|
You can use `useInstance` to get the current instance within a component. This can be used to let a component destroy or deactivate itself.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { Button, useInstance } from "reacord"
|
||||||
|
|
||||||
|
function SelfDestruct() {
|
||||||
|
const instance = useInstance()
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
style="danger"
|
||||||
|
label="delete this"
|
||||||
|
onClick={() => instance.destroy()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
reacord.send(channelId, <SelfDestruct />)
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user