Skip to content

Commit

Permalink
Add RootProps implementation and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
desko27 committed Aug 16, 2024
1 parent cb6bc68 commit 632ea8a
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 42 deletions.
42 changes: 37 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,38 @@ Place `Root` once, which is what listens to every single call and renders it. An
> [!WARNING]
> Since it's the source of truth, there can only be one `Root`. Avoid placing it in multiple locations of the React Tree at the same time, an error will be thrown if so.
# Passing Root props

You can also read props from Root, which are separate from the call props. To do that, just add your RootProps type to createCallable and pass them to your Root.

Root props will be available to your component via `call.root` object.

```diff
+ type RootProps = { userName: string }

const Confirm = createCallable<
Props,
Response,
+ RootProps
>(({ call, message }) => (
...
+ Hi {call.root.userName}!
...
))
```

```diff
<Confirm.Root
+ userName='John Doe'
/>
```

You may want to use Root props if you need to:

- Share the same piece of data to every call
- Use something that is availble in Root's parent
- Update your active call components on data changes

# 🦄 Build your thing

Again, this is no way limited to confirmation dialogs. You can build anything!
Expand All @@ -140,11 +172,11 @@ import type { ReactCall } from 'react-call'

Type | Description
--- | ---
ReactCall.Function<Props, Response> | The call() method
ReactCall.Context<Props, Response> | The call prop in UserComponent
ReactCall.Props<Props, Response> | Your props + the call prop
ReactCall.UserComponent<Props, Response> | What is passed to createCallable
ReactCall.Callable<Props, Response> | What createCallable returns
ReactCall.Function<Props?, Response?> | The call() method
ReactCall.Context<Props?, Response?, RootProps?> | The call prop in UserComponent
ReactCall.Props<Props?, Response?, RootProps?> | Your props + the call prop
ReactCall.UserComponent<Props?, Response?, RootProps?> | What is passed to createCallable
ReactCall.Callable<Props?, Response?, RootProps?> | What createCallable returns

# Errors

Expand Down
10 changes: 10 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@
"ignore": ["./package.json"]
},
"overrides": [
{
"include": ["lib/**"],
"linter": {
"rules": {
"complexity": {
"noBannedTypes": "off"
}
}
}
},
{
"include": ["vite.config.ts"],
"linter": {
Expand Down
21 changes: 11 additions & 10 deletions lib/createCallable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ import type {
Callable,
} from './types'

export function createCallable<P = void, R = void>(
UserComponent: UserComponentType<P, R>,
): Callable<P, R> {
let $setStack: PrivateStackStateSetter<P, R> | null = null
export function createCallable<Props = void, Response = void, RootProps = {}>(
UserComponent: UserComponentType<Props, Response, RootProps>,
): Callable<Props, Response, RootProps> {
let $setStack: PrivateStackStateSetter<Props, Response> | null = null
let $nextKey = 0

const call: CallFunction<P, R> = (props) => {
const call: CallFunction<Props, Response> = (props) => {
if ($setStack === null) throw new Error('No <Root> found!')

const key = String($nextKey++)
const promise = Promise.withResolvers<R>()
const promise = Promise.withResolvers<Response>()

const end = (response: R) => {
const end = (response: Response) => {
if ($setStack === null) return
promise.resolve(response)
$setStack((prev) => prev.filter((c) => c.key !== key))
Expand All @@ -31,8 +31,8 @@ export function createCallable<P = void, R = void>(
return promise.promise
}

function Root() {
const [stack, setStack] = useState<PrivateStackState<P, R>>([])
function Root(rootProps: RootProps) {
const [stack, setStack] = useState<PrivateStackState<Props, Response>>([])

useEffect(() => {
if ($setStack !== null)
Expand All @@ -45,7 +45,8 @@ export function createCallable<P = void, R = void>(
}, [])

return stack.map((stackedCall) => {
const { props, ...call } = stackedCall // filter out props from call
const { props, ...callWithoutProps } = stackedCall
const call = { ...callWithoutProps, root: rootProps }
return <UserComponent key={call.key} {...props} call={call} />
})
}
Expand Down
54 changes: 40 additions & 14 deletions lib/createCallable/types.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
export type CallFunction<P, R> = (props: P) => Promise<R>

export interface PrivateCallContext<P, R> {
export interface PrivateCallContext<Props, Response> {
key: string
props: P
end: (response: R) => void
props: Props
end: (response: Response) => void
}
export type CallContext<P, R> = Omit<PrivateCallContext<P, R>, 'props'>
export type PrivateStackState<Props, Response> = PrivateCallContext<
Props,
Response
>[]
export type PrivateStackStateSetter<Props, Response> = React.Dispatch<
React.SetStateAction<PrivateStackState<Props, Response>>
>

/**
* The call() method
*/
export type CallFunction<Props, Response> = (props: Props) => Promise<Response>

export type PropsWithCall<P, R> = P & { call: CallContext<P, R> }
export type UserComponent<P, R> = React.FunctionComponent<PropsWithCall<P, R>>
/**
* The special call prop in UserComponent
*/
export type CallContext<Props, Response, RootProps> = Omit<
PrivateCallContext<Props, Response>,
'props'
> & { root: RootProps }

export type Callable<P, R> = {
Root: React.FunctionComponent
call: CallFunction<P, R>
/**
* User props + the call prop
*/
export type PropsWithCall<Props, Response, RootProps> = Props & {
call: CallContext<Props, Response, RootProps>
}

export type PrivateStackState<P, R> = PrivateCallContext<P, R>[]
export type PrivateStackStateSetter<P, R> = React.Dispatch<
React.SetStateAction<PrivateStackState<P, R>>
/**
* What is passed to createCallable
*/
export type UserComponent<Props, Response, RootProps> = React.FunctionComponent<
PropsWithCall<Props, Response, RootProps>
>

/**
* What createCallable returns
*/
export type Callable<Props, Response, RootProps> = {
Root: React.FunctionComponent<RootProps>
call: CallFunction<Props, Response>
}
31 changes: 18 additions & 13 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@ import { createCallable } from '#lib/main'

type Props = { message: string }
type Response = boolean
type RootProps = { userName: string }

const Modal = createCallable<Props, Response>(({ call, message }) => (
<div>
<p>{message}</p>
<button type="button" onClick={() => call.end(true)}>
Yes
</button>
<button type="button" onClick={() => call.end(false)}>
No
</button>
</div>
))
const Modal = createCallable<Props, Response, RootProps>(
({ call, message }) => (
<div>
<p>
{call.root.userName}, {message}
</p>
<button type="button" onClick={() => call.end(true)}>
Yes
</button>
<button type="button" onClick={() => call.end(false)}>
No
</button>
</div>
),
)

export function App() {
const handleConfirm = async () => {
const result = await Modal.call({ message: 'Do you?' })
const result = await Modal.call({ message: 'are you sure?' })
console.log(`Resolved: ${result}`)
}

Expand All @@ -26,7 +31,7 @@ export function App() {
<button type="button" onClick={handleConfirm}>
Confirm
</button>
<Modal.Root />
<Modal.Root userName="desko27" />
</div>
)
}

0 comments on commit 632ea8a

Please sign in to comment.