diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c678229 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,47 @@ +name: CI +on: [push] +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Begin CI... + uses: actions/checkout@v2 + + - name: Use Node 12 + uses: actions/setup-node@v1 + with: + node-version: 12.x + + - name: Use cached node_modules + uses: actions/cache@v2 + with: + path: node_modules + key: nodeModules-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + nodeModules- + + - name: Install dependencies + run: yarn install --frozen-lockfile + env: + CI: true + + - name: Typecheck + run: yarn typecheck + env: + CI: true + + - name: Lint + run: yarn lint + env: + CI: true + + - name: Test + run: yarn test --ci --coverage --maxWorkers=2 + env: + CI: true + + - name: Build + run: yarn build + env: + CI: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1de4442 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.log +.DS_Store +node_modules +.cache +dist +coverage diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3662b37 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a7ca4bc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Yaroslav Kukytsyak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5af4e7a --- /dev/null +++ b/README.md @@ -0,0 +1,306 @@ +# pre-router + +`pre-router` is a router for React with code and data preloading at its core. + +`pre-router` allows to specify for each route the component to render and the data to preload. Then, the code and data for the matching routes start loading in parallel right after the path changes, so even before rendering begins. This means that implementing the ["Render-as-You-Fetch" pattern](https://reactjs.org/docs/concurrent-mode-suspense.html#approach-3-render-as-you-fetch-using-suspense) is very natural with `pre-router`. + +Once we begin rendering the matching routes, if the code or data has not finished loading for a route, then it will suspend until code and data are loaded, showing a fallback. + +`pre-router` also gives you the ability to start loading code and data even before the user clicks on a link. If the user hovers over a link, there's a chance that they'll click it, so we could start loading the code for that route as soon as the user hovers. And if the user presses the mouse down on a link, there's a very good chance that they'll complete the click, so we could also start loading the data for the route as soon as the user presses in on the link. + +## Installation + +Using yarn: + +``` +yarn add pre-router suspendable +``` + +Using npm: + +``` +npm install pre-router suspendable +``` + +Note that we also need to install `suspendable` in order to use `lazyComponent` for route components. `lazyComponent` is similar to `React.lazy` but has a few important differences like being able to start loading the component even before rendering it and clear any errors after a component fails to load. You can read more about the differences in the [`suspendable` README](https://github.com/kyarik/suspendable#lazycomponent). + +## API + +- [`createRouter`](#createrouter) +- [`RouterOptions`](#routeroptions) +- [`PreloadContent`](#preloadcontent) +- [`Router`](#router) +- [``](#prerouter) +- [``](#link) +- [``](#navlink) +- [``](#redirect) + +### `createRouter` + +```ts +createRouter(routes: Route[], options?: RouterOptions): Router +``` + +**Parameters** + +- `routes: Route[]` is an array with the definition of all routes. A `Route` object consists of the following properties: + + - `path?: string` The path for which this route will match. Path parameters, even with custom regular expressions, are supported. For example, `/profile`, `/post/:slug`, and `/@:username([a-z]+)` are all valid paths. If no `path` is specified, then this route will always match. This can be used for the `404` route. + - `preloadData?: (params: any) => any` is the function used to preload data for the route whenever it matches. This function is called with the route parameters and it should return the preloaded data in the form of a resource that the route component can attempt to read and if it's not loaded yet, the component suspends. + - `component: ComponentType` is the component to render for the route. The component that is specified should be wrapped in `lazyComponent` so that it is code-split and it will start loading only when the route matches, in parallel with the data. This component will be passed the following props: + - `params` The values of the dynamic parameters in the `path`, if any. For example, if `path` is `/post/:slug`, then `params` could be `{ slug: 'an-interesting-post' }`. + - `preloadedData` is the prelaoded data returned by `preloadData`. This prop is passed only when `preloadData` is specified for the route. + - `children` Any matching child routes that should be rendered inside the parent route. + - `fallback?: ComponentType` is the optional fallback component that will be shown while the component or data for the route are still loading. + - `routes?: Route[]` any children routes of the current route. + +* `options?: RouterOptions` are the [router options](#routeroptions). + +**Return value** + +- `Router` a [router object](#router) that should be passed as the `router` prop to the [``](#prerouter) component. + +**Description** + +`createRouter` is used to create a router by specifying the definition for all our app's routes. The main properties in the definition of each route are the path, the function to preload the data for the route, and the component to render for the route. The created `Router` object can then be passed to the `PreRouter` component. + +**Example** + +```ts +const router = createRouter([ + { + path: '/', + component: lazyComponent(() => import('./components/Homepage'), { + autoRetry: true, + }), + }, + { + path: '/post/:slug', + component: lazyComponent(() => import('./components/PostPage'), { + autoRetry: true, + }), + preloadData: ({ slug }: { slug: string }) => preloadPost(slug), + }, + { + component: lazyComponent(() => import('./components/404'), { + autoRetry: true, + }), + }, +]); +``` + +Even though all routes can be specified top-level as shown in this example, most apps have layout around the route component which often consists of the header and the footer. In that case, we should have a root component like this: + +```tsx +const Root = ({ children }) => ( + <> +
+ {children} +