Skip to content

Instantly share code, notes, and snippets.

@Exifers
Created May 13, 2024 16:10
Show Gist options
  • Save Exifers/ecfb67e0a4022c22de4c68bd80cee5f5 to your computer and use it in GitHub Desktop.
Save Exifers/ecfb67e0a4022c22de4c68bd80cee5f5 to your computer and use it in GitHub Desktop.
React createStateProvider utility function
import React, {
createContext,
forwardRef,
useContext,
useMemo,
type ForwardedRef,
type ReactNode,
isValidElement,
} from "react"
import { RefUtil } from "./RefUtil.ts"
import { assert } from "typescript-utils"
export function createStateProvider<T>() {
interface StateProviderContext<T> {
value: T
setValue: (value: T) => void
}
const StateProviderContext = createContext<StateProviderContext<T> | null>(null)
type StateProviderProps<T> =
| {
value: T
setValue: (value: T) => void
children: ReactNode | ((value: T, setValue: (value: T) => void) => ReactNode)
}
| {
value: T
children: ReactNode | ((value: T) => ReactNode)
}
| {
initialValue: T
children: ReactNode | ((value: T, setValue: (value: T) => void) => ReactNode)
}
function StateProvider({ children, ...props }: StateProviderProps<T>, ref: ForwardedRef<HTMLElement>) {
let value: T
let setValue: ((value: T) => void) | undefined = undefined
let actual: ReactNode
if ("setValue" in props) {
value = props.value
setValue = props.setValue
actual = typeof children === "function" ? children(value, setValue) : children
} else if ("initialValue" in props) {
// eslint-disable-next-line react-hooks/rules-of-hooks
;[value, setValue] = React.useState(props.initialValue)
actual = typeof children === "function" ? children(value, setValue) : children
} else {
value = props.value
setValue = undefined
// @ts-expect-error control flow analysis limitation
actual = typeof children === "function" ? children(value) : children
}
if (isValidElement(actual)) {
// @ts-expect-error undefined at runtime needed to extract rest
const { value, setValue, initialValue, ...rest } = props
actual = RefUtil.cloneElement(actual, [ref], rest)
}
return (
// @ts-expect-error complex
<StateProviderContext.Provider value={useMemo(() => ({ value, setValue }), [value, setValue])}>
{actual}
</StateProviderContext.Provider>
)
}
function useState() {
const context = useContext(StateProviderContext)
assert(context, "missing provider")
return [context.value, context.setValue] as const
}
return [forwardRef(StateProvider), useState] as const
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment