├── example ├── .npmignore ├── index.tsx ├── index.html ├── tsconfig.json └── package.json ├── .gitignore ├── .vscode └── settings.json ├── docs └── superjson-remix.png ├── .github └── workflows │ ├── size.yml │ └── main.yml ├── test └── blah.test.tsx ├── LICENSE ├── src └── index.tsx ├── tsconfig.json ├── package.json └── README.md /example/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /docs/superjson-remix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donavon/superjson-remix/HEAD/docs/superjson-remix.png -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import { Thing } from '../.'; 5 | 6 | const App = () => { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | }; 13 | 14 | ReactDOM.render(, document.getElementById('root')); 15 | -------------------------------------------------------------------------------- /test/blah.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Thing } from '../src'; 4 | 5 | describe('it', () => { 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render(, div); 9 | ReactDOM.unmountComponentAtNode(div); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "types": ["node"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "react-app-polyfill": "^1.0.0" 12 | }, 13 | "alias": { 14 | "react": "../node_modules/react", 15 | "react-dom": "../node_modules/react-dom/profiling", 16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.11", 20 | "@types/react-dom": "^16.8.4", 21 | "parcel": "^1.12.3", 22 | "typescript": "^3.4.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Donavon West 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | HtmlMetaDescriptor, 3 | useLoaderData as useRemixLoaderData, 4 | } from '@remix-run/react'; 5 | 6 | import { json as remixJson, MetaFunction } from '@remix-run/node'; 7 | import { serialize, deserialize } from 'superjson'; 8 | import { SuperJSONResult } from 'superjson/dist/types'; 9 | 10 | type JsonResponse = ReturnType; 11 | type MetaArgs = Parameters[0]; 12 | type MetaArgsSansData = Omit; 13 | 14 | type SuperJSONMetaFunction = { 15 | (args: MetaArgsSansData & { data: Data }): HtmlMetaDescriptor; 16 | }; 17 | 18 | export const json = ( 19 | obj: Data, 20 | init?: number | ResponseInit 21 | ): JsonResponse => { 22 | const superJsonResult = serialize(obj); 23 | return remixJson(superJsonResult, init); 24 | }; 25 | 26 | export const parse = (superJsonResult: SuperJSONResult) => 27 | deserialize(superJsonResult) as Data; 28 | 29 | export const withSuperJSON = ( 30 | metaFn: MetaFunction 31 | ): SuperJSONMetaFunction => ({ 32 | data, 33 | ...rest 34 | }: MetaArgs): HtmlMetaDescriptor => 35 | metaFn({ ...rest, data: parse(data) }); 36 | 37 | export const useLoaderData = () => { 38 | const loaderData = useRemixLoaderData(); // HACK: any to avoid type error 39 | return parse(loaderData); 40 | }; 41 | 42 | export const useSuperJSONLoaderData = useLoaderData; 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=16" 12 | }, 13 | "scripts": { 14 | "start": "tsdx watch", 15 | "build": "tsdx build", 16 | "test": "tsdx test --passWithNoTests", 17 | "lint": "tsdx lint", 18 | "prepare": "tsdx build", 19 | "size": "size-limit", 20 | "analyze": "size-limit --why" 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "tsdx lint" 25 | } 26 | }, 27 | "prettier": { 28 | "printWidth": 80, 29 | "semi": true, 30 | "singleQuote": true, 31 | "trailingComma": "es5" 32 | }, 33 | "name": "superjson-remix", 34 | "author": "Donavon West (https://donavon.com/)", 35 | "module": "dist/superjson-remix.esm.js", 36 | "size-limit": [ 37 | { 38 | "path": "dist/superjson-remix.cjs.production.min.js", 39 | "limit": "10 KB" 40 | }, 41 | { 42 | "path": "dist/superjson-remix.esm.js", 43 | "limit": "10 KB" 44 | } 45 | ], 46 | "devDependencies": { 47 | "@remix-run/dev": "^1.12.0", 48 | "@remix-run/react": "^1.12.0", 49 | "@remix-run/serve": "^1.12.0", 50 | "@size-limit/preset-small-lib": "^7.0.8", 51 | "@types/react": "^17.0.40", 52 | "@types/react-dom": "^17.0.13", 53 | "husky": "^7.0.4", 54 | "react": "^17.0.2", 55 | "react-dom": "^17.0.2", 56 | "size-limit": "^7.0.8", 57 | "tsdx": "^0.14.1", 58 | "tslib": "^2.3.1", 59 | "typescript": "^4.9.5" 60 | }, 61 | "dependencies": { 62 | "superjson": "^1.12.2" 63 | }, 64 | "peerDependencies": { 65 | "@remix-run/node": ">=1.12.0", 66 | "@remix-run/react": ">=1.12.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # superjson-remix 2 | 3 |

4 | superjson 5 |

6 | 7 | A solution for Remix that allows you to send binary data from your `loader` function to your React client app. It uses the awesome [`superjson`](https://github.com/blitz-js/superjson) package to serialize/deserialize your data automatically. 8 | 9 | ## Problem 10 | 11 | With the existing Remix approach, only JSON parsable data can be sent to the client. This is a problem for binary data such as `Date`, `Map`, `Set` or `BigInt`. 12 | 13 | Say you have a `loader` function and would like to send this to the client: 14 | 15 | ```ts 16 | import { json } from 'remix'; 17 | 18 | const loader: LoaderFunction = () => { 19 | const obj = { 20 | set: new Set([1, 2, 3]), 21 | now: new Date(), 22 | largeNumber: BigInt(9007199254740991), 23 | }; 24 | 25 | return json(obj); 26 | }; 27 | ``` 28 | 29 | Well you're out of luck! On the client, `Set` will parse to an empty object. 30 | 31 | ``` 32 | set: {}, 33 | ``` 34 | 35 | `Date` will parse as an ISO-8601 string — not the end of the world, but certainly inconvenient. 36 | 37 | ``` 38 | now: '2022-03-13T16:47:37.521Z' 39 | ``` 40 | 41 | And you just can't send `BigInt` to the client, period. 42 | 43 | ``` 44 | TypeError: Do not know how to serialize a BigInt 45 | ``` 46 | 47 | ## Solution 48 | 49 | With `superjson-remix`, you can easily send our example data to the client. Just replace the `json` helper function that you normally would import from `remix` with an import from `superjson`. 50 | 51 | ```diff 52 | - import { json } from 'remix'; 53 | + import { json } from 'superjson-remix'; 54 | ``` 55 | 56 | ## What about the client? 57 | 58 | We do the same thing on the client. We import `useLoaderData` from `superjson-remix` instead of from `remix`, just like we did with `json` on the server. 59 | 60 | ```diff 61 | - import { useLoaderData } from 'remix'; 62 | + import { useLoaderData } from 'superjson-remix'; 63 | ``` 64 | 65 | ## Complete example 66 | 67 | Here is an example route that uses `superjson-remix` to send our data to the client. 68 | 69 | ```js 70 | import { json, useLoaderData } from 'superjson-remix'; 71 | 72 | export const loader = () => { 73 | const obj = { 74 | set: new Set([1, 2, 3]), 75 | now: new Date(), 76 | largeNumber: BigInt(9007199254740991), 77 | }; 78 | 79 | return json(obj); 80 | }; 81 | 82 | const MyComponent = () => { 83 | const { set, now, largeNumber } = useLoaderData(); 84 | 85 | return ( 86 |
87 |

Our set: {Array.from(set).join(', ');}

88 |

Server time: {now.toLocaleString()}

89 |

A large number: {largeNumber.toString()}

90 |
91 | ); 92 | }; 93 | 94 | export default MyComponent; 95 | ``` 96 | 97 | It renders the following. 98 | 99 | ``` 100 | Our set: 1, 2, 3 101 | 102 | Server time: 3/13/2022, 1:05:20 PM 103 | 104 | A large number: 9007199254740991 105 | ``` 106 | 107 | ## Oh, yeah. The `meta` function. 108 | 109 | We provide a `withSuperJSON` higher-order function that wraps your `meta` function and will automatically deserialize the `data` argument. 110 | 111 | ```js 112 | import { withSuperJSON } from 'superjson-remix'; 113 | 114 | export const meta = withSuperJSON(({ data }) => { 115 | return { 116 | title: 'Sample App', 117 | description: `Created on ${data.now.toLocaleString()}`, 118 | }; 119 | }); 120 | ``` 121 | 122 | ## Getting Started 123 | 124 | Install the library with your package manager of choice, e.g.: 125 | 126 | ``` 127 | 128 | npm i superjson-remix 129 | 130 | ``` 131 | 132 | or 133 | 134 | ``` 135 | 136 | yarn add superjson-remix 137 | 138 | ``` 139 | 140 | ## License 141 | 142 | © 2022 Donavon West. Released under [MIT license](./LICENSE). 143 | --------------------------------------------------------------------------------