Skip to content

Commit

Permalink
feat: prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
oosawy committed Feb 12, 2023
1 parent e8bdb63 commit 6294a73
Show file tree
Hide file tree
Showing 6 changed files with 308 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,68 @@
# react-warp
make wormhole in react🐛

Construct Warp Tunnels to warp elements between renders in React🪐

> **Warning**
> This library is still Proof of Concept, and not tested enough. Don't use this in production!
## Motivation

An article is work in progress.

## Install

```sh
npm i @oosawy/react-warp
```

## Usage

1. Add `<WarpProvider />` your app to prepare the warp tunnels.

```tsx
// if you use next.js, _app.tsx is good place to put WarpProvider
import type { AppProps } from 'next/app'
import { WarpProvider } from '@oosawy/react-warp'

export default function MyApp({ Component, pageProps }: AppProps) {
return (
<WarpProvider>
<Component {...pageProps} />
</WarpProvider>
)
}
```

2. Then wrap the element which you want to be warped with `<Warp />`. then the element will be warped to the same `<Warp />` in other renders.
Note: the children are only allowed to have one React element, which must have a `key` prop.

```tsx
const IndexPage = () => {
return (
<div>
<LandingLayout
header={
<Warp>
<input type="text" key="search-input" />
</Warp>
}
>
<h1>The search input warps when you navigate to another page!</h1>
<p>So the text you entered will persist even though its state is not managed.</p>
</LandingLayout>
</div>
)
}

const SearchPage = () => {
return (
<div>
<SearchLayout>
<Warp>
<input type="text" key="search-input" />
</Warp>
</SearchLayout>
</div>
)
}
```
123 changes: 123 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@oosawy/react-warp",
"version": "0.0.1",
"description": "Construct Warp Tunnels in React",
"repository": "oosawy/react-warp",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"LICENSE",
"README.md",
"dist/"
],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^4.9.5"
},
"peerDependencies": {
"react": ">= 16",
"react-dom": ">= 16"
}
}
67 changes: 67 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useState, useRef, useContext, useLayoutEffect } from 'react'
import ReactDOM from 'react-dom'

type WarpManager = {
set(key: string, element: React.ReactElement): HTMLElement
}

const WarpContext = React.createContext<WarpManager | undefined>(undefined)

type Portals = Record<
string,
{ node: HTMLElement; element: React.ReactElement }
>

export const WarpProvider = (props: { children: React.ReactNode }) => {
const [portals, setPortals] = useState<Portals>({})

const manager: WarpManager = {
set(key, element) {
const node = portals[key]?.node ?? document.createElement('span')
setPortals((prev) =>
Object.assign({}, prev, { [key]: { node, element } })
)
return node
},
}

return (
<WarpContext.Provider value={manager}>
{props.children}
<WarpHost portals={portals} />
</WarpContext.Provider>
)
}

const WarpHost = (props: { portals: Portals }) => {
return (
<>
{Object.values(props.portals).map(({ node, element }) =>
ReactDOM.createPortal(element, node)
)}
</>
)
}

export const Warp = (props: { children: React.ReactElement }) => {
const manager = useContext(WarpContext)

if (!manager) {
throw new Error('<Warp /> must used under <WarpProvider />')
}

const ref = useRef<HTMLSpanElement>(null)

const key = React.Children.only(props.children).key

if (!key) {
throw new Error('<Warp /> child must have key prop')
}

useLayoutEffect(() => {
const node = manager.set(key.toString(), props.children)
ref.current?.replaceChildren(node)
}, [props.children, key])

return <span ref={ref} />
}
20 changes: 20 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"include": ["src"],
"exclude": ["dist", "node_modules"],
"compilerOptions": {
"module": "esnext",
"target": "es6",
"lib": ["dom", "esnext"],
"declaration": true,
"sourceMap": true,
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"noImplicitReturns": true,
"moduleResolution": "node",
"jsx": "react-jsx",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}

0 comments on commit 6294a73

Please sign in to comment.