|
| 1 | +# Migrating from useWebView to Devvit Web |
| 2 | + |
| 3 | +This guide will migrate your legacy webview implementation (using useWebView inside of Blocks) to the official Devvit Web setup. |
| 4 | + |
| 5 | +:::note |
| 6 | +Apps can be partially migrated, you don't need to re-write everything! |
| 7 | +::: |
| 8 | + |
| 9 | +# Before |
| 10 | + |
| 11 | +- Use postMessage for message passing |
| 12 | +- App logic is isomorphic (server/client) in Blocks |
| 13 | +- No client effects available |
| 14 | + |
| 15 | +# After |
| 16 | + |
| 17 | +- No postMessage required |
| 18 | +- Use web native fetch() to server endpoints directly |
| 19 | +- App logic is either on the client, or the server, with clear deliniation |
| 20 | +- Client effects are available directly from web views |
| 21 | + |
| 22 | +## Setting up devvit.json |
| 23 | + |
| 24 | +The first thing you need to do is setup `devvit.json`. |
| 25 | + |
| 26 | +Schema here: https://developers.reddit.com/schema/config-file.v1.json |
| 27 | + |
| 28 | +`devvit.json` supports all capabilities previously available in the `Devvit` singleton, e.g. `Devvit.addCustomPostType()`. For the purposes of this guide, only the post rendering logic will be migrated. |
| 29 | + |
| 30 | +### Understanding entrypoints |
| 31 | + |
| 32 | +Your `devvit.json` must have entrypoints that point to **outputs** of your code. It is assumed that you have installed a bundler or can otherwise prepare static assets to appear in your dist folders. |
| 33 | + |
| 34 | +```js |
| 35 | +{ |
| 36 | + "post": { |
| 37 | + "client": { // The output of your client app, probably /src/webroot |
| 38 | + "dir": "dist/client", |
| 39 | + "entry": "dist/client/index.html" |
| 40 | + } |
| 41 | + }, |
| 42 | + "blocks": { // point to where you export Devvit singleton, probably src/main.tsx |
| 43 | + "entry": "src/devvit/main.tsx" |
| 44 | + }, |
| 45 | + "server": { // new folder which will contain your Node server |
| 46 | + "entry": "dist/server/index.cjs" |
| 47 | + }, |
| 48 | +} |
| 49 | +``` |
| 50 | + |
| 51 | +> You'll notice that the `blocks` entrypoint points to your TypeScript source file (`src/devvit/main.tsx`). This is because the Devvit CLI handles bundling for Blocks automatically. For your `client` and `server` entrypoints, however, you are responsible for bundling your code and pointing to the final output files in your `dist` directory. |
| 52 | +
|
| 53 | +### Building your client and server |
| 54 | + |
| 55 | +The `devvit.json` configuration for `client` and `server` points to files in a `dist` directory. This means you're responsible for building your web and server assets. You can use any bundler you like, such as `vite`. |
| 56 | + |
| 57 | +For example, your `package.json` might include scripts to output your assets to the `dist` folder. |
| 58 | + |
| 59 | +Sample server vite config |
| 60 | + |
| 61 | +```ts title="src/server/vite.config.ts |
| 62 | +import { defineConfig } from 'vite'; |
| 63 | +import { builtinModules } from 'node:module'; |
| 64 | + |
| 65 | +export default defineConfig({ |
| 66 | + ssr: { |
| 67 | + noExternal: true, |
| 68 | + }, |
| 69 | + build: { |
| 70 | + ssr: 'index.ts', |
| 71 | + outDir: '../../dist/server', |
| 72 | + target: 'node22', |
| 73 | + sourcemap: true, |
| 74 | + rollupOptions: { |
| 75 | + external: [...builtinModules], |
| 76 | + |
| 77 | + output: { |
| 78 | + format: 'cjs', |
| 79 | + entryFileNames: 'index.cjs', |
| 80 | + inlineDynamicImports: true, |
| 81 | + }, |
| 82 | + }, |
| 83 | + }, |
| 84 | +}); |
| 85 | +``` |
| 86 | + |
| 87 | +Sample client Vite config (for React) |
| 88 | + |
| 89 | +```ts title="src/client/vite.config.ts |
| 90 | +import { defineConfig } from 'vite'; |
| 91 | +import tailwind from '@tailwindcss/vite'; |
| 92 | +import react from '@vitejs/plugin-react'; |
| 93 | + |
| 94 | +// https://vitejs.dev/config/ |
| 95 | +export default defineConfig({ |
| 96 | + plugins: [react(), tailwind()], |
| 97 | + build: { |
| 98 | + outDir: '../../dist/client', |
| 99 | + sourcemap: true, |
| 100 | + chunkSizeWarningLimit: 1500, |
| 101 | + }, |
| 102 | +}); |
| 103 | +``` |
| 104 | + |
| 105 | +## Setting up your server endpoints |
| 106 | + |
| 107 | +You can use any Node server for your server endpoints. This guide will use [Express](https://expressjs.com/). |
| 108 | + |
| 109 | +1. Install Express |
| 110 | + |
| 111 | +``` |
| 112 | +npm i express |
| 113 | +``` |
| 114 | + |
| 115 | +2. Create a server index file |
| 116 | + |
| 117 | +```ts title='src/server/index.ts' |
| 118 | +import express from 'express'; |
| 119 | +// The `@devvit/server` package provides the tools to create a server, |
| 120 | +// and gives you access to the request context. |
| 121 | +import { createServer, context, getServerPort, redis } from '@devvit/web/server'; |
| 122 | + |
| 123 | +const app = express(); |
| 124 | + |
| 125 | +// Middleware for JSON body parsing |
| 126 | +app.use(express.json()); |
| 127 | +// Middleware for URL-encoded body parsing |
| 128 | +app.use(express.urlencoded({ extended: true })); |
| 129 | +// Middleware for plain text body parsing |
| 130 | +app.use(express.text()); |
| 131 | + |
| 132 | +const router = express.Router(); |
| 133 | + |
| 134 | +// The `context` object is automatically populated with useful information, |
| 135 | +// like the current user's ID. Devvit's services, like redis, are also |
| 136 | +// available via named imports from `@devvit/server`. |
| 137 | +router.get<{ postId: string }, { message: string }>( |
| 138 | + '/api/hello', |
| 139 | + async (_req, res): Promise<void> => { |
| 140 | + const { userId } = context; |
| 141 | + res.status(200).json({ |
| 142 | + message: `Hello ${userId}`, |
| 143 | + }); |
| 144 | + } |
| 145 | +); |
| 146 | + |
| 147 | +router.get('/api/init', async (_req, res): Promise<void> => { |
| 148 | + res.json({ initialState: await redis.get('initialState') }); |
| 149 | +}); |
| 150 | + |
| 151 | +// Use router middleware |
| 152 | +app.use(router); |
| 153 | + |
| 154 | +// Get port from environment variable with fallback |
| 155 | +const port = getServerPort(); |
| 156 | + |
| 157 | +const server = createServer(app); |
| 158 | +server.on('error', (err) => console.error(`server error; ${err.stack}`)); |
| 159 | +server.listen(port, () => console.log(`http://localhost:${port}`)); |
| 160 | +``` |
| 161 | + |
| 162 | +### Calling your server endpoints |
| 163 | + |
| 164 | +Now |
| 165 | + |
| 166 | +Instead of using `postMessage`, your client-side code can now directly fetch the initial state from the `/api/init` endpoint we defined in the server. |
| 167 | + |
| 168 | +```ts title=/src/client/app.ts |
| 169 | +const res = await fetch('/api/init'); |
| 170 | +const data = await res.json(); |
| 171 | +console.log(data.initialState); // Logs the state from Redis |
| 172 | +``` |
| 173 | + |
| 174 | +## Client effects |
| 175 | + |
| 176 | +Previously, client effects were not available to your webview app. You had to pass a custom postMessage and handle that message in Blocks. Now, all client effects are available directly in the web-view through `@devvit/client`. |
| 177 | + |
| 178 | +Before |
| 179 | + |
| 180 | +```ts title=/src/devvit/main.tsx |
| 181 | +const BlocksComponent = () => { |
| 182 | + const wv = useWebView({ |
| 183 | + onMessage: (message) => { |
| 184 | + if (message.type === 'navigate_to') { |
| 185 | + ui.navigateTo(message.data.destination); |
| 186 | + } |
| 187 | + }, |
| 188 | + }); |
| 189 | +}; |
| 190 | +``` |
| 191 | + |
| 192 | +```js title=webroot/app.js |
| 193 | +window.postMessage({ type: 'navigate_to', destination: 'reddit.com' }); |
| 194 | +``` |
| 195 | + |
| 196 | +Now |
| 197 | + |
| 198 | +```ts title=client/app.ts |
| 199 | +import { navigateTo } from '@devvit/web/client'; |
| 200 | + |
| 201 | +navigateTo('reddit.com'); |
| 202 | +``` |
0 commit comments