├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── run-tests.yml
├── .gitignore
├── .npmignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── deploy.mjs
├── docs
├── .gitignore
├── README.md
├── babel.config.js
├── blog
│ ├── 2019-05-28-first-blog-post.md
│ ├── 2019-05-29-long-blog-post.md
│ ├── 2021-08-01-mdx-blog-post.mdx
│ ├── 2021-08-26-welcome
│ │ ├── docusaurus-plushie-banner.jpeg
│ │ └── index.md
│ └── authors.yml
├── docs
│ ├── Features
│ │ ├── _category_.json
│ │ ├── custom-loader.md
│ │ ├── defer-queries.mdx
│ │ ├── deferred.png
│ │ ├── extending.mdx
│ │ ├── index.mdx
│ │ ├── other-libs.md
│ │ ├── passing-arguments.mdx
│ │ ├── stateful-loader.md
│ │ └── transforming.md
│ ├── Migrations
│ │ └── v0.x.md
│ ├── Quick Guide
│ │ ├── _category_.json
│ │ ├── accept-arguments.mdx
│ │ ├── add-queries.md
│ │ ├── base-loader.mdx
│ │ ├── consume-loader.mdx
│ │ ├── extend-loader.mdx
│ │ ├── extend-loader.png
│ │ ├── index.mdx
│ │ ├── queriesArg.png
│ │ └── rtk-query-loader-chart.png
│ ├── Reference
│ │ ├── aggregate-to-query.md
│ │ ├── awaitloader.md
│ │ ├── consumer-props.md
│ │ ├── create-loader.md
│ │ ├── index.mdx
│ │ ├── infer-loader-data.md
│ │ ├── use-create-query.md
│ │ └── with-loader.md
│ ├── examples.mdx
│ ├── intro.md
│ ├── problem-solve.mdx
│ └── terminology.png
├── docusaurus.config.js
├── package-lock.json
├── package.json
├── sidebars.js
├── src
│ ├── components
│ │ └── HomepageFeatures
│ │ │ ├── index.tsx
│ │ │ └── styles.module.css
│ └── css
│ │ └── custom.css
├── static
│ ├── .nojekyll
│ └── img
│ │ ├── docusaurus-social-card.jpg
│ │ ├── docusaurus.png
│ │ ├── favicon.ico
│ │ ├── logo.png
│ │ ├── logo.svg
│ │ ├── ryfrea-social-card.png
│ │ ├── undraw_docusaurus_mountain.svg
│ │ ├── undraw_docusaurus_react.svg
│ │ └── undraw_docusaurus_tree.svg
├── tsconfig.json
└── yarn.lock
├── package-lock.json
├── package.json
├── src
├── AwaitLoader.tsx
├── RTKLoader.tsx
├── aggregateToQuery.ts
├── createLoader.ts
├── createQuery.ts
├── index.ts
├── types.ts
├── useCreateLoader.ts
└── withLoader.tsx
├── testing-app
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── src
│ ├── components
│ │ └── ErrorBoundary.tsx
│ ├── index.tsx
│ ├── mocks.ts
│ ├── react-app-env.d.ts
│ ├── setupTests.ts
│ ├── store.ts
│ ├── testComponents.tsx
│ ├── tests.test.tsx
│ └── utils.tsx
├── tsconfig.json
└── yarn.lock
├── tsconfig.json
└── yarn.lock
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Actual behavior**
24 | A clear and concise description of what actually happened.
25 |
26 | **Screenshots**
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | **Device (please complete the following information):**
30 | - OS: [e.g. iOS]
31 | - Browser [e.g. chrome, safari]
32 | - Version [e.g. 22]
33 |
34 | **Additional context**
35 | Add any other context about the problem here.
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 | run-name: Testing out changes 🚀
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | build_and_test:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node: [10, 12]
16 |
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v2
20 | - name: Use Node.js ${{ matrix.node-version }}
21 | uses: actions/setup-node@v1
22 | with:
23 | node-version: ${{ matrix.node-version }}
24 | - name: Install package
25 | run: yarn install
26 | - name: Install testing-app
27 | working-directory: ./testing-app
28 | run: yarn install
29 | - name: Run tests
30 | working-directory: ./testing-app
31 | run: yarn run test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | lerna-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
13 |
14 | # Runtime data
15 | pids
16 | *.pid
17 | *.seed
18 | *.pid.lock
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 | *.lcov
26 |
27 | # nyc test coverage
28 | .nyc_output
29 |
30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
31 | .grunt
32 |
33 | # Bower dependency directory (https://bower.io/)
34 | bower_components
35 |
36 | # node-waf configuration
37 | .lock-wscript
38 |
39 | # Compiled binary addons (https://nodejs.org/api/addons.html)
40 | build/Release
41 |
42 | # Dependency directories
43 | node_modules/
44 | jspm_packages/
45 |
46 | # TypeScript v1 declaration files
47 | typings/
48 |
49 | # TypeScript cache
50 | *.tsbuildinfo
51 |
52 | # Optional npm cache directory
53 | .npm
54 |
55 | # Optional eslint cache
56 | .eslintcache
57 |
58 | # Microbundle cache
59 | .rpt2_cache/
60 | .rts2_cache_cjs/
61 | .rts2_cache_es/
62 | .rts2_cache_umd/
63 |
64 | # Optional REPL history
65 | .node_repl_history
66 |
67 | # Output of 'npm pack'
68 | *.tgz
69 |
70 | # Yarn Integrity file
71 | .yarn-integrity
72 |
73 | # dotenv environment variables file
74 | .env
75 | .env.test
76 |
77 | # parcel-bundler cache (https://parceljs.org/)
78 | .cache
79 |
80 | # Next.js build output
81 | .next
82 |
83 | # Nuxt.js build / generate output
84 | .nuxt
85 | dist
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | .vuepress/dist
95 |
96 | # Serverless directories
97 | .serverless/
98 |
99 | # FuseBox cache
100 | .fusebox/
101 |
102 | # DynamoDB Local files
103 | .dynamodb/
104 |
105 | # TernJS port file
106 | .tern-port
107 |
108 | .DS_Store
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | testing-app
3 | docs
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | **Thank you for considering contributing to this package**! This small guide will help you get started.
4 |
5 | ## 🍽️ Fork + Clone
6 |
7 | 1. Make a fork of this repository
8 | 2. Navigate to your forked repository and copy the SSH url. Clone your fork locally:
9 |
10 | ```bash
11 | $ git clone git@github.com:{ YOUR_USERNAME }/rtk-query-loader.git
12 | $ cd rtk-query-loader
13 | ```
14 |
15 | 3. Once cloned, you will see `origin` as your default remote, pointing to your personal forked repository. Add a remote named upstream pointing to the main rtk-query-loader:
16 |
17 | ```bash
18 | $ git remote add upstream git@github.com:ryfylke-react-as/rtk-query-loader.git
19 | $ git remote -v
20 | ```
21 |
22 | ## ➕ Create a new branch
23 |
24 | 1. First, make sure you have the latest changes:
25 |
26 | ```bash
27 | $ git pull upstream main
28 | ```
29 |
30 | 2. Create a new feature branch for your changes:
31 |
32 | ```bash
33 | $ git checkout -b { YOUR_BRANCH_NAME } main
34 | ```
35 |
36 | ## 👩💻 Add your changes
37 |
38 | For now, try to following existing patterns. Formalized code-guidelines will come at some point.
39 |
40 | If you have any questions, feel free to open up an issue!
41 |
42 | ## 🩺 Test you changes
43 |
44 | 1. In the `testing-app` directory, install the project dependencies, and then run the tests:
45 |
46 | ```bash
47 | $ yarn install
48 | $ yarn run test
49 | ```
50 |
51 | 2. If you are adding a new feature, make sure to write a new test for your change in `testing-app/tests.test.tsx` and rerun.
52 |
53 | ## 🤜 Push your changes
54 | If all the tests pass, you can commit your final changes and push!
55 |
56 | ```bash
57 | $ git commit -a -m "feat: withLoader now has a new argument, ..."
58 | $ git push origin { YOUR_BRANCH_NAME }
59 | ```
60 |
61 | > We prefer it if you try to stick to [conventional commit messages](https://www.conventionalcommits.org/en/v1.0.0/#summary).
62 |
63 | ## 📄 Create a pull-request
64 |
65 | - On Github, navigate to [@ryfylke-react/rtk-query-loader](https://github.com/ryfylke-react-as/rtk-query-loader) and click the button that reads "Compare & pull request".
66 | - Write a title and description, and submit your pull request.
67 |
68 | Your code will be reviewed and if everything looks good you'll be added to the list of contributors.
69 |
70 | Any contribution is appreciated 🤘
71 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | 
4 | 
5 | 
6 |
7 | **RTK Query Loader lets you create query loaders for your React components.**
8 |
9 | > Made for use with RTK Query, but is also [query-agnostic](#features).
10 |
11 | - [Live demo / Playground](https://codesandbox.io/s/rtk-query-loader-demo-42tubp)
12 | - [NPM](https://www.npmjs.com/package/@ryfylke-react/rtk-query-loader)
13 | - [Documentation](https://rtk-query-loader.ryfylke.dev/)
14 | - [Quick Start](https://rtk-query-loader.ryfylke.dev/Quick%20Guide/)
15 |
16 | ## Install
17 |
18 | ```bash
19 | yarn add @ryfylke-react/rtk-query-loader
20 | # or
21 | npm i @ryfylke-react/rtk-query-loader
22 | ```
23 |
24 | ## Features
25 |
26 | - [x] **Flexible**: You can extend and inherit existing loaders to create new ones.
27 | - [x] **Transformable**: Combine and transform the data from your loaders to your desired format.
28 | - [x] **Query Agnostic**: Can be used with RTK Query, Tanstack Query, JS promises, [and more...](https://rtk-query-loader.ryfylke.dev/Features/other-libs)
29 | - [x] **Configurable**: Exposes important configuration options, all of which are inheritable.
30 |
31 | You can read more about the features @ [the docs](https://rtk-query-loader.ryfylke.dev/Features/).
32 |
33 |
34 |
35 |
36 | 🔬 We're also properly tested! (✓ 30/30)
37 |
38 | ---
39 |
40 | * **aggregateToQuery**
41 | * ✓ It aggregates query status (167 ms)
42 | * **useCreateQuery**
43 | * ✓ It creates a query (107 ms)
44 | * ✓ The query can throw error (108 ms)
45 | * ✓ You can refetch the query (645 ms)
46 | * ** **
47 | * ✓ Renders loading state until data is available (130 ms)
48 | * ✓ Will pass arguments properly (129 ms)
49 | * **withLoader**
50 | * ✓ Renders loading state until data is available (132 ms)
51 | * ✓ onError renders when applicable (130 ms)
52 | * ✓ onFetching renders when applicable (319 ms)
53 | * ✓ Internal state won't reset when using whileFetching (272 ms)
54 | * ✓ Internal state will reset when using onFetching (271 ms)
55 | * ✓ Can use custom loader component (129 ms)
56 | * ✓ loaderComponent is backwards compatible (121 ms)
57 | * ✓ Can defer some queries (231 ms)
58 | * ✓ Can defer all queries (130 ms)
59 | * ✓ Loaders with no queries render immediately (4 ms)
60 | * ✓ Can remount a component that has a failed query (161 ms)
61 | * **createLoader**
62 | * ✓ Normally, deferred queries do not throw (205 ms)
63 | * ✓ Deferred queries throw error when configured to (209 ms)
64 | * ✓ Can send static payload to loader (7 ms)
65 | * ✓ Loader passes props through queriesArg to queries (128 ms)
66 | * **.extend()**
67 | * ✓ Can extend onLoading (5 ms)
68 | * ✓ Can extend onError (128 ms)
69 | * ✓ Can extend onFetching (156 ms)
70 | * ✓ Can extend whileFetching (133 ms)
71 | * ✓ Can extend queries (122 ms)
72 | * ✓ Can extend deferred queries (230 ms)
73 | * ✓ Can extend many times (282 ms)
74 | * ✓ Can extend with only transform (133 ms)
75 | * ✓ Can partially extend config (138 ms)
76 |
77 | ---
78 |
79 |
80 | ## Example
81 | A simple example of a component using rtk-query-loader:
82 |
83 | ```tsx
84 | import {
85 | createLoader,
86 | withLoader,
87 | } from "@ryfylke-react/rtk-query-loader";
88 |
89 | const loader = createLoader({
90 | useQueries: () => {
91 | const pokemon = useGetPokemon();
92 | const currentUser = useGetCurrentUser();
93 |
94 | return {
95 | queries: {
96 | pokemon,
97 | currentUser,
98 | },
99 | };
100 | },
101 | onLoading: () =>
Loading pokemon...
,
102 | });
103 |
104 | const Pokemon = withLoader((props, loader) => {
105 | const pokemon = loader.queries.pokemon.data;
106 | const currentUser = loader.queries.currentUser.data;
107 |
108 | return (
109 |
116 | );
117 | }, loader);
118 | ```
119 |
120 | ## What problem does this solve?
121 |
122 | Let's say you have a component that depends on data from more than one query.
123 |
124 | ```tsx
125 | function Component(props){
126 | const userQuery = useGetUser(props.id);
127 | const postsQuery = userGetPostsByUser(userQuery.data?.id, {
128 | skip: user?.data?.id === undefined,
129 | });
130 |
131 | if (userQuery.isError || postsQuery.isError){
132 | // handle error
133 | }
134 |
135 | /* possible something like */
136 | // if (userQuery.isLoading){ return (...) }
137 |
138 | return (
139 |
140 | {/* or checking if the type is undefined in the jsx */}
141 | {(userQuery.isLoading || postsQuery.isLoading) && (...)}
142 | {userQuery.data && postsQuery.data && (...)}
143 |
144 | )
145 | }
146 | ```
147 |
148 | The end result is possibly lots of bloated code that has to take into consideration that the values could be undefined, optional chaining, etc...
149 |
150 | What if we could instead "join" these queries into one, and then just return early if we are in the initial loading stage. That's basically the approach that rtk-query-loader takes. Some pros include:
151 |
152 | - [x] Way less optional chaining in your components
153 | - [x] Better type certainty
154 | - [x] Easy to write re-usable loaders that can be abstracted away from the components
155 |
156 | ## [Documentation](https://rtk-query-loader.ryfylke.dev)
157 |
158 | ## [Quick Guide](https://rtk-query-loader.ryfylke.dev/Quick%20Guide/)
159 |
--------------------------------------------------------------------------------
/deploy.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env zx
2 | try {
3 | await Promise.all([
4 | $`yarn build`,
5 | $`npm version patch --force`,
6 | ]);
7 | const version = require("./package.json").version;
8 | await Promise.all([
9 | $`git commit --allow-empty -m "version: ${version}"`,
10 | $`git push`,
11 | $`npm publish --access public`,
12 | $`echo "======================"`,
13 | $`echo "Deployed! 🚀 (${version})"`,
14 | $`echo "======================"`,
15 | ]);
16 | } catch (err) {
17 | await Promise.all([
18 | $`echo "======================"`,
19 | $`echo "Deploy failed! 😭"`,
20 | $`echo "======================"`,
21 | $`echo "${err}"`,
22 | ]);
23 | }
24 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
4 |
5 | ### Installation
6 |
7 | ```
8 | $ yarn
9 | ```
10 |
11 | ### Local Development
12 |
13 | ```
14 | $ yarn start
15 | ```
16 |
17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ### Build
20 |
21 | ```
22 | $ yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ### Deployment
28 |
29 | Using SSH:
30 |
31 | ```
32 | $ USE_SSH=true yarn deploy
33 | ```
34 |
35 | Not using SSH:
36 |
37 | ```
38 | $ GIT_USER= yarn deploy
39 | ```
40 |
41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
42 |
--------------------------------------------------------------------------------
/docs/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/docs/blog/2019-05-28-first-blog-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | slug: first-blog-post
3 | title: First Blog Post
4 | authors:
5 | name: Gao Wei
6 | title: Docusaurus Core Team
7 | url: https://github.com/wgao19
8 | image_url: https://github.com/wgao19.png
9 | tags: [hola, docusaurus]
10 | ---
11 |
12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
13 |
--------------------------------------------------------------------------------
/docs/blog/2019-05-29-long-blog-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | slug: long-blog-post
3 | title: Long Blog Post
4 | authors: endi
5 | tags: [hello, docusaurus]
6 | ---
7 |
8 | This is the summary of a very long blog post,
9 |
10 | Use a `` comment to limit blog post size in the list view.
11 |
12 |
13 |
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
15 |
16 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
17 |
18 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
19 |
20 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
21 |
22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
23 |
24 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
25 |
26 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
27 |
28 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
29 |
30 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
31 |
32 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
33 |
34 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
35 |
36 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
37 |
38 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
39 |
40 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
41 |
42 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
43 |
44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
45 |
--------------------------------------------------------------------------------
/docs/blog/2021-08-01-mdx-blog-post.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: mdx-blog-post
3 | title: MDX Blog Post
4 | authors: [slorber]
5 | tags: [docusaurus]
6 | ---
7 |
8 | Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
9 |
10 | :::tip
11 |
12 | Use the power of React to create interactive blog posts.
13 |
14 | ```js
15 | alert('button clicked!')}>Click me!
16 | ```
17 |
18 | alert('button clicked!')}>Click me!
19 |
20 | :::
21 |
--------------------------------------------------------------------------------
/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg
--------------------------------------------------------------------------------
/docs/blog/2021-08-26-welcome/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | slug: welcome
3 | title: Welcome
4 | authors: [slorber, yangshun]
5 | tags: [facebook, hello, docusaurus]
6 | ---
7 |
8 | [Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
9 |
10 | Simply add Markdown files (or folders) to the `blog` directory.
11 |
12 | Regular blog authors can be added to `authors.yml`.
13 |
14 | The blog post date can be extracted from filenames, such as:
15 |
16 | - `2019-05-30-welcome.md`
17 | - `2019-05-30-welcome/index.md`
18 |
19 | A blog post folder can be convenient to co-locate blog post images:
20 |
21 | 
22 |
23 | The blog supports tags as well!
24 |
25 | **And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.
26 |
--------------------------------------------------------------------------------
/docs/blog/authors.yml:
--------------------------------------------------------------------------------
1 | endi:
2 | name: Endilie Yacop Sucipto
3 | title: Maintainer of Docusaurus
4 | url: https://github.com/endiliey
5 | image_url: https://github.com/endiliey.png
6 |
7 | yangshun:
8 | name: Yangshun Tay
9 | title: Front End Engineer @ Facebook
10 | url: https://github.com/yangshun
11 | image_url: https://github.com/yangshun.png
12 |
13 | slorber:
14 | name: Sébastien Lorber
15 | title: Docusaurus maintainer
16 | url: https://sebastienlorber.com
17 | image_url: https://github.com/slorber.png
18 |
--------------------------------------------------------------------------------
/docs/docs/Features/_category_.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/docs/docs/Features/custom-loader.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 5
3 | ---
4 |
5 | # Custom `loaderComponent`
6 |
7 | A `loaderComponent` is the component that determines what to render given the aggregated query status, when using `withLoader`.
8 |
9 | ```typescript title="loaderComponent Props"
10 | export type CustomLoaderProps = {
11 | /** The joined query for the loader */
12 | query: UseQueryResult;
13 | /** What the loader requests be rendered when data is available */
14 | onSuccess: (data: T) => React.ReactElement;
15 | /** What the loader requests be rendered when the query fails */
16 | onError?: (
17 | error: SerializedError | FetchBaseQueryError
18 | ) => JSX.Element;
19 | /** What the loader requests be rendered while loading data */
20 | onLoading?: React.ReactElement;
21 | /** What the loader requests be rendered while fetching data */
22 | onFetching?: React.ReactElement;
23 | /** What the loader requests be rendered while fetching data */
24 | whileFetching?: {
25 | /** Should be appended to the success result while fetching */
26 | append?: React.ReactElement;
27 | /** Should be prepended to the success result while fetching */
28 | prepend?: React.ReactElement;
29 | };
30 | };
31 | ```
32 |
33 | This is what the default `loaderComponent` looks like:
34 |
35 | ```typescript title=RTKLoader.tsx
36 | import { SerializedError } from "@reduxjs/toolkit";
37 | import * as React from "react";
38 | import { CustomLoaderProps } from "./types";
39 |
40 | export function RTKLoader(
41 | props: CustomLoaderProps
42 | ): React.ReactElement {
43 | const shouldLoad =
44 | props.query.isLoading || props.query.isUninitialized;
45 | const hasError = props.query.isError && props.query.error;
46 | const isFetching = props.query.isFetching;
47 |
48 | if (shouldLoad) {
49 | return props.onLoading ?? ;
50 | }
51 |
52 | if (hasError) {
53 | return (
54 | props.onError?.(props.query.error as SerializedError) ?? (
55 |
56 | )
57 | );
58 | }
59 |
60 | if (isFetching && props.onFetching) {
61 | return props.onFetching;
62 | }
63 |
64 | if (props.query.data !== undefined) {
65 | const prepend = isFetching
66 | ? props.whileFetching?.prepend ?? null
67 | : null;
68 | const append = isFetching
69 | ? props.whileFetching?.append ?? null
70 | : null;
71 | const componentWithData = props.onSuccess(props.query.data);
72 |
73 | return (
74 | <>
75 | {prepend}
76 | {componentWithData}
77 | {append}
78 | >
79 | );
80 | }
81 |
82 | return ;
83 | }
84 | ```
85 |
86 | You could pass a custom `loaderComponent` to your loaders, if you'd like:
87 |
88 | ```typescript
89 | const CustomLoader = (props: CustomLoaderProps) => {
90 | // Handle rendering
91 | };
92 |
93 | const loader = createLoader({
94 | loaderComponent: CustomLoader,
95 | // ...
96 | });
97 | ```
98 |
99 | :::tip
100 | This allows you to have really fine control over how the rendering of components using `withLoader` should work.
101 | :::
102 |
--------------------------------------------------------------------------------
/docs/docs/Features/defer-queries.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | # Deferring queries
6 |
7 | Say you have a query that takes a long time to resolve. You want to put it in the loader, to co-locate it with the rest of your queries, but you don't want it to affect the loading state and postpone the initial load of the component. This is where `deferredQueries` comes in.
8 |
9 | ## Example
10 |
11 | ```typescript {3-13}
12 | const loader = createLoader({
13 | useQueries: () => {
14 | const importantQuery = useImportantQuery();
15 | const slowQuery = useSlowQuery();
16 |
17 | return {
18 | queries: {
19 | importantQuery,
20 | },
21 | deferredQueries: {
22 | slowQuery,
23 | },
24 | };
25 | },
26 | transform: (loader) => ({
27 | important: loader.queries.importantQuery.data, // ImportantQueryResponse
28 | slow: loader.deferredQueries.slowQuery.data, // SlowQueryResponse | undefined
29 | }),
30 | });
31 | ```
32 |
33 |
37 |
38 | ## Configure
39 |
40 | > **New in version 1.0.3**
41 |
42 | You can pass a `Config` to your `Loader`s:
43 |
44 | ```typescript {4-8}
45 | const loader = createLoader({
46 | useQueries: () => ({...}),
47 | onError: () => (...),
48 | config: {
49 | deferred: {
50 | shouldThrowError: true,
51 | },
52 | }
53 | })
54 | ```
55 |
56 | - `shouldThrowError` - Determines whether or not `deferredQueries` should send the component to the `onError` view if one of the deferred queries end up in a error state.
57 |
--------------------------------------------------------------------------------
/docs/docs/Features/deferred.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/docs/Features/deferred.png
--------------------------------------------------------------------------------
/docs/docs/Features/extending.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Extend
6 |
7 | You can **extend** existing `Loader`s. This lets you inherit and overwrite properties from an existing `Loader`.
8 |
9 | ## Example
10 |
11 | ```tsx
12 | const parentLoader = createLoader({
13 | onLoading: () => Loading...
14 | });
15 |
16 | const childLoader = parentLoader.extend({
17 | useQueries: () => ({...}),
18 | onError: () => Error
,
19 | }).extend({
20 | transform: () => ({...}),
21 | }).extend({
22 | onLoading: () => Overwritten loading...
,
23 | });
24 | ```
25 |
26 | ## Separation of concerns
27 |
28 | Its up to you how much you want to separate logic here. Some examples would be...
29 |
30 | - Co-locating loaders in a shared folder
31 | - Co-locating loaders in same file as component
32 | - Co-locating loaders in same directory but in a separate file from the component
33 |
34 | I personally prefer to keep the loaders close to the component, either in a file besides it or directly in the file itself, and then keep a base loader somewhere else to extend from.
35 |
36 | ## Creating a loader hierarchy
37 |
38 | You can extend from any loader, including loaders that have already been extended. This allows you to create a hierarchy of loaders that can be used to share logic between components.
39 |
40 |
44 |
45 | ## Tips
46 |
47 | :::caution `.extend` will not merge what two separate `useQueries` returns.
48 | For example, you cannot _just_ inherit the deferredQueries, you must either inherit or overwrite the whole `useQueries` argument.
49 | :::
50 |
51 | :::tip Reusable transformers
52 | You can extend as many times as you'd like. You can use this feature to easily inject reusable snippets, like transformers.
53 |
54 | ```typescript
55 | type QueryRecord = Record>;
56 |
57 | export const transformData = {
58 | transform: (data: {
59 | queries: QueryRecord;
60 | deferredQueries: QueryRecord;
61 | payload: unknown;
62 | }) => {
63 | // handle transformation in generic way
64 | },
65 | };
66 | ```
67 |
68 | ```typescript
69 | const loader = createLoader({...}).extend(transformData);
70 | ```
71 |
72 | :::
73 |
--------------------------------------------------------------------------------
/docs/docs/Features/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 5
3 | ---
4 |
5 | import DocCardList from "@theme/DocCardList";
6 |
7 | # Features
8 |
9 | RTK Query Loader tries to give you all the flexibility you need to create reusable and extendable loaders.
10 |
11 | - Supply as many queries as you'd like.
12 | - Supply queries that [don't affect loading state](defer-queries).
13 | - Send down payloads that contain any static data.
14 | - [Transform](./transforming) the data to your desired output-format.
15 | - Set up [default](../Quick%20Guide/base-loader) loading and error states.
16 | - [Extend](./extending) and re-use existing loaders.
17 | - Create [stateful loaders](./stateful-loader)
18 |
19 | And even if you don't use `RTK Query`...
20 |
21 | - Supply queries that are [just promises](../Reference/use-create-query).
22 | - [Use with other data loading libraries](./other-libs)
23 |
24 | ---
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/docs/Features/other-libs.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 4
3 | ---
4 |
5 | # Use with other libraries
6 |
7 | You can use RTK Query Loader with most other similar query-fetching libraries. This is possible through the use of _resolvers_.
8 |
9 | :::note
10 | Although `rtk-query-loader` was build with `@reduxjs/toolkit` in mind, the underlying principles can be applied to any similar data loading solution.
11 | :::
12 |
13 | ## Tanstack Query
14 |
15 | [See example on CodeSandbox](https://codesandbox.io/s/tanstack-query-rtk-query-loader-example-6393w2)
16 |
17 | ```typescript
18 | import {
19 | useQuery,
20 | UseQueryResult as TanstackUseQueryResult,
21 | UseQueryOptions,
22 | } from "@tanstack/react-query";
23 |
24 | const tanstackResolver = (
25 | query: TanstackUseQueryResult
26 | ): UseQueryResult & {
27 | original_query: TanstackUseQueryResult;
28 | } => ({
29 | isLoading: query.isLoading,
30 | isFetching: query.isFetching,
31 | isSuccess: query.isSuccess,
32 | isError: query.isError,
33 | error: query.error,
34 | data: query.data,
35 | isUninitialized: !query.isFetchedAfterMount,
36 | originalArgs: null,
37 | refetch: () => query.refetch(),
38 | currentData: query.data,
39 | endpointName: undefined,
40 | original_query: query,
41 | });
42 |
43 | const useTanstackQuery = (
44 | args: UseQueryOptions
45 | ) => tanstackResolver(useQuery(args));
46 | ```
47 |
48 | This is how you would use it:
49 |
50 | ```typescript
51 | import { useTanstackQuery } from "../loader-resolvers";
52 |
53 | const loader = createLoader({
54 | useQueries: () => {
55 | const repoData = useTanstackQuery({
56 | queryKey: ["repoData"],
57 | queryFn: () =>
58 | fetch(
59 | "https://api.github.com/repos/ryfylke-react-as/rtk-query-loader"
60 | ).then((res) => res.json() as Promise),
61 | });
62 |
63 | return {
64 | queries: {
65 | repoData,
66 | },
67 | };
68 | },
69 | });
70 | ```
71 |
72 | The output format will obviously be a bit different, but in this example you have access to the original query at the `.original_query` property.
73 |
74 | ## Other libraries
75 |
76 | If you are interested in creating resolvers for other libraries, you can [edit this page](https://github.com/ryfylke-react-as/rtk-query-loader/tree/main/docs/docs/Advanced/other-libs.md) and then [submit a pull request](https://github.com/ryfylke-react-as/rtk-query-loader/compare) on GitHub to share your resolver here, as an npm package, or with the code embedded directly in the docs.
77 |
78 | ## Use with promises
79 |
80 | If you want to use RTK Query Loader with promises, you can read more about that on the [`useCreateQuery` reference page](../Reference/use-create-query.md).
81 |
--------------------------------------------------------------------------------
/docs/docs/Features/passing-arguments.mdx:
--------------------------------------------------------------------------------
1 | # Passing arguments
2 |
3 | You can setup loaders so that they can be used with arguments. How you pass the arguments depends on how you use the loader.
4 |
5 | You set this up using the `queriesArg` argument. This is a function that takes the consumer's expected props and returns the argument that should be passed to the useQueries hook.
6 |
7 | ```tsx
8 | const loader = createLoader({
9 | queriesArg: (props: { userId: string }) => props.userId,
10 | useQueries: (userId) => {
11 | // userId is automatically inferred as string
12 | const user = useGetUserQuery(userId);
13 |
14 | return {
15 | queries: {
16 | user,
17 | },
18 | };
19 | },
20 | });
21 | ```
22 |
23 | ## Data flow
24 |
25 | The transformation layer between the useQueries hook and the loader is the `queriesArg` function.
26 |
27 | It takes the component's props and returns the argument that should be passed to the useQueries hook. This is nessecary to be able to consume the loader in a type-safe way through withLoader.
28 |
29 |
34 |
35 | ## When using ` `
36 |
37 | You should pass the expected _props_ to the `arg` prop when using ` `.
38 |
39 | ```tsx
40 | (...)}
43 | args={{
44 | userId: "1234",
45 | }}
46 | />
47 | ```
48 |
49 | ## Properly typing the `queriesArg` function
50 |
51 | You can use the `ConsumerProps` utility type to type the `queriesArg` function.
52 |
53 | ```tsx
54 | import {
55 | ConsumerProps,
56 | createLoader,
57 | } from "@ryfylke-react/rtk-query-loader";
58 |
59 | type UserRouteLoaderProps = ConsumerProps<{
60 | userId: string;
61 | }>;
62 |
63 | const loader = createLoader({
64 | queriesArg: (props: UserRouteLoaderProps) => props.userId,
65 | // ...
66 | });
67 | ```
68 |
69 | The type utility will ensure that the type can be extended by the consumer components. This means that the following types are both valid for the definition above:
70 |
71 | ```ts
72 | // This is valid ✅
73 | type Props_1 = {
74 | userId: string;
75 | };
76 |
77 | // This is also valid ✅
78 | type Props_2 = {
79 | userId: string;
80 | someOtherProp: string;
81 | };
82 | ```
83 |
--------------------------------------------------------------------------------
/docs/docs/Features/stateful-loader.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar-position: 6
3 | ---
4 |
5 | # Stateful loaders
6 |
7 | Since a `Loader` contains a hook, that hook can contain state.
8 |
9 | ```typescript
10 | const loader = createLoader({
11 | useQueries: () => {
12 | const [name, setName] = useState("charizard");
13 | const pokemon = useGetPokemon(name);
14 | return {
15 | queries: {
16 | pokemon,
17 | },
18 | };
19 | },
20 | });
21 | ```
22 |
23 | You can then control this state, by sending the handlers through `payload`:
24 |
25 | ```typescript {10}
26 | const componentLoader = createLoader({
27 | useQueries: () => {
28 | const [name, setName] = useState("charizard");
29 | const debouncedName = useDebounce(name, 200);
30 | const pokemon = useGetPokemon(debouncedName);
31 | return {
32 | queries: {
33 | pokemon,
34 | },
35 | payload: { name, setName },
36 | };
37 | },
38 | });
39 |
40 | const Component = withLoader((props, loader) => {
41 | const onChange = (e) => loader.payload.setName(e.target.value);
42 | // ...
43 | }, componentLoader);
44 | ```
45 |
--------------------------------------------------------------------------------
/docs/docs/Features/transforming.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Transform
6 |
7 | You can transform the queries to any format you'd like.
8 |
9 | ## Example
10 |
11 | ```ts
12 | const notTransformed = createLoader({
13 | useQueries: () => ({
14 | queries: { pokemons: useGetPokemonsQuery() },
15 | }),
16 | });
17 |
18 | type NotTransformedData = InferLoaderData;
19 | // { queries: { pokemons: UseQueryResult } }
20 |
21 | const transformed = createLoader({
22 | useQueries: () => ({
23 | queries: { pokemons: useGetPokemonsQuery() },
24 | }),
25 | transform: (loader) => ({
26 | results: loader.queries.pokemons.data,
27 | query: loader.queries.pokemons,
28 | }),
29 | });
30 |
31 | type TransformedData = InferLoaderData;
32 | // { results: Pokemon[]; query: UseQueryResult; }
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/docs/Migrations/v0.x.md:
--------------------------------------------------------------------------------
1 | # Migrating to `1.0.0`
2 |
3 | If you are using `@ryfylke-react/rtk-query-loader@0.3.51` or earlier, these docs will help you migrate your existing codebase over to `1.0.0`.
4 |
5 | ## Input format for data
6 |
7 | Previously, queries have been passed using two arguments:
8 |
9 | - **`queries`**
10 | - **`deferredQueries`**
11 |
12 | These two functions used to return a `readonly UseQueryResult[]`.
13 |
14 | In version `1.0.0`, these arguments have now been joined into one, called `useQueries`.
15 |
16 | ```typescript {3-10,15-23}
17 | // Previously
18 | const loader = createLoader({
19 | queries: (arg: string) => {
20 | const pokemonQuery = useGetPokemon(arg);
21 | return [pokemonQuery] as const;
22 | },
23 | deferredQueries: () => {
24 | const otherQuery = useOtherQuery();
25 | return [otherQuery] as const;
26 | },
27 | });
28 |
29 | // In version 1:
30 | const loader = createLoader({
31 | useQueries: (arg: string) => {
32 | const pokemonQuery = useGetPokemon(arg);
33 | const otherQuery = useOtherQuery();
34 |
35 | return {
36 | queries: { pokemonQuery },
37 | deferredQueries: { otherQuery },
38 | };
39 | },
40 | });
41 | ```
42 |
43 | :::info
44 | Previously, you _had_ to use `transform` as well to expose the deferred queries to your consumer. This is no longer required.
45 | :::
46 |
47 | ## Output format for data
48 |
49 | This also means that the output format of the loader has changed. Typescript will help you out here, but for the most part **what you send in is what you get out**.
50 |
51 | ```typescript
52 | const loader = createLoader({
53 | useQueries: () => ({
54 | queries: {
55 | pokemons: useGetPokemons(),
56 | },
57 | }),
58 | });
59 |
60 | type LoaderData = InferLoaderData;
61 | // {
62 | // queries: {
63 | // pokemons: UseQueryResult
64 | // }
65 | // }
66 | ```
67 |
68 | This output format change will affect your consumer components, as well as the (now single) argument passed to `transform`.
69 |
70 | You can still of course optionally [transform](../Features/transforming.md) the output.
71 |
72 | :::tip You should know
73 | This I/O interface change is the **only breaking change** from the previous versions.
74 | :::
75 |
76 | :::note
77 | Through rewriting the tests and lots of local testing, I can assure you that this refactor should be relatively straight forward and easy to do. If you fix the loaders first by changing and `queries` and `deferredQueries` to now use `useQueries`, you can let Typescript help you out in the consumers afterwards.
78 | :::
79 |
80 | ## New feature: `payload`
81 |
82 | You can now send _any_ data you want through the loader. This is useful for when your loader contains a lot of logic, or if you want to pass some handlers down to the consumer to change the arguments of your queries.
83 |
84 | ```typescript
85 | const loader = createLoader({
86 | useQueries: () => {
87 | const [pokemonName, setPokemonName] = useState("charizard");
88 | const pokemon = useGetPokemon(pokemonName);
89 |
90 | return {
91 | queries: {
92 | pokemon,
93 | },
94 | payload: {
95 | changePokemon: setPokemonName,
96 | },
97 | };
98 | },
99 | });
100 |
101 | const Consumer = withLoader((props, data) => {
102 | const { payload, queries } = data;
103 | // ...
104 | }, loader);
105 | ```
106 |
107 | Previously, if you wanted to do something like this, you had to use [useCreateQuery](../Reference/use-create-query.md), or a wrapper component. If you did, you can now refactor your code to use `payload` instead, which should be a lot more clean and flexible.
108 |
109 | ## Change: Extend transform
110 |
111 | Previously, `transform` and `queries` were linked when extending. Meaning you had to either extend both, just `queries` or neither of these. You could not supply _just_ a `transform`. This is no longer the case, and you are free to extend with _just_ transform as well.
112 |
113 | ## Reporting bugs
114 |
115 | If you find any bugs or issues with this new update, I strongly encourage you to [file a bug report](https://github.com/ryfylke-react-as/rtk-query-loader/issues/new?assignees=&labels=&template=bug_report.md&title=) on the Github repo.
116 |
117 | I hope you are happy with the changes coming in version `1`. They have been thoroughly thought out and brewed on for a couple of months. Although it's never fun to push a breaking change, I hope you agree that these changes add a lot more flexibility to the loaders, and less confusion around I/O format. I'm personally very glad that I'm no longer required to rembember to use `as const` to ensure I get the correct types in my loader.
118 |
--------------------------------------------------------------------------------
/docs/docs/Quick Guide/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "collapsed": false
3 | }
4 |
--------------------------------------------------------------------------------
/docs/docs/Quick Guide/accept-arguments.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 4
3 | ---
4 |
5 | # 4. Arguments for your loader
6 |
7 | If you want the loader to take an argument, all you have to do is to add a `queriesArg`:
8 |
9 | ```tsx {5-7,10-12}
10 | import { ConsumerProps } from "@ryfylke-react/rtk-query-loader";
11 |
12 | // This means that any component that has props that extend this
13 | // type can consume the loader using `withLoader`
14 | type UserRouteLoaderProps = ConsumerProps<{
15 | userId: string;
16 | }>;
17 |
18 | export const userRouteLoader = baseLoader.extend({
19 | queriesArg: (props: UserRouteLoaderProps) => props.userId,
20 | // type is now inferred from queriesArg return
21 | useQueries: (userId) => {
22 | const user = useGetUserQuery(userId);
23 | const posts = useGetPostsByUser(userId);
24 |
25 | return {
26 | queries: {
27 | user,
28 | posts,
29 | },
30 | };
31 | },
32 | });
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/docs/Quick Guide/add-queries.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | # 3. Add queries
6 |
7 | You can now start to add queries to your extended loaders.
8 |
9 | The `useQueries` argument of [createLoader](/Reference/create-loader) is a _hook_, which means that [the rules of hooks](https://reactjs.org/docs/hooks-rules.html) apply. This gives you the super-power of utilizing other hooks inside of your loader.
10 |
11 | ```tsx title="/src/loaders/userRouteLoader.tsx" {6-15}
12 | import { baseLoader } from "./baseLoader";
13 | // ...
14 |
15 | export const userRouteLoader = baseLoader.extend({
16 | useQueries: () => {
17 | const { userId } = useParams();
18 | const user = useGetUserQuery(userId);
19 | const posts = useGetPostsByUser(userId);
20 |
21 | return {
22 | queries: {
23 | user,
24 | posts,
25 | },
26 | };
27 | },
28 | });
29 | ```
30 |
31 | You can add as many queries as you'd like to `Response.queries`, and they will all aggregate to a common loading state.
32 |
--------------------------------------------------------------------------------
/docs/docs/Quick Guide/base-loader.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # 1. Create a base loader
6 |
7 | A `Loader` can control the loading, fetch and error state views for consumer components.
8 |
9 | :::tip
10 | Use a base loader to create sensible "fallback"/"default"-states for consumer components.
11 | :::
12 |
13 | ```tsx title="/src/loaders/baseLoader.tsx" {7-20}
14 | import {
15 | createLoader,
16 | withLoader,
17 | } from "@ryfylke-react/rtk-query-loader";
18 | import { Loading, GenericErrorView } from "../components/common";
19 |
20 | export const baseLoader = createLoader({
21 | onLoading: () => ,
22 | onError: (props, error) => ,
23 | });
24 | ```
25 |
--------------------------------------------------------------------------------
/docs/docs/Quick Guide/consume-loader.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 5
3 | ---
4 |
5 | import Tabs from "@theme/Tabs";
6 | import TabItem from "@theme/TabItem";
7 |
8 | # 5. Consume the loader
9 |
10 | ## Picking the right method
11 |
12 | We recommend consuming the loader with either `withLoader` or `AwaitLoader`.
13 |
14 | - Use `withLoader` if you want to use the loader to affect the whole component.
15 | - Use `AwaitLoader` if you want to use the loader in a more conditional way, or if you only want a small section of your component to start loading data, after the rest of the component has rendered.
16 |
17 | You could also use `useLoader`, which is the hook that `withLoader` and `AwaitLoader` use under the hood. This will simply return the aggregated queries of the loader, and is useful if you want to use the data, but **not the loading and error states of the loader**.
18 |
19 |
24 |
25 | ## Examples
26 |
27 | Select a tab below to see an example of how you would consume the loader using that specific method.
28 |
29 |
30 |
31 |
32 | A convenient wrapper that ensures that the component is only rendered when it has data.
33 |
34 | :::info
35 | This is the recommended and optimal way to consume the loaders.
36 | :::
37 |
38 | ```tsx {8-26}
39 | import { withLoader } from "@ryfylke-react/rtk-query-loader";
40 | import { userRouteLoader } from "../loaders/baseLoader";
41 |
42 | type Props = {
43 | /* ... */
44 | };
45 |
46 | export const UserRoute = withLoader((props: Props, loader) => {
47 | // `queries` is typed correctly, and ensured to have loaded.
48 | const {
49 | user,
50 | posts
51 | } = loader.queries;
52 |
53 | return (
54 |
55 |
56 | {user.data.name}
57 | {user.isFetching || posts.isFetching ? ( ) : null}
58 |
59 |
60 | {posts.data.map((post) => (...))}
61 |
62 |
63 | );
64 | }, userRouteLoader);
65 | ```
66 |
67 |
77 |
78 |
79 |
80 |
81 |
82 | > **New in version 1.0.4**
83 |
84 | If you prefer not using higher order components, or want to use the loader in a more conditional way, you can use the <`AwaitLoader` /> component.
85 |
86 | ```tsx {6-18}
87 | import { AwaitLoader } from "@ryfylke-react/rtk-query-loader";
88 | import { userRouteLoader } from "./baseLoader";
89 |
90 | const UserRoute = () => {
91 | return (
92 | (
95 |
96 |
97 | {queries.user.data.name}
98 |
99 |
100 | {queries.posts.data.map((post) => (...))}
101 |
102 |
103 | )}
104 | />
105 | );
106 | };
107 | ```
108 |
109 |
119 |
120 |
121 |
122 |
123 | Every `Loader` contains an actual hook that you can call to run all the queries and aggregate their statuses as if they were just one joined query.
124 |
125 | This is convenient if you want to simply use the data, but not the loading and error states of the loader.
126 |
127 | ```tsx {3,9}
128 | import { userRouteLoader } from "./baseLoader";
129 |
130 | const useLoaderData = userRouteLoader.useLoader;
131 |
132 | type Props = {
133 | /* ... */
134 | };
135 | const UserRoute = (props: Props) => {
136 | const loaderQuery = useLoaderData();
137 |
138 | if (loaderQuery.isLoading) {
139 | // ...
140 | }
141 |
142 | if (loaderQuery.isError) {
143 | // ...
144 | }
145 | // ...
146 | };
147 | ```
148 |
149 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/docs/docs/Quick Guide/extend-loader.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # 2. Extend the base loader
6 |
7 | Extend from the base loader so that you inherit the sensible defaults. You can overwrite these at any point.
8 |
9 | ```tsx title="/src/loaders/userRouteLoader.tsx" {3-11}
10 | import { baseLoader } from "./baseLoader";
11 |
12 | export const userRouteLoader = baseLoader.extend({});
13 | ```
14 |
15 | You can pass any argument from [`createLoader`](/Reference/create-loader) into [`Loader.extend`](/Features/extending).
16 |
--------------------------------------------------------------------------------
/docs/docs/Quick Guide/extend-loader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/docs/Quick Guide/extend-loader.png
--------------------------------------------------------------------------------
/docs/docs/Quick Guide/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 4
3 | ---
4 |
5 | import DocCardList from "@theme/DocCardList";
6 |
7 | # Quick guide
8 |
9 | The following guide will walk you through some of our recommended best practises for making the most out of RTK Query Loader.
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/docs/Quick Guide/queriesArg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/docs/Quick Guide/queriesArg.png
--------------------------------------------------------------------------------
/docs/docs/Quick Guide/rtk-query-loader-chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/docs/Quick Guide/rtk-query-loader-chart.png
--------------------------------------------------------------------------------
/docs/docs/Reference/aggregate-to-query.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 7
3 | ---
4 |
5 | # `aggregateToQuery()`
6 |
7 | Aggregates a set of `UseQueryResult` into a single query.
8 |
9 | ```typescript
10 | import {
11 | aggregateToQuery,
12 | UseQueryResult,
13 | } from "@ryfylke-react/rtk-query-loader";
14 |
15 | const queries = [
16 | useQueryOne(),
17 | useQueryTwo(),
18 | useQueryThree(),
19 | ] as const;
20 |
21 | const query: UseQueryResult = {
22 | ...aggregateToQuery(queries),
23 | data: queries.map((query) => query.data),
24 | };
25 | ```
26 |
27 | :::caution
28 | `aggregateToQuery` does not add any `data` to the resulting query. You will have to do that manually afterwards.
29 | :::
30 |
--------------------------------------------------------------------------------
/docs/docs/Reference/awaitloader.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 5
3 | ---
4 |
5 | # ` `
6 |
7 | > **New in version 1.0.4**
8 |
9 | AwaitLoader is a component that consumes a loader and then renders the proper load/fetch/error states, and `props.render` on success.
10 |
11 | ## Arguments
12 |
13 | - **`loader`** - The loader that contains the queries you want to await.
14 | - **`render`** - A function that recieves the loader data and should return a ReactElement.
15 | - **`args`** - Takes in the expected props for the given loader.
16 |
17 | ```tsx
18 | const loader = createLoader({
19 | queriesArg: (props: { name: string }) => props.name,
20 | useQueries: (name) => ({
21 | queries: {
22 | pokemon: useGetPokemonQuery(name),
23 | },
24 | }),
25 | });
26 |
27 | const App = () => {
28 | return (
29 |
30 |
(
34 | {JSON.stringify(data.queries.pokemon.data)}
35 | )}
36 | />
37 | (
41 | {JSON.stringify(data.queries.pokemon.data)}
42 | )}
43 | />
44 |
45 | );
46 | };
47 | ```
48 |
--------------------------------------------------------------------------------
/docs/docs/Reference/consumer-props.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 6
3 | ---
4 |
5 | # `ConsumerProps<>`
6 |
7 | Helper for typing your expected consumer props for the loader.
8 |
9 | ```typescript {1-3,6}
10 | type PokemonLoaderProps = ConsumerProps<{
11 | name: string;
12 | }>;
13 |
14 | const pokemonLoader = createLoader({
15 | queriesArg: (props: PokemonLoaderProps) => props.name,
16 | useQueries: (name) => ({
17 | queries: { pokemon: useGetPokemon(name) },
18 | }),
19 | });
20 |
21 | // pokemonLoader can now we used by any component
22 | // that accepts a prop `name` of type `string`.
23 |
24 | type PokemonComponentProps = {
25 | name: string;
26 | size: number;
27 | }; // ✅
28 |
29 | type OtherPokemonComponentProps = {
30 | name: string;
31 | }; // ✅
32 |
33 | type IncompatibleProps = {
34 | pokemonName: string;
35 | }; // ❌
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/docs/Reference/create-loader.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # `createLoader()`
6 |
7 | Creates a `Loader`.
8 |
9 | ## Arguments
10 |
11 | All arguments are optional.
12 |
13 | - **`useQueries`** - A hook that runs the queries and returns them. It can take an argument, which can be transformed from the consumer props through `queriesArg`
14 | ```typescript
15 | useQueries: (arg?: Arg) => {
16 | queries?: Record;
17 | deferredQueries?: Record;
18 | payload?: any;
19 | }
20 | ```
21 | - **`queriesArg`** - A function that transforms the consumer props into the argument for `useQueries` - required if your loader should be able to take arguments from props.
22 | ```typescript
23 | queriesArg: (props: Props) => Arg;
24 | ```
25 | - **`transform`** - A function that lets you transform the shape of the output data.
26 | ```typescript
27 | transform: (data: Data) => TransformedData;
28 | ```
29 | - **`onLoading`** - A function that determines what to render while the component is going through it's initial load phase.
30 | ```typescript
31 | onLoading: (props: Props) => ReactElement;
32 | ```
33 | - **`onError`** - A function that determines what to render while the component is going through it's initial load phase.
34 | ```typescript
35 | onError: (props: Props, error: unknown) => ReactElement;
36 | ```
37 | - **`whileFetching`** - An object that lets you append &/ prepend elements to your component while it is fetching.
38 | ```typescript
39 | whileFetching: {
40 | prepend?: (props: Props, data?: Data) => ReactElement;
41 | append?: (props: Props, data?: Data) => ReactElement;
42 | };
43 | ```
44 | - **`config`** - An object that lets you configure the behavior of the loader.
45 | ```typescript
46 | config: {
47 | defer? {
48 | shouldThrowError?: boolean;
49 | };
50 | loaderComponent?: Component;
51 | }
52 | ```
53 |
54 | ## Return
55 |
56 | `createLoader` returns a `Loader`. You can use this loader with [withLoader](with-loader.md), or you can call the `.extend` method to produce a new loader using the original loader as a base. `.extend` takes the same set of arguments as `createLoader`.
57 |
58 | ## Example usage
59 |
60 | ```typescript title="example.ts"
61 | type ConsumerProps = Record & {
62 | userId: string;
63 | };
64 |
65 | const loader = createLoader({
66 | queriesArg: (props: ConsumerProps) => props.userId,
67 | useQueries: (userId) => {
68 | return {
69 | queries: {
70 | user: useGetUser(userId),
71 | },
72 | deferredQueries: {
73 | relations: useGetUserRelations(userId),
74 | },
75 | payload: {
76 | // Lets you send any static data to your consumer
77 | static: "data",
78 | },
79 | };
80 | },
81 | transform: (loaderData) => ({
82 | ...loaderData,
83 | foo: "bar",
84 | }),
85 | onLoading: (props) => ,
86 | onError: (props, error) => ,
87 | whileFetching: {
88 | prepend: (props) => ,
89 | },
90 | });
91 | ```
92 |
93 | :::caution Using version `0.3.51` or earlier?
94 | Please refer to the [**migration guide**](../Migrations/v0.x).
95 | :::
96 |
--------------------------------------------------------------------------------
/docs/docs/Reference/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 6
3 | sidebar_collapsed: false
4 | ---
5 |
6 | import DocCardList from "@theme/DocCardList";
7 |
8 | # Reference
9 |
10 | Describes the different exports and their related types.
11 |
12 | All the types can be found [here](https://github.com/ryfylke-react-as/rtk-query-loader/blob/main/src/types.ts).
13 |
14 |
15 |
--------------------------------------------------------------------------------
/docs/docs/Reference/infer-loader-data.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 5
3 | ---
4 |
5 | # `InferLoaderData<>`
6 |
7 | Returns the return type of a given `Loader`:
8 |
9 | ```typescript
10 | import {
11 | createLoader,
12 | InferLoaderData,
13 | } from "@ryfylke-react/rtk-query-loader";
14 |
15 | const loader = createLoader({...});
16 |
17 | type LoaderData = InferLoaderData;
18 | ```
19 |
--------------------------------------------------------------------------------
/docs/docs/Reference/use-create-query.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # `useCreateQuery()`
6 |
7 | Lets you use any function that returns a promise to load your component.
8 |
9 | ## Arguments
10 |
11 | `useCreateQuery` takes two arguments:
12 |
13 | - The first argument is a function that returns a promise.
14 | - The second argument is a dependency array. Whenever a value in this array changes, the query is re-run.
15 |
16 | ## Example usage
17 |
18 | ```typescript
19 | import {
20 | createLoader,
21 | useCreateQuery,
22 | } from "@ryfylke-react/rtk-query-loader";
23 |
24 | const getUser = async (userId: string) => {
25 | const res = await fetch(`users/${userId}`);
26 | const json = await res.json();
27 | return json as SomeDataType;
28 | };
29 |
30 | const loader = createLoader({
31 | useQueries: (userId: string) => {
32 | const query = useCreateQuery(
33 | async () => await getUser(userId),
34 | [userId]
35 | );
36 |
37 | return {
38 | queries: {
39 | query,
40 | },
41 | };
42 | },
43 | });
44 | ```
45 |
46 | :::caution
47 | You lose some great features from RTK query when using `useCreateQuery`, like global query invalidation (beyond the dependency array), request cancellation and caching.
48 | :::
49 |
--------------------------------------------------------------------------------
/docs/docs/Reference/with-loader.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | # `withLoader()`
6 |
7 | Wraps a component and provides it with a given `Loader`'s data. Renders the appropriate load/fetch/error states of the loader, and finally the given component when data is loaded successfully.
8 |
9 | ## Arguments
10 |
11 | `withLoader` takes two arguments
12 |
13 | - The first argument is a component, but with an extra parameter for the loaded data.
14 | - The second argument is a `Loader` (returned from [`createLoader`](./create-loader.md))
15 |
16 | ```typescript
17 | type Arg1 = (props: P, loaderData: R) => ReactElement;
18 | type Arg2 = Loader;
19 | ```
20 |
21 | ## Example usage
22 |
23 | ```tsx
24 | const pokemonLoader = createLoader({
25 | useQueries: () => ({
26 | queries: { pokemon: useGetPokemon() },
27 | }),
28 | });
29 |
30 | const Pokemon = withLoader((props, { queries }) => {
31 | return {queries.pokemon.data.name}
;
32 | }, pokemonLoader);
33 | ```
34 |
35 | Here is another example, where the arguments are all unwrapped:
36 |
37 | ```tsx
38 | const pokemonLoader = createLoader({
39 | useQueries: () => ({
40 | queries: { pokemon: useGetPokemon() },
41 | }),
42 | });
43 |
44 | type PokemonData = InferLoaderData;
45 |
46 | const LoadedComponent = (props, loaderData: PokemonData) => {
47 | return {loaderData.queries.pokemon.data.name}
;
48 | };
49 |
50 | const Pokemon = withLoader(LoadedComponent, pokemonLoader);
51 | ```
52 |
--------------------------------------------------------------------------------
/docs/docs/examples.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 4
3 | ---
4 |
5 | import TOCInline from "@theme/TOCInline";
6 |
7 | # Examples
8 |
9 | This page contains a collection of examples that cover typical use cases for the library.
10 |
11 |
12 |
13 | ## React Router (route loader)
14 |
15 | By utilizing `useParams` from `react-router-dom` we can create a loader that will load data based on the route parameters.
16 |
17 | ```tsx
18 | import {
19 | withLoader,
20 | createLoader,
21 | } from "@ryfylke-react/rtk-query-loader";
22 | import { useParams, Redirect } from "react-router-dom";
23 | // ...
24 |
25 | const userRouteLoader = createLoader({
26 | useQueries: () => {
27 | const { userId } = useParams();
28 | const userQuery = useGetUserQuery(userId, {
29 | skip: userId ? false : true,
30 | });
31 |
32 | return {
33 | queries: {
34 | userQuery,
35 | },
36 | };
37 | },
38 | onLoading: (props) => {
39 | const { userId } = useParams();
40 | if (!userId) {
41 | return ;
42 | }
43 | return ;
44 | },
45 | onError: (props, error) => ,
46 | });
47 | ```
48 |
49 | ## Reusable loader
50 |
51 | We can also create a reusable loader that can be used with multiple components.
52 |
53 | ```tsx
54 | import {
55 | withLoader,
56 | createLoader,
57 | ConsumerProps,
58 | } from "@ryfylke-react/rtk-query-loader";
59 |
60 | type UserLoaderProps = {
61 | userId: string;
62 | };
63 |
64 | const userLoader = createLoader({
65 | queriesArg: (props: UserLoaderProps) => props.userId,
66 | useQueries: (userId) => {
67 | const userQuery = useGetUserQuery(userId);
68 |
69 | return {
70 | queries: {
71 | userQuery,
72 | },
73 | };
74 | },
75 | onLoading: (props) => ,
76 | onError: (props, error) => ,
77 | });
78 | ```
79 |
80 | You can now use the `userLoader` in any component whos props extend `UserLoaderProps`.
81 |
82 | ### Consumer 1
83 |
84 | ```tsx title="UserProfile.tsx"
85 | import { userLoader } from "../loaders";
86 |
87 | type UserProfileProps = {
88 | userId: string;
89 | // ... other props
90 | };
91 |
92 | export const UserProfile = withLoader(
93 | (props: UserProfileProps, data) => {
94 | return <>...>;
95 | },
96 | userLoader
97 | );
98 | ```
99 |
100 | ### Consumer 2
101 |
102 | ```tsx title="UserProfile.tsx"
103 | import { userLoader } from "../loaders";
104 |
105 | type InlineUserDetailsProps = {
106 | userId: string;
107 | dir: "row" | "column";
108 | // ... other props
109 | };
110 |
111 | export const InlineUserDetails = withLoader(
112 | (props: InlineUserDetailsProps, data) => {
113 | return <>...>;
114 | },
115 | userLoader
116 | );
117 | ```
118 |
119 | ## Stateful loader
120 |
121 | You can also create loaders that contain state.
122 |
123 | ```tsx
124 | const loader = createLoader({
125 | useQueries: () => {
126 | const [name, setName] = useState("charizard");
127 | const debouncedName = useDebounce(name, 200);
128 | const pokemon = useGetPokemon(debouncedName);
129 | return {
130 | queries: {
131 | pokemon,
132 | },
133 | payload: {
134 | name,
135 | setName,
136 | },
137 | };
138 | },
139 | });
140 |
141 | const Consumer = withLoader((props, data) => {
142 | return (
143 |
144 |
data.payload.setName(e.target.value)}
147 | />
148 |
AP: {data.queries.pokemon.data.ability_power}
149 |
150 | );
151 | }, loader);
152 | ```
153 |
--------------------------------------------------------------------------------
/docs/docs/intro.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | slug: /
4 | title: Introduction
5 | ---
6 |
7 | # RTK Query Loader
8 |
9 | > Create _loaders_ for your React components.
10 |
11 | 🔗 **Extendable**
12 | 💫 **Transformable**
13 | ♻️ **Reusable**
14 | ✅ **Properly typed**
15 |
16 | You write your components, as if the data has already loaded.
17 |
18 | ## Install
19 |
20 | ```shell
21 | npm i @ryfylke-react/rtk-query-loader
22 | ```
23 |
24 | ## Example
25 |
26 | ```tsx
27 | type Props = {
28 | name: string;
29 | };
30 |
31 | // Creating a loader
32 | const loader = createLoader({
33 | queriesArg: (props: Props) => props.name,
34 | useQueries: (name) => ({
35 | queries: {
36 | pokemon: useGetPokemonQuery(name),
37 | },
38 | }),
39 | });
40 |
41 | // Consuming the loader
42 | const Pokemon = withLoader((props, { queries }) => {
43 | return (
44 |
45 | {queries.pokemon.data.name}
46 | {/* ... */}
47 |
48 | );
49 | }, loader);
50 | ```
51 |
52 | ## Playground
53 |
54 |
61 |
--------------------------------------------------------------------------------
/docs/docs/problem-solve.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # What problem does this solve?
6 |
7 | **Handling the loading and error state of components that depend on external data can be tedious,
8 | especially when you are managing multiple queries.**
9 |
10 | ### Example
11 |
12 | This component requires some data. There are 26 lines of code that are **just** concerned with ensuring that the data is present before rendering the _actual_ component.
13 |
14 | ```typescript {2-28}
15 | const Component = () => {
16 | const pokemonQuery = useGetPokemon("charizard");
17 | const userQuery = useGetCurrentUser();
18 | const userStatsQuery = useGetUserStats();
19 |
20 | if (
21 | pokemonQuery.isLoading ||
22 | userQuery.isLoading ||
23 | userStatsQuery.isLoading
24 | ) {
25 | return ;
26 | }
27 |
28 | if (
29 | pokemonQuery.isError ||
30 | userQuery.isError ||
31 | userStatsQuery.isError
32 | ) {
33 | return (
34 |
41 | );
42 | }
43 | // Finally...
44 | return (
45 |
50 | );
51 | };
52 | ```
53 |
54 | RTK Query Loader lets you **move all of this logic out of the component**, and also make it _reusable_ and _composable_, so that other components can reuse that loader and have access to the same data.
55 |
56 | - [x] Isolate the data-loading code away from the presentational components
57 | - [x] Increase type certainty
58 | - 🔥 Way less optional chaining in your components
59 | - 🔥 You write the components as if the data is already present
60 | - [x] Composability
61 | - ♻️ Extend existing loaders
62 | - ♻️ Overwrite only select properties
63 | - [x] You're still fully in control
64 | - 🛠️ Loading/error states
65 | - 🛠️ Custom loader-component
66 | - 🛠️ Configure the behavior of your loaders
67 |
68 | ## Terminology
69 |
70 |
74 |
75 | ## Alternatives
76 |
77 | If you are using a Suspense-enabled framework, or any form of server-side rendering that can feed your components with data, then that would be a more optimal approach. [Remix](https://remix.run/docs/en/1.14.3/route/loader) has the concept of loaders built in, and NextJS is suspense enabled.
78 |
79 | If you are, however, building an SPA, or not using Next or Remix, then this package can be a great way for you gain the concept of loaders without moving to server-side rendering.
80 |
--------------------------------------------------------------------------------
/docs/docs/terminology.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/docs/terminology.png
--------------------------------------------------------------------------------
/docs/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Note: type annotations allow type checking and IDEs autocompletion
3 |
4 | const lightCodeTheme = require("prism-react-renderer/themes/github");
5 | const darkCodeTheme = require("prism-react-renderer/themes/dracula");
6 | require("dotenv").config();
7 |
8 | /** @type {import('@docusaurus/types').Config} */
9 | const config = {
10 | title: "RTK Query Loader",
11 | tagline: "Component loaders for RTK Query",
12 | favicon: "img/logo.png",
13 | // Algolia search config
14 | // themes: ["@docusaurus/theme-search-algolia"],
15 | // Set the production url of your site here
16 | url: "https://rtk-query-loader.ryfylke.dev",
17 | // Set the // pathname under which your site is served
18 | // For GitHub pages deployment, it is often '//'
19 | baseUrl: "/",
20 |
21 | // GitHub pages deployment config.
22 | // If you aren't using GitHub pages, you don't need these.
23 | organizationName: "ryfylke-react-as", // Usually your GitHub org/user name.
24 | projectName: "rtk-query-loader", // Usually your repo name.
25 |
26 | onBrokenLinks: "throw",
27 | onBrokenMarkdownLinks: "warn",
28 |
29 | // Even if you don't use internalization, you can use this field to set useful
30 | // metadata like html lang. For example, if your site is Chinese, you may want
31 | // to replace "en" with "zh-Hans".
32 | i18n: {
33 | defaultLocale: "en",
34 | locales: ["en"],
35 | },
36 |
37 | presets: [
38 | [
39 | "classic",
40 | /** @type {import('@docusaurus/preset-classic').Options} */
41 | ({
42 | docs: {
43 | sidebarPath: require.resolve("./sidebars.js"),
44 | editUrl:
45 | "https://github.com/ryfylke-react-as/rtk-query-loader/tree/main/docs/",
46 | routeBasePath: "/",
47 | },
48 | blog: false,
49 | theme: {
50 | customCss: require.resolve("./src/css/custom.css"),
51 | },
52 | }),
53 | ],
54 | ],
55 |
56 | themeConfig:
57 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
58 | ({
59 | algolia: {
60 | apiKey: process.env.ALGOLIA_API_KEY,
61 | appId: "1NCL1FSCF8",
62 | indexName: "rtk-query-loader",
63 | },
64 | // Replace with your project's social card
65 | image: "img/ryfrea-social-card.png",
66 | navbar: {
67 | title: "RTK Query Loader",
68 | logo: {
69 | alt: "Ryfylke React Logo",
70 | src: "img/logo.png",
71 | },
72 | items: [
73 | {
74 | href: "https://github.com/ryfylke-react-as/rtk-query-loader/releases",
75 | label: "Changelog",
76 | position: "right",
77 | },
78 | {
79 | href: "https://codesandbox.io/s/rtk-query-loader-1-0-0-demo-forked-du3936?file=/src/loaders/pokemonLoader.tsx",
80 | label: "Demo",
81 | position: "right",
82 | },
83 | {
84 | href: "https://www.npmjs.com/package/@ryfylke-react/rtk-query-loader",
85 | label: "NPM",
86 | position: "right",
87 | className: "icon-npm",
88 | },
89 | {
90 | href: "https://github.com/ryfylke-react-as/rtk-query-loader",
91 | label: "GitHub",
92 | position: "right",
93 | },
94 | {
95 | href: "https://github.com/ryfylke-react-as/rtk-query-loader",
96 | label: "GitHub",
97 | position: "right",
98 | className: "icon-github",
99 | },
100 | ],
101 | },
102 | footer: {
103 | style: "dark",
104 | links: [
105 | {
106 | title: "Docs",
107 | items: [
108 | {
109 | label: "Introduction",
110 | to: "/",
111 | },
112 | {
113 | label: "Quick guide",
114 | to: "/Quick Guide",
115 | },
116 | {
117 | label: "Examples",
118 | to: "/examples",
119 | },
120 | {
121 | label: "Features",
122 | to: "/Features",
123 | },
124 | {
125 | label: "Reference",
126 | to: "/Reference",
127 | },
128 | ],
129 | },
130 | {
131 | title: "Resources",
132 | items: [
133 | {
134 | label: "GitHub",
135 | href: "https://github.com/ryfylke-react-as/rtk-query-loader",
136 | },
137 | {
138 | label: "NPM",
139 | href: "https://www.npmjs.com/package/@ryfylke-react/rtk-query-loader",
140 | },
141 | {
142 | label: "Demo (CodeSandbox)",
143 | href: "https://codesandbox.io/s/rtk-query-loader-1-0-0-demo-forked-du3936?file=/src/loaders/pokemonLoader.tsx",
144 | },
145 | ],
146 | },
147 | {
148 | title: "More from Ryfylke React",
149 | items: [
150 | {
151 | label: "Ryfylke React Toast",
152 | href: "https://toast.ryfylke.dev",
153 | },
154 | {
155 | label: "More",
156 | href: "https://open-source.ryfylke.dev",
157 | },
158 | ],
159 | },
160 | ],
161 | copyright: ` Open source / GPL-3.0 License Made with ❤️ by Ryfylke React `,
162 | },
163 | prism: {
164 | theme: lightCodeTheme,
165 | darkTheme: darkCodeTheme,
166 | },
167 | }),
168 | };
169 |
170 | module.exports = config;
171 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids",
15 | "typecheck": "tsc"
16 | },
17 | "dependencies": {
18 | "@docusaurus/core": "^2.4.0",
19 | "@docusaurus/preset-classic": "^2.4.0",
20 | "@docusaurus/theme-search-algolia": "^2.4.0",
21 | "@mdx-js/react": "^1.6.22",
22 | "@reduxjs/toolkit": "^1.9.3",
23 | "clsx": "^1.2.1",
24 | "prism-react-renderer": "^1.3.5",
25 | "react": "^17.0.2",
26 | "react-dom": "^17.0.2",
27 | "react-redux": "^8.0.5"
28 | },
29 | "devDependencies": {
30 | "@docusaurus/module-type-aliases": "2.3.1",
31 | "@tsconfig/docusaurus": "^1.0.5",
32 | "dotenv": "^16.0.3",
33 | "typescript": "^4.7.4"
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.5%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | },
47 | "engines": {
48 | "node": ">=16.14"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/docs/sidebars.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creating a sidebar enables you to:
3 | - create an ordered group of docs
4 | - render a sidebar for each doc of that group
5 | - provide next/previous navigation
6 |
7 | The sidebars can be generated from the filesystem, or explicitly defined here.
8 |
9 | Create as many sidebars as you want.
10 | */
11 |
12 | // @ts-check
13 |
14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
15 | const sidebars = {
16 | // By default, Docusaurus generates a sidebar from the docs folder structure
17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
18 |
19 | // But you can create a sidebar manually
20 | /*
21 | tutorialSidebar: [
22 | 'intro',
23 | 'hello',
24 | {
25 | type: 'category',
26 | label: 'Tutorial',
27 | items: ['tutorial-basics/create-a-document'],
28 | },
29 | ],
30 | */
31 | };
32 |
33 | module.exports = sidebars;
34 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import clsx from 'clsx';
3 | import styles from './styles.module.css';
4 |
5 | type FeatureItem = {
6 | title: string;
7 | Svg: React.ComponentType>;
8 | description: JSX.Element;
9 | };
10 |
11 | const FeatureList: FeatureItem[] = [
12 | {
13 | title: 'Easy to Use',
14 | Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
15 | description: (
16 | <>
17 | Docusaurus was designed from the ground up to be easily installed and
18 | used to get your website up and running quickly.
19 | >
20 | ),
21 | },
22 | {
23 | title: 'Focus on What Matters',
24 | Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
25 | description: (
26 | <>
27 | Docusaurus lets you focus on your docs, and we'll do the chores. Go
28 | ahead and move your docs into the docs
directory.
29 | >
30 | ),
31 | },
32 | {
33 | title: 'Powered by React',
34 | Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
35 | description: (
36 | <>
37 | Extend or customize your website layout by reusing React. Docusaurus can
38 | be extended while reusing the same header and footer.
39 | >
40 | ),
41 | },
42 | ];
43 |
44 | function Feature({title, Svg, description}: FeatureItem) {
45 | return (
46 |
47 |
48 |
49 |
50 |
51 |
{title}
52 |
{description}
53 |
54 |
55 | );
56 | }
57 |
58 | export default function HomepageFeatures(): JSX.Element {
59 | return (
60 |
61 |
62 |
63 | {FeatureList.map((props, idx) => (
64 |
65 | ))}
66 |
67 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/styles.module.css:
--------------------------------------------------------------------------------
1 | .features {
2 | display: flex;
3 | align-items: center;
4 | padding: 2rem 0;
5 | width: 100%;
6 | }
7 |
8 | .featureSvg {
9 | height: 200px;
10 | width: 200px;
11 | }
12 |
--------------------------------------------------------------------------------
/docs/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 |
7 | /* You can override the default Infima variables here. */
8 | :root {
9 | --ifm-color-primary: #2e8555;
10 | --ifm-color-primary-dark: #29784c;
11 | --ifm-color-primary-darker: #277148;
12 | --ifm-color-primary-darkest: #205d3b;
13 | --ifm-color-primary-light: #33925d;
14 | --ifm-color-primary-lighter: #359962;
15 | --ifm-color-primary-lightest: #3cad6e;
16 | --ifm-code-font-size: 95%;
17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
18 | }
19 |
20 | /* For readability concerns, you should choose a lighter palette in dark mode. */
21 | [data-theme="dark"] {
22 | --ifm-color-primary: #25c2a0;
23 | --ifm-color-primary-dark: #21af90;
24 | --ifm-color-primary-darker: #1fa588;
25 | --ifm-color-primary-darkest: #1a8870;
26 | --ifm-color-primary-light: #29d5b0;
27 | --ifm-color-primary-lighter: #32d8b4;
28 | --ifm-color-primary-lightest: #4fddbf;
29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
30 | }
31 |
32 | /* .icon-npm {
33 | display: flex;
34 | align-items: center;
35 | gap: 0.5rem;
36 | font-size: 0px;
37 | }
38 | .icon-npm svg {
39 | display: none;
40 | }
41 | .icon-npm::after {
42 | background-color: rgb(55, 55, 55);
43 | background-image: url("https://img.shields.io/npm/v/@ryfylke-react/rtk-query-loader?color=gray&style=flat-square");
44 | width: 80px;
45 | height: 20px;
46 | content: "";
47 | display: flex;
48 | border: 1px solid transparent;
49 | }
50 | .icon-npm:hover::after,
51 | .icon-npm:focus::after {
52 | border: 1px solid currentColor;
53 | } */
54 |
55 | .icon-github {
56 | display: flex;
57 | align-items: center;
58 | gap: 0.5rem;
59 | font-size: 0px;
60 | }
61 | .icon-github svg {
62 | display: none;
63 | }
64 | .icon-github::after {
65 | background-image: url("https://img.shields.io/github/stars/ryfylke-react-as/rtk-query-loader?style=social");
66 | width: 82px;
67 | height: 20px;
68 | content: "";
69 | transition: transform 0.1s ease-in-out;
70 | display: flex;
71 | }
72 | .icon-github:hover::after,
73 | .icon-github:focus::after {
74 | transform: scale(1.05);
75 | }
76 |
77 | [data-theme="light"] .DocSearch {
78 | /* --docsearch-primary-color: var(--ifm-color-primary); */
79 | /* --docsearch-text-color: var(--ifm-font-color-base); */
80 | --docsearch-muted-color: var(--ifm-color-secondary-darkest);
81 | --docsearch-container-background: rgba(94, 100, 112, 0.7);
82 | /* Modal */
83 | --docsearch-modal-background: var(
84 | --ifm-color-secondary-lighter
85 | );
86 | /* Search box */
87 | --docsearch-searchbox-background: var(--ifm-color-secondary);
88 | --docsearch-searchbox-focus-background: var(--ifm-color-white);
89 | /* Hit */
90 | --docsearch-hit-color: var(--ifm-font-color-base);
91 | --docsearch-hit-active-color: var(--ifm-color-white);
92 | --docsearch-hit-background: var(--ifm-color-white);
93 | /* Footer */
94 | --docsearch-footer-background: var(--ifm-color-white);
95 | }
96 |
97 | [data-theme="dark"] .DocSearch {
98 | --docsearch-text-color: var(--ifm-font-color-base);
99 | --docsearch-muted-color: var(--ifm-color-secondary-darkest);
100 | --docsearch-container-background: rgba(47, 55, 69, 0.7);
101 | /* Modal */
102 | --docsearch-modal-background: var(--ifm-background-color);
103 | /* Search box */
104 | --docsearch-searchbox-background: var(--ifm-background-color);
105 | --docsearch-searchbox-focus-background: var(--ifm-color-black);
106 | /* Hit */
107 | --docsearch-hit-color: var(--ifm-font-color-base);
108 | --docsearch-hit-active-color: var(--ifm-color-white);
109 | --docsearch-hit-background: var(--ifm-color-emphasis-100);
110 | /* Footer */
111 | --docsearch-footer-background: var(
112 | --ifm-background-surface-color
113 | );
114 | --docsearch-key-gradient: linear-gradient(
115 | -26.5deg,
116 | var(--ifm-color-emphasis-200) 0%,
117 | var(--ifm-color-emphasis-100) 100%
118 | );
119 | }
120 |
121 | .DocSearch-Button-Placeholder {
122 | padding: 0 12px !important;
123 | }
124 | .DocSearch-Button-Container svg {
125 | --size: 14px;
126 | width: var(--size) !important;
127 | height: var(--size) !important;
128 | margin-left: 8px;
129 | }
130 |
131 | .navbar {
132 | background: linear-gradient(350deg, #e2e2f3, rgb(239 254 239));
133 | }
134 |
135 | [data-theme="dark"] .navbar {
136 | background: linear-gradient(350deg, #1b1b1d, #262b2e);
137 | }
138 |
--------------------------------------------------------------------------------
/docs/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/static/.nojekyll
--------------------------------------------------------------------------------
/docs/static/img/docusaurus-social-card.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/static/img/docusaurus-social-card.jpg
--------------------------------------------------------------------------------
/docs/static/img/docusaurus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/static/img/docusaurus.png
--------------------------------------------------------------------------------
/docs/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/static/img/favicon.ico
--------------------------------------------------------------------------------
/docs/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/static/img/logo.png
--------------------------------------------------------------------------------
/docs/static/img/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/static/img/ryfrea-social-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryfylke-react-as/rtk-query-loader/3ed7aa5c3be00afd8d6adacb8a97a45bc27330a0/docs/static/img/ryfrea-social-card.png
--------------------------------------------------------------------------------
/docs/static/img/undraw_docusaurus_tree.svg:
--------------------------------------------------------------------------------
1 |
2 | Focus on What Matters
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // This file is not used in compilation. It is here just for a nice editor experience.
3 | "extends": "@tsconfig/docusaurus/tsconfig.json",
4 | "compilerOptions": {
5 | "baseUrl": "."
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ryfylke-react/rtk-query-loader",
3 | "version": "1.0.4",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@ryfylke-react/rtk-query-loader",
9 | "version": "1.0.4",
10 | "license": "ISC",
11 | "devDependencies": {
12 | "@reduxjs/toolkit": "^2.2.3",
13 | "@types/react": "^18.0.21",
14 | "react": "^18.2.0",
15 | "tslib": "^2.4.0"
16 | },
17 | "peerDependencies": {
18 | "@reduxjs/toolkit": "^2.2.3"
19 | }
20 | },
21 | "node_modules/@reduxjs/toolkit": {
22 | "version": "2.2.5",
23 | "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.5.tgz",
24 | "integrity": "sha512-aeFA/s5NCG7NoJe/MhmwREJxRkDs0ZaSqt0MxhWUrwCf1UQXpwR87RROJEql0uAkLI6U7snBOYOcKw83ew3FPg==",
25 | "dev": true,
26 | "dependencies": {
27 | "immer": "^10.0.3",
28 | "redux": "^5.0.1",
29 | "redux-thunk": "^3.1.0",
30 | "reselect": "^5.1.0"
31 | },
32 | "peerDependencies": {
33 | "react": "^16.9.0 || ^17.0.0 || ^18",
34 | "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
35 | },
36 | "peerDependenciesMeta": {
37 | "react": {
38 | "optional": true
39 | },
40 | "react-redux": {
41 | "optional": true
42 | }
43 | }
44 | },
45 | "node_modules/@types/prop-types": {
46 | "version": "15.7.12",
47 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
48 | "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
49 | "dev": true
50 | },
51 | "node_modules/@types/react": {
52 | "version": "18.3.3",
53 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
54 | "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
55 | "dev": true,
56 | "dependencies": {
57 | "@types/prop-types": "*",
58 | "csstype": "^3.0.2"
59 | }
60 | },
61 | "node_modules/csstype": {
62 | "version": "3.1.3",
63 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
64 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
65 | "dev": true
66 | },
67 | "node_modules/immer": {
68 | "version": "10.1.1",
69 | "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
70 | "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
71 | "dev": true,
72 | "funding": {
73 | "type": "opencollective",
74 | "url": "https://opencollective.com/immer"
75 | }
76 | },
77 | "node_modules/js-tokens": {
78 | "version": "4.0.0",
79 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
80 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
81 | "dev": true
82 | },
83 | "node_modules/loose-envify": {
84 | "version": "1.4.0",
85 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
86 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
87 | "dev": true,
88 | "dependencies": {
89 | "js-tokens": "^3.0.0 || ^4.0.0"
90 | },
91 | "bin": {
92 | "loose-envify": "cli.js"
93 | }
94 | },
95 | "node_modules/react": {
96 | "version": "18.3.1",
97 | "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
98 | "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
99 | "dev": true,
100 | "dependencies": {
101 | "loose-envify": "^1.1.0"
102 | },
103 | "engines": {
104 | "node": ">=0.10.0"
105 | }
106 | },
107 | "node_modules/redux": {
108 | "version": "5.0.1",
109 | "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
110 | "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
111 | "dev": true
112 | },
113 | "node_modules/redux-thunk": {
114 | "version": "3.1.0",
115 | "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
116 | "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
117 | "dev": true,
118 | "peerDependencies": {
119 | "redux": "^5.0.0"
120 | }
121 | },
122 | "node_modules/reselect": {
123 | "version": "5.1.0",
124 | "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz",
125 | "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==",
126 | "dev": true
127 | },
128 | "node_modules/tslib": {
129 | "version": "2.6.2",
130 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
131 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
132 | "dev": true
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ryfylke-react/rtk-query-loader",
3 | "version": "1.0.7",
4 | "description": "Lets you create reusable, extendable RTK loaders for React components.",
5 | "main": "./dist/cjs/index.js",
6 | "module": "./dist/esm/index.js",
7 | "types": "./dist/esm/index.d.ts",
8 | "scripts": {
9 | "build": "yarn build:esm && yarn build:cjs",
10 | "build:esm": "tsc",
11 | "build:cjs": "tsc --module commonjs --outDir dist/cjs",
12 | "prepare": "yarn build",
13 | "setup-link": "npm link && cd testing-app && npm link @ryfylke-react/rtk-query-loader"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/ryfylke-react-as/rtk-query-loader.git"
18 | },
19 | "author": "Ryfylke React AS",
20 | "license": "ISC",
21 | "bugs": {
22 | "url": "https://github.com/ryfylke-react-as/rtk-query-loader/issues"
23 | },
24 | "homepage": "https://rtk-query-loader.ryfylke.dev",
25 | "devDependencies": {
26 | "@types/react": "^18.2.37",
27 | "@types/react-dom": "^18.2.15",
28 | "@reduxjs/toolkit": "^1.6.2",
29 | "react": "^18.2.0",
30 | "tslib": "^2.4.0",
31 | "typescript": "^5.4.5"
32 | },
33 | "peerDependencies": {
34 | "@reduxjs/toolkit": "^1.6.2"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/AwaitLoader.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as Types from "./types";
3 | import { withLoader } from "./withLoader";
4 | type ReactType = typeof React;
5 | let R = React;
6 |
7 | type AwaitLoaderProps<
8 | TProps extends Record,
9 | TReturn,
10 | TArg = never
11 | > = [TArg] extends [never]
12 | ? {
13 | loader: Types.Loader;
14 | render: (data: TReturn) => React.ReactElement;
15 | }
16 | : {
17 | loader: Types.Loader;
18 | render: (data: TReturn) => React.ReactElement;
19 | args: TProps;
20 | };
21 |
22 | /**
23 | * @typedef AwaitLoaderProps
24 | * @type {Object}
25 | * @property {Types.Loader} loader The loader to use.
26 | * @property {(data: TReturn) => React.ReactElement} render The render function to use.
27 | * @property {TProps} args The arguments to pass to the loader.
28 | */
29 |
30 | /**
31 | * A component that awaits a loader and renders the data.
32 | * @param {AwaitLoaderProps} args The arguments to pass to the loader.
33 | */
34 | export const AwaitLoader = <
35 | TProps extends Record,
36 | TReturn extends unknown,
37 | TArg = never
38 | >(
39 | args: AwaitLoaderProps
40 | ) => {
41 | const Component = R.useCallback(
42 | withLoader(
43 | (_, loaderData) => args.render(loaderData),
44 | args.loader
45 | ),
46 | []
47 | );
48 | return R.createElement(
49 | Component,
50 | "args" in args ? args.args : ({} as TProps),
51 | null
52 | );
53 | };
54 |
55 | export const _testLoad = (react: any) => {
56 | R = react as ReactType;
57 | return AwaitLoader;
58 | };
59 |
--------------------------------------------------------------------------------
/src/RTKLoader.tsx:
--------------------------------------------------------------------------------
1 | import { SerializedError } from "@reduxjs/toolkit";
2 | import * as React from "react";
3 | import { CustomLoaderProps } from "./types";
4 |
5 | /**
6 | * The default loader component for use with RTK Query Loader.
7 | */
8 | export function RTKLoader(
9 | props: CustomLoaderProps
10 | ): React.ReactElement {
11 | const shouldLoad =
12 | props.query.isLoading || props.query.isUninitialized;
13 | const hasError = props.query.isError && props.query.error;
14 | const isFetching = props.query.isFetching;
15 |
16 | if (shouldLoad) {
17 | return props.onLoading ?? ;
18 | }
19 |
20 | if (hasError) {
21 | return (
22 | props.onError?.(props.query.error as SerializedError) ?? (
23 |
24 | )
25 | );
26 | }
27 |
28 | if (isFetching && props.onFetching) {
29 | return props.onFetching;
30 | }
31 |
32 | if (props.query.data !== undefined) {
33 | const prepend = isFetching
34 | ? props.whileFetching?.prepend ?? null
35 | : null;
36 | const append = isFetching
37 | ? props.whileFetching?.append ?? null
38 | : null;
39 | const componentWithData = props.onSuccess(props.query.data);
40 |
41 | return (
42 | <>
43 | {prepend}
44 | {componentWithData}
45 | {append}
46 | >
47 | );
48 | }
49 |
50 | return ;
51 | }
52 |
--------------------------------------------------------------------------------
/src/aggregateToQuery.ts:
--------------------------------------------------------------------------------
1 | import * as Types from "./types";
2 |
3 | export const aggregateToQuery = (
4 | queries: readonly Types.UseQueryResult[]
5 | ): Types.UseQueryResult => {
6 | const isLoading = queries.some((query) => query.isLoading);
7 | const isError = queries.some(
8 | (query) =>
9 | query.isError ||
10 | (query.isFetching && query.error && !query.data)
11 | );
12 | const isFetching = queries.some((query) => query.isFetching);
13 | const isUninitialized = queries.some(
14 | (query) => query.isUninitialized
15 | );
16 | const isSuccess = !isUninitialized && !isLoading && !isError;
17 | const error = queries.find(
18 | (query) => query.error !== undefined
19 | )?.error;
20 | const fulfilledTimeStamp = Math.max(
21 | ...queries
22 | .filter(
23 | (query) => typeof query.fulfilledTimeStamp === "number"
24 | )
25 | .map((query) => query.fulfilledTimeStamp as number)
26 | );
27 | const startedTimeStamp = Math.max(
28 | ...queries
29 | .filter(
30 | (query) => typeof query.startedTimeStamp === "number"
31 | )
32 | .map((query) => query.startedTimeStamp as number)
33 | );
34 | const requestId = queries
35 | .filter((query) => typeof query.requestId === "string")
36 | .map((query) => query.requestId)
37 | .join("");
38 |
39 | const refetch = () => {
40 | queries.forEach((query) => query.refetch());
41 | };
42 |
43 | return {
44 | isLoading,
45 | isError,
46 | isFetching,
47 | isSuccess,
48 | isUninitialized,
49 | refetch,
50 | error,
51 | fulfilledTimeStamp,
52 | startedTimeStamp,
53 | requestId,
54 | };
55 | };
56 |
--------------------------------------------------------------------------------
/src/createLoader.ts:
--------------------------------------------------------------------------------
1 | import { aggregateToQuery } from "./aggregateToQuery";
2 | import { RTKLoader } from "./RTKLoader";
3 | import * as Types from "./types";
4 |
5 | const mergeConfig = (
6 | original: Types.LoaderConfig,
7 | extended: Types.LoaderConfig
8 | ): Types.LoaderConfig => {
9 | if (!original) return extended;
10 | return {
11 | ...original,
12 | ...extended,
13 | deferred: {
14 | ...original.deferred,
15 | ...extended.deferred,
16 | },
17 | };
18 | };
19 |
20 | export const createUseLoader = <
21 | TQueries extends Types._TQueries,
22 | TDeferred extends Types._TDeferred,
23 | TPayload extends Types._TPayload,
24 | TReturn extends unknown = Types.ResolveDataShape<
25 | Types.MakeDataRequired,
26 | TDeferred,
27 | TPayload
28 | >,
29 | TArg = never
30 | >(
31 | createUseLoaderArgs: Types.CreateUseLoaderArgs<
32 | TQueries,
33 | TDeferred,
34 | TPayload,
35 | TReturn,
36 | TArg
37 | >
38 | ): Types.UseLoader<
39 | TArg,
40 | TReturn,
41 | TQueries,
42 | TDeferred,
43 | TPayload
44 | > => {
45 | const useLoader = (
46 | ...args: Types.OptionalGenericArg
47 | ) => {
48 | const loaderRes = createUseLoaderArgs.useQueries(...args);
49 | const queriesList = loaderRes.queries
50 | ? Object.keys(loaderRes.queries).map(
51 | (key) => (loaderRes.queries as TQueries)[key]
52 | )
53 | : [];
54 | const aggregatedQuery = aggregateToQuery(queriesList);
55 |
56 | if (createUseLoaderArgs.config?.deferred?.shouldThrowError) {
57 | if (loaderRes.deferredQueries) {
58 | const deferredQueriesList = Object.keys(
59 | loaderRes.deferredQueries
60 | ).map(
61 | (key) => (loaderRes.deferredQueries as TDeferred)[key]
62 | );
63 | if (deferredQueriesList.some((q) => q.isError)) {
64 | aggregatedQuery.isSuccess = false;
65 | aggregatedQuery.isError = true;
66 | aggregatedQuery.error = deferredQueriesList.find(
67 | (q) => q.isError
68 | )?.error;
69 | }
70 | }
71 | }
72 |
73 | if (aggregatedQuery.isSuccess || queriesList.length === 0) {
74 | const data = createUseLoaderArgs.transform
75 | ? createUseLoaderArgs.transform(
76 | loaderRes as Types.ResolveDataShape<
77 | Types.MakeDataRequired,
78 | TDeferred,
79 | TPayload
80 | >
81 | )
82 | : loaderRes;
83 |
84 | return {
85 | ...aggregatedQuery,
86 | isSuccess: true,
87 | data,
88 | currentData: data,
89 | originalArgs: args,
90 | } as Types.UseQueryResult;
91 | }
92 |
93 | return aggregatedQuery as Types.UseQueryResult;
94 | };
95 |
96 | useLoader.original_args = createUseLoaderArgs;
97 | return useLoader;
98 | };
99 |
100 | /**
101 | * Creates a `loader` that can be used to fetch data and render error & loading states.
102 | * @example
103 | * const loader = createLoader({
104 | * queriesArg: (props) => props.userId,
105 | * useQueries: (userId) => {
106 | * const user = useGetUserQuery(userId);
107 | * return { queries: { user } };
108 | * },
109 | * onError: (error) => ,
110 | * onLoading: () => ,
111 | * });
112 | */
113 | export const createLoader = <
114 | TProps extends unknown,
115 | TQueries extends Types._TQueries,
116 | TDeferred extends Types._TDeferred,
117 | TPayload extends Types._TPayload,
118 | TReturn extends unknown = Types.ResolveDataShape<
119 | Types.MakeDataRequired,
120 | TDeferred,
121 | TPayload
122 | >,
123 | TArg extends unknown = never
124 | >(
125 | createLoaderArgs: Types.CreateLoaderArgs<
126 | TProps,
127 | TQueries,
128 | TDeferred,
129 | TPayload,
130 | TReturn,
131 | TArg
132 | >
133 | ): Types.Loader<
134 | TProps,
135 | TReturn,
136 | TQueries,
137 | TDeferred,
138 | TPayload,
139 | TArg
140 | > => {
141 | const useLoader = createUseLoader({
142 | useQueries:
143 | createLoaderArgs.useQueries ??
144 | (() => ({} as unknown as TQueries)),
145 | transform: createLoaderArgs.transform,
146 | config: createLoaderArgs.config,
147 | });
148 |
149 | const loader: Types.Loader<
150 | TProps,
151 | TReturn,
152 | TQueries,
153 | TDeferred,
154 | TPayload,
155 | TArg
156 | > = {
157 | useLoader,
158 | onLoading: createLoaderArgs.onLoading,
159 | onError: createLoaderArgs.onError,
160 | onFetching: createLoaderArgs.onFetching,
161 | whileFetching: createLoaderArgs.whileFetching,
162 | queriesArg: createLoaderArgs.queriesArg,
163 | config: createLoaderArgs.config,
164 | LoaderComponent:
165 | createLoaderArgs.config?.loaderComponent ??
166 | createLoaderArgs.loaderComponent ??
167 | RTKLoader,
168 | extend: function <
169 | E_TQueries extends Types._TQueries = TQueries,
170 | E_TDeferred extends Types._TDeferred = TDeferred,
171 | E_TPayload extends Types._TPayload = TPayload,
172 | E_TReturn extends unknown = Types.AllEql<
173 | TQueries,
174 | E_TQueries,
175 | TDeferred,
176 | E_TDeferred,
177 | TPayload,
178 | E_TPayload
179 | > extends true
180 | ? TReturn
181 | : Types.ResolveLoadedDataShape<
182 | E_TQueries,
183 | E_TDeferred,
184 | E_TPayload
185 | >,
186 | E_TProps extends unknown = TProps,
187 | E_TArg = TArg
188 | >({
189 | useQueries,
190 | transform,
191 | ...extendedArgs
192 | }: Partial<
193 | Types.CreateLoaderArgs<
194 | E_TProps,
195 | E_TQueries,
196 | E_TDeferred,
197 | E_TPayload,
198 | E_TReturn,
199 | E_TArg
200 | >
201 | >) {
202 | const original = this as unknown as Types.Loader<
203 | E_TProps,
204 | E_TReturn,
205 | E_TQueries,
206 | E_TDeferred,
207 | E_TPayload,
208 | E_TArg
209 | >;
210 | const mergedConfig = mergeConfig(
211 | original.config ?? {},
212 | extendedArgs.config ?? {}
213 | );
214 |
215 | backwardsSupportLoaderComponent(
216 | mergedConfig,
217 | extendedArgs
218 | );
219 |
220 | const extendedLoader: Types.Loader<
221 | E_TProps,
222 | E_TReturn,
223 | E_TQueries,
224 | E_TDeferred,
225 | E_TPayload,
226 | E_TArg
227 | > = {
228 | ...original,
229 | ...extendedArgs,
230 | config: mergedConfig,
231 | };
232 |
233 | if (useQueries) {
234 | const newUseLoader = createUseLoader({
235 | useQueries,
236 | transform,
237 | config: mergedConfig,
238 | });
239 | extendedLoader.useLoader = newUseLoader;
240 | } else if (transform) {
241 | const newUseLoader = createUseLoader({
242 | useQueries:
243 | extendedLoader.useLoader.original_args.useQueries,
244 | transform,
245 | config: mergedConfig,
246 | });
247 | extendedLoader.useLoader = newUseLoader;
248 | } else if (extendedArgs.config) {
249 | const newUseLoader = createUseLoader({
250 | useQueries:
251 | extendedLoader.useLoader.original_args.useQueries,
252 | transform:
253 | extendedLoader.useLoader.original_args.transform,
254 | config: mergedConfig,
255 | });
256 | extendedLoader.useLoader = newUseLoader;
257 | }
258 |
259 | return extendedLoader;
260 | },
261 | };
262 |
263 | return loader;
264 | };
265 |
266 | function backwardsSupportLoaderComponent(
267 | mergedConfig: Types.LoaderConfig,
268 | extendedArgs: Types.CreateLoaderArgs<
269 | any,
270 | any,
271 | any,
272 | any,
273 | any,
274 | any
275 | >
276 | ) {
277 | if (
278 | !mergedConfig.loaderComponent &&
279 | extendedArgs.loaderComponent
280 | ) {
281 | mergedConfig.loaderComponent = extendedArgs.loaderComponent;
282 | }
283 | return mergedConfig;
284 | }
285 |
--------------------------------------------------------------------------------
/src/createQuery.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as Types from "./types";
3 |
4 | type ReactType = typeof React;
5 | let R = React;
6 |
7 | let requestIdCount = 0;
8 | const requestIdGenerator = () => {
9 | requestIdCount += 1;
10 | return `usecreatequery-${requestIdCount}`;
11 | };
12 |
13 | /**
14 | * Creates a query from an async getter function.
15 | * @param getter The async function to get the data.
16 | * @param dependencies The dependency array to watch for changes.
17 | * @example
18 | * const query = useCreateQuery(async () => {
19 | * const response = await fetch(`/users/${userId}`);
20 | * return response.json();
21 | * }, [userId]);
22 | */
23 | export const useCreateQuery = (
24 | getter: Types.CreateQueryGetter,
25 | dependencies?: any[]
26 | ): Types.UseQueryResult => {
27 | const safeDependencies = dependencies ?? [];
28 | const [requestId] = R.useState(() => requestIdGenerator());
29 | const [state, dispatch] = R.useReducer(reducer, {
30 | isLoading: true,
31 | isSuccess: false,
32 | isError: false,
33 | isFetching: false,
34 | refetch: () => {},
35 | isUninitialized: true,
36 | currentData: undefined,
37 | data: undefined,
38 | error: undefined,
39 | endpointName: "",
40 | fulfilledTimeStamp: 0,
41 | originalArgs: safeDependencies,
42 | requestId,
43 | startedTimeStamp: 0,
44 | });
45 |
46 | const runQuery = (overrideInitialized?: boolean) => {
47 | const fetchData = async () => {
48 | try {
49 | const data = await getter();
50 | dispatch({ type: "success", payload: { data } });
51 | } catch (error) {
52 | dispatch({ type: "error", payload: { error } });
53 | }
54 | };
55 |
56 | dispatch({
57 | type: overrideInitialized
58 | ? "fetch"
59 | : state.isUninitialized
60 | ? "load"
61 | : "fetch",
62 | payload: { refetch: () => runQuery(true) },
63 | });
64 |
65 | fetchData();
66 | };
67 |
68 | R.useEffect(() => runQuery(), safeDependencies);
69 |
70 | return state as Types.UseQueryResult;
71 | };
72 |
73 | export const _testCreateUseCreateQuery = (react: any) => {
74 | R = react as ReactType;
75 | return useCreateQuery;
76 | };
77 |
78 | const reducer = (
79 | state: Types.UseQueryResult,
80 | action: Types.CreateQueryReducerAction
81 | ) => {
82 | switch (action.type) {
83 | case "load":
84 | return {
85 | ...state,
86 | isSuccess: false,
87 | isError: false,
88 | isFetching: false,
89 | isLoading: true,
90 | isUninitialized: false,
91 | startedTimeStamp: Date.now(),
92 | refetch: action.payload.refetch,
93 | };
94 | case "fetch":
95 | return {
96 | ...state,
97 | isLoading: false,
98 | isSuccess: false,
99 | isError: false,
100 | isFetching: true,
101 | isUninitialized: false,
102 | startedTimeStamp: Date.now(),
103 | refetch: action.payload.refetch,
104 | };
105 | case "success":
106 | return {
107 | ...state,
108 | isLoading: false,
109 | isFetching: false,
110 | isError: false,
111 | isUninitialized: false,
112 | isSuccess: true,
113 | data: action.payload.data,
114 | currentData: action.payload.data,
115 | fulfilledTimeStamp: Date.now(),
116 | };
117 | case "error":
118 | return {
119 | ...state,
120 | isLoading: false,
121 | isSuccess: false,
122 | isFetching: false,
123 | isUninitialized: false,
124 | isError: true,
125 | error: action.payload.error,
126 | fulfilledTimeStamp: Date.now(),
127 | };
128 | default:
129 | return state;
130 | }
131 | };
132 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { AwaitLoader } from "./AwaitLoader";
2 | export { RTKLoader } from "./RTKLoader";
3 | export { aggregateToQuery } from "./aggregateToQuery";
4 | export { createLoader, createUseLoader } from "./createLoader";
5 | export { useCreateQuery } from "./createQuery";
6 | export type {
7 | Component,
8 | ComponentWithLoaderData,
9 | ConsumerProps,
10 | CreateLoaderArgs,
11 | CreateUseLoaderArgs,
12 | CustomLoaderProps,
13 | DataShapeInput,
14 | DeferredConfig,
15 | InferLoaderData,
16 | Loader,
17 | LoaderConfig,
18 | LoaderTransformFunction,
19 | MakeDataRequired,
20 | OptionalGenericArg,
21 | UseLoader,
22 | UseQueryResult,
23 | WithLoaderArgs,
24 | _TDeferred,
25 | _TPayload,
26 | _TProps,
27 | _TQueries,
28 | _TReturn,
29 | } from "./types";
30 | export { useCreateLoader } from "./useCreateLoader";
31 | export { withLoader } from "./withLoader";
32 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { SerializedError } from "@reduxjs/toolkit";
2 | import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query";
3 | import { ReactElement } from "react";
4 |
5 | export type Unwrap = {
6 | [K in keyof T]: T[K];
7 | } & {};
8 |
9 | export type AllNever<
10 | TQueries,
11 | TDeferred,
12 | TPayload,
13 | TReturn = never
14 | > = TQueries | TDeferred | TPayload | TReturn extends never
15 | ? true
16 | : false;
17 |
18 | export type AllEql<
19 | TQueries,
20 | E_TQueries,
21 | TDeferred,
22 | E_TDeferred,
23 | TPayload,
24 | E_TPayload
25 | > = TQueries extends E_TQueries
26 | ? TDeferred extends E_TDeferred
27 | ? TPayload extends E_TPayload
28 | ? true
29 | : false
30 | : false
31 | : false;
32 |
33 | /** Result of a RTK useQuery hook */
34 | export type UseQueryResult = {
35 | // Base query state
36 | /** Arguments passed to the query */
37 | originalArgs?: unknown;
38 | /** The latest returned result regardless of hook arg, if present */
39 | data?: T;
40 | /** The latest returned result for the current hook arg, if present */
41 | currentData?: T;
42 | /** Error result if present */
43 | error?: unknown;
44 | /** A string generated by RTK Query */
45 | requestId?: string;
46 | /** The name of the given endpoint for the query */
47 | endpointName?: string;
48 | /** Timestamp for when the query was initiated */
49 | startedTimeStamp?: number;
50 | /** Timestamp for when the query was completed */
51 | fulfilledTimeStamp?: number;
52 |
53 | // Derived request status booleans
54 | /** Query has not started yet */
55 | isUninitialized: boolean;
56 | /** Query is currently loading for the first time. No data yet. */
57 | isLoading: boolean;
58 | /** Query is currently fetching, but might have data from an earlier request. */
59 | isFetching: boolean;
60 | /** Query has data from a successful load */
61 | isSuccess: boolean;
62 | /** Query is currently in an "error" state */
63 | isError: boolean;
64 |
65 | /** A function to force refetch the query */
66 | refetch: () => void;
67 | };
68 |
69 | /** _X are types that are extended from in the generics */
70 | export type _TQueries =
71 | | Record>
72 | | never;
73 | export type _TDeferred =
74 | | Record>
75 | | never;
76 | export type _TPayload = unknown | never;
77 | export type _TProps = Record;
78 | export type _TReturn = unknown;
79 |
80 | export type MakeDataRequired = {
81 | // @ts-ignore: TS2536: Type '"data"' cannot be used to index type 'T[K]'.
82 | [K in keyof T]: T[K] & { data: NonNullable };
83 | };
84 |
85 | export type DataShapeInput<
86 | TQueries extends _TQueries,
87 | TDeferred extends _TDeferred,
88 | TPayload extends _TPayload
89 | > = {
90 | queries?: TQueries;
91 | deferredQueries?: TDeferred;
92 | payload?: TPayload;
93 | /** This should be specified at the **top level** of `createLoader` or `extend` */
94 | onLoading?: never;
95 | /** This should be specified at the **top level** of `createLoader` or `extend` */
96 | onError?: never;
97 | /** This should be specified at the **top level** of `createLoader` or `extend` */
98 | onFetching?: never;
99 | /** This should be specified at the **top level** of `createLoader` or `extend` */
100 | whileFetching?: never;
101 | };
102 |
103 | export type ResolveDataShape<
104 | TQueries extends _TQueries,
105 | TDeferred extends _TDeferred,
106 | TPayload extends _TPayload
107 | > = {
108 | queries: TQueries extends never ? never : TQueries;
109 | deferredQueries: TDeferred extends never ? never : TDeferred;
110 | payload: TPayload extends never ? never : TPayload;
111 | };
112 |
113 | export type ResolveLoadedDataShape<
114 | TQueries extends _TQueries,
115 | TDeferred extends _TDeferred,
116 | TPayload extends _TPayload
117 | > = ResolveDataShape<
118 | MakeDataRequired,
119 | TDeferred,
120 | TPayload
121 | >;
122 |
123 | /** Use: `(...args: OptionalGenericArg) => void;`
124 | * Allows either `T` or `none` for the parameter
125 | */
126 | export type OptionalGenericArg = T extends never ? [] : [T];
127 |
128 | export type LoaderTransformFunction<
129 | TQueries extends _TQueries,
130 | TDeferred extends _TDeferred,
131 | TPayload extends _TPayload,
132 | TReturn extends unknown
133 | > = (
134 | data: ResolveLoadedDataShape
135 | ) => TReturn;
136 |
137 | export type CreateUseLoaderArgs<
138 | TQueries extends _TQueries,
139 | TDeferred extends _TDeferred,
140 | TPayload extends _TPayload,
141 | TReturn extends _TReturn,
142 | TArg = never
143 | > = {
144 | /** Should return a list of RTK useQuery results.
145 | * Example:
146 | * ```typescript
147 | * (args: Args) => ({
148 | * queries: {
149 | * pokemon: useGetPokemonQuery(args.pokemonId),
150 | * }
151 | * })
152 | * ```
153 | */
154 | useQueries: (
155 | ...args: OptionalGenericArg
156 | ) => DataShapeInput;
157 | /** Transforms the output of the queries */
158 | transform?: LoaderTransformFunction<
159 | TQueries,
160 | TDeferred,
161 | TPayload,
162 | TReturn
163 | >;
164 | config?: LoaderConfig;
165 | };
166 |
167 | export type UseLoader<
168 | TArg,
169 | TReturn,
170 | TQueries extends _TQueries,
171 | TDeferred extends _TDeferred,
172 | TPayload
173 | > = {
174 | (...args: OptionalGenericArg): UseQueryResult;
175 | original_args: CreateUseLoaderArgs<
176 | TQueries,
177 | TDeferred,
178 | TPayload,
179 | TReturn,
180 | TArg
181 | >;
182 | };
183 | export type ComponentWithLoaderData<
184 | TProps extends Record,
185 | TReturn extends unknown
186 | > = (props: TProps, loaderData: TReturn) => ReactElement;
187 |
188 | /** Use: `InferLoaderData`. Returns the return-value of the given loader's aggregated query. */
189 | export type InferLoaderData = T extends
190 | | Loader
191 | | Loader
192 | | Loader
193 | | Loader
194 | | Loader
195 | | Loader
196 | | Loader
197 | | Loader
198 | | Loader
199 | | Loader
200 | | Loader
201 | | Loader
202 | | Loader
203 | | Loader
204 | | Loader
205 | | Loader
206 | | Loader
207 | | Loader
208 | | Loader
209 | ? R
210 | : never;
211 |
212 | export type Component> = (
213 | props: TProps
214 | ) => ReactElement;
215 |
216 | export type WhileFetchingArgs<
217 | TProps extends unknown,
218 | TReturn extends unknown
219 | > = {
220 | /** Will be prepended before the component while the query is fetching */
221 | prepend?: (props: TProps, data?: TReturn) => ReactElement;
222 | /** Will be appended after the component while the query is fetching */
223 | append?: (props: TProps, data?: TReturn) => ReactElement;
224 | };
225 |
226 | export type CustomLoaderProps = {
227 | /** What the loader requests be rendered while fetching data */
228 | onFetching?: React.ReactElement;
229 | /** What the loader requests be rendered while fetching data */
230 | whileFetching?: {
231 | /** Should be appended to the success result while fetching */
232 | append?: React.ReactElement;
233 | /** Should be prepended to the success result while fetching */
234 | prepend?: React.ReactElement;
235 | };
236 | /** What the loader requests be rendered when data is available */
237 | onSuccess: (data: T) => React.ReactElement;
238 | /** What the loader requests be rendered when the query fails */
239 | onError?: (
240 | error: SerializedError | FetchBaseQueryError
241 | ) => JSX.Element;
242 | /** What the loader requests be rendered while loading data */
243 | onLoading?: React.ReactElement;
244 | config?: LoaderConfig;
245 | /** The joined query for the loader */
246 | query: UseQueryResult;
247 | };
248 |
249 | export type DeferredConfig = {
250 | shouldThrowError?: boolean;
251 | };
252 |
253 | export type LoaderConfig = {
254 | deferred?: DeferredConfig;
255 | loaderComponent?: Component;
256 | };
257 |
258 | export type CreateLoaderArgs<
259 | TProps extends unknown,
260 | TQueries extends _TQueries,
261 | TDeferred extends _TDeferred,
262 | TPayload extends _TPayload,
263 | TReturn extends unknown,
264 | TArg = never
265 | > = Partial<
266 | CreateUseLoaderArgs<
267 | TQueries,
268 | TDeferred,
269 | TPayload,
270 | TReturn,
271 | TArg
272 | >
273 | > & {
274 | /** Generates an argument for the `queries` based on component props */
275 | queriesArg?: (props: TProps) => TArg;
276 | /** Determines what to render while loading (with no data to fallback on) */
277 | onLoading?: (
278 | props: TProps,
279 | joinedQuery: UseQueryResult
280 | ) => ReactElement;
281 | /** Determines what to render when query fails. */
282 | onError?: (
283 | props: TProps,
284 | error: FetchBaseQueryError | SerializedError,
285 | joinedQuery: UseQueryResult
286 | ) => ReactElement;
287 | /** @deprecated Using onFetching might result in loss of internal state. Use `whileFetching` instead, or pass the query to the component */
288 | onFetching?: (
289 | props: TProps,
290 | renderBody: () => ReactElement
291 | ) => ReactElement;
292 | /** Determines what to render besides success-result while query is fetching. */
293 | whileFetching?: WhileFetchingArgs;
294 | config?: LoaderConfig;
295 | /** @deprecated Use `config.loaderComponent` */
296 | loaderComponent?: Component;
297 | };
298 |
299 | export type CreateLoader<
300 | TProps extends unknown,
301 | TQueries extends _TQueries = never,
302 | TDeferred extends _TDeferred = never,
303 | TPayload extends _TPayload = never,
304 | TReturn extends unknown = MakeDataRequired,
305 | TArg = never
306 | > = (
307 | args: CreateLoaderArgs<
308 | TProps,
309 | TQueries,
310 | TDeferred,
311 | TPayload,
312 | TReturn,
313 | TArg
314 | >
315 | ) => Loader<
316 | TProps,
317 | TReturn,
318 | TQueries,
319 | TDeferred,
320 | TPayload,
321 | TArg
322 | >;
323 |
324 | export type Loader<
325 | TProps extends unknown,
326 | TReturn extends unknown,
327 | TQueries extends _TQueries = never,
328 | TDeferred extends _TDeferred = never,
329 | TPayload extends _TPayload = never,
330 | TArg = never
331 | > = {
332 | /** A hook that runs all queries and returns aggregated result */
333 | useLoader: UseLoader<
334 | TArg,
335 | TReturn,
336 | TQueries,
337 | TDeferred,
338 | TPayload
339 | >;
340 | /** Generates an argument for the `queries` based on component props */
341 | queriesArg?: (props: TProps) => TArg;
342 | /** Determines what to render while loading (with no data to fallback on) */
343 | onLoading?: (
344 | props: TProps,
345 | joinedQuery: UseQueryResult
346 | ) => ReactElement;
347 | /** Determines what to render when query fails. */
348 | onError?: (
349 | props: TProps,
350 | error: SerializedError | FetchBaseQueryError,
351 | joinedQuery: UseQueryResult
352 | ) => ReactElement;
353 | /** @deprecated Using onFetching might result in loss of internal state. Use `whileFetching` instead, or pass the query to the component */
354 | onFetching?: (
355 | props: TProps,
356 | renderBody: () => ReactElement
357 | ) => ReactElement;
358 | /** Determines what to render besides success-result while query is fetching. */
359 | whileFetching?: WhileFetchingArgs;
360 | config?: LoaderConfig;
361 | /**
362 | * Creates a `loader` that can be used to fetch data and render error & loading states.
363 | * @example
364 | * const loader = baseLoader.extend({
365 | * queriesArg: (props) => props.userId,
366 | * useQueries: (userId) => {
367 | * const user = useGetUserQuery(userId);
368 | * return { queries: { user } };
369 | * },
370 | * });
371 | */
372 | extend: <
373 | E_TQueries extends _TQueries = TQueries,
374 | E_TDeferred extends _TDeferred = TDeferred,
375 | E_TPayload extends _TPayload = TPayload,
376 | E_TReturn extends unknown = AllEql<
377 | TQueries,
378 | E_TQueries,
379 | TDeferred,
380 | E_TDeferred,
381 | TPayload,
382 | E_TPayload
383 | > extends true
384 | ? TReturn
385 | : ResolveLoadedDataShape<
386 | E_TQueries,
387 | E_TDeferred,
388 | E_TPayload
389 | >,
390 | E_TProps extends unknown = TProps,
391 | E_TArg = TArg
392 | >(
393 | newLoader: Partial<
394 | CreateLoaderArgs<
395 | E_TProps,
396 | E_TQueries,
397 | E_TDeferred,
398 | E_TPayload,
399 | E_TReturn,
400 | E_TArg
401 | >
402 | >
403 | ) => Loader<
404 | E_TProps,
405 | E_TReturn,
406 | E_TQueries,
407 | E_TDeferred,
408 | E_TPayload,
409 | E_TArg
410 | >;
411 | /** The component to use to switch between rendering the different query states. */
412 | LoaderComponent: Component;
413 | };
414 |
415 | export type CreateQueryGetter =
416 | () => Promise;
417 |
418 | export type CreateQueryReducerAction =
419 | | {
420 | type: "load";
421 | payload: {
422 | refetch: () => void;
423 | };
424 | }
425 | | {
426 | type: "fetch";
427 | payload: {
428 | refetch: () => void;
429 | };
430 | }
431 | | {
432 | type: "error";
433 | payload: {
434 | error: unknown;
435 | };
436 | }
437 | | {
438 | type: "success";
439 | payload: {
440 | data: T;
441 | };
442 | };
443 |
444 | export type ConsumerProps> =
445 | Record & T;
446 |
447 | /************************************************/
448 | /* Legacy/unused, for backwards compatibility */
449 | /************************************************/
450 | export type WithLoaderArgs<
451 | TProps extends unknown,
452 | TReturn extends unknown,
453 | TArg = never
454 | > = Loader;
455 |
--------------------------------------------------------------------------------
/src/useCreateLoader.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as Types from "./types";
3 | import { createLoader } from "./createLoader";
4 |
5 | /**
6 | * Allows you to create a loader inside of a component.
7 | * This is useful if you want to create a loader for use with `AwaitLoader`, scoped to a component.
8 | */
9 | export const useCreateLoader = <
10 | TProps extends unknown,
11 | TQueries extends Types._TQueries,
12 | TDeferred extends Types._TDeferred,
13 | TPayload extends Types._TPayload,
14 | TReturn extends unknown = Types.ResolveDataShape<
15 | Types.MakeDataRequired,
16 | TDeferred,
17 | TPayload
18 | >,
19 | TArg extends unknown = never
20 | >(
21 | createLoaderArgs: Types.CreateLoaderArgs<
22 | TProps,
23 | TQueries,
24 | TDeferred,
25 | TPayload,
26 | TReturn,
27 | TArg
28 | >
29 | ) => {
30 | return React.useRef(createLoader(createLoaderArgs)).current;
31 | };
32 |
--------------------------------------------------------------------------------
/src/withLoader.tsx:
--------------------------------------------------------------------------------
1 | import { SerializedError } from "@reduxjs/toolkit";
2 | import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query";
3 | import * as React from "react";
4 | import * as Types from "./types";
5 |
6 | /**
7 | * A higher order component that wraps a component and provides it with a loader.
8 | * @param Component The component to wrap with a loader. Second argument is the resolved loader data.
9 | * @param loader The loader to use.
10 | * @returns A component that will load the data and pass it to the wrapped component.
11 | * @example
12 | * const Component = withLoader((props, loaderData) => {
13 | * return {loaderData.queries.user.name}
;
14 | * }, loader);
15 | */
16 | export const withLoader = <
17 | TProps extends Record,
18 | TReturn extends unknown,
19 | TQueries extends Types._TQueries,
20 | TDeferred extends Types._TDeferred,
21 | TPayload extends Types._TPayload,
22 | TArg = never
23 | >(
24 | Component: Types.ComponentWithLoaderData<
25 | TProps,
26 | Types.Unwrap
27 | >,
28 | loader: Types.Loader<
29 | TProps,
30 | TReturn,
31 | TQueries,
32 | TDeferred,
33 | TPayload,
34 | TArg
35 | >
36 | ): Types.Component => {
37 | let CachedComponent: Types.ComponentWithLoaderData<
38 | TProps,
39 | TReturn
40 | >;
41 | const LoadedComponent = (props: TProps) => {
42 | const useLoaderArgs = [];
43 | if (loader.queriesArg) {
44 | useLoaderArgs.push(loader.queriesArg(props));
45 | }
46 | const query = loader.useLoader(
47 | ...(useLoaderArgs as Types.OptionalGenericArg)
48 | );
49 | if (!CachedComponent) {
50 | CachedComponent = React.forwardRef(
51 | Component as unknown as React.ForwardRefRenderFunction<
52 | TReturn,
53 | TProps
54 | >
55 | ) as unknown as Types.ComponentWithLoaderData<
56 | TProps,
57 | TReturn
58 | >;
59 | }
60 |
61 | const onLoading = loader.onLoading?.(props, query);
62 |
63 | const onError = loader.onError
64 | ? (error: SerializedError | FetchBaseQueryError) => {
65 | if (!loader.onError) return ;
66 | return loader.onError(props, error, query);
67 | }
68 | : undefined;
69 |
70 | const onSuccess = (data: TReturn) => (
71 |
72 | );
73 |
74 | const whileFetching = loader.whileFetching
75 | ? {
76 | prepend: loader.whileFetching.prepend?.(
77 | props,
78 | query?.data
79 | ),
80 | append: loader.whileFetching.append?.(
81 | props,
82 | query?.data
83 | ),
84 | }
85 | : undefined;
86 |
87 | const onFetching = loader?.onFetching?.(
88 | props,
89 | query.data
90 | ? () => onSuccess(query.data as TReturn)
91 | : () =>
92 | );
93 |
94 | const { LoaderComponent } = loader;
95 |
96 | return (
97 | React.ReactElement
102 | }
103 | onError={onError}
104 | onLoading={onLoading}
105 | query={query}
106 | />
107 | );
108 | };
109 | return LoadedComponent;
110 | };
111 |
--------------------------------------------------------------------------------
/testing-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/testing-app/README.md:
--------------------------------------------------------------------------------
1 | ## Setup tests
2 |
3 | > Ensure you are using node 18+
4 |
5 | 1. Install dependencies in parent repository (`yarn install`)
6 | 2. Run `yarn setup-link`
7 | 3. Navigate to testing app `cd testing-app`
8 | 4. Install dependencies `yarn install`
9 | 5. Run tests `yarn test`
10 |
--------------------------------------------------------------------------------
/testing-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rtk-query-loader-testing-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "main": "src/index.tsx",
6 | "dependencies": {
7 | "@testing-library/jest-dom": "^5.16.5",
8 | "@testing-library/react": "^13.4.0",
9 | "@testing-library/user-event": "^14.4.3",
10 | "@types/jest": "^27.5.2",
11 | "@types/node": "^20.12.12",
12 | "@types/react": "^18.2.37",
13 | "@types/react-dom": "^18.2.15",
14 | "jest-environment-jsdom": "^29.7.0",
15 | "jest-fixed-jsdom": "^0.0.2",
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0",
18 | "react-scripts": "5.0.1",
19 | "typescript": "^4.8.4",
20 | "undici": "^6.18.1",
21 | "web-vitals": "^2.1.4"
22 | },
23 | "scripts": {
24 | "test": "react-scripts test"
25 | },
26 | "eslintConfig": {
27 | "extends": [
28 | "react-app",
29 | "react-app/jest"
30 | ]
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | },
44 | "devDependencies": {
45 | "@reduxjs/toolkit": "^2.2.5",
46 | "@testing-library/dom": "^8.19.0",
47 | "msw": "^1.0.0",
48 | "react-redux": "^8.0.4",
49 | "redux": "^4.2.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/testing-app/src/components/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export class ErrorBoundary extends React.Component<
4 | {
5 | children?: React.ReactNode;
6 | fallback?: React.ReactNode;
7 | },
8 | {
9 | hasError: boolean;
10 | }
11 | > {
12 | public state = {
13 | hasError: false,
14 | };
15 |
16 | public static getDerivedStateFromError(_: Error) {
17 | return { hasError: true };
18 | }
19 |
20 | public componentDidCatch(
21 | error: Error,
22 | errorInfo: React.ErrorInfo
23 | ) {
24 | console.error("Uncaught error:", error, errorInfo);
25 | }
26 |
27 | public render() {
28 | if (this.state.hasError) {
29 | return (
30 | {this.props.fallback ?? "_error_boundary_"}
31 | );
32 | }
33 |
34 | return this.props.children;
35 | }
36 | }
37 |
38 | export default ErrorBoundary;
39 |
--------------------------------------------------------------------------------
/testing-app/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 |
4 | const root = ReactDOM.createRoot(
5 | document.getElementById("root") as HTMLElement
6 | );
7 | root.render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/testing-app/src/mocks.ts:
--------------------------------------------------------------------------------
1 | import { rest } from "msw";
2 |
3 | const RESPONSE_DELAY = 100;
4 |
5 | export const handlers = [
6 | rest.get("/pokemons", (req, res, c) => {
7 | return res(
8 | c.delay(RESPONSE_DELAY),
9 | c.status(200),
10 | c.json({
11 | results: [
12 | {
13 | name: "charizard",
14 | },
15 | {
16 | name: "pikachu",
17 | },
18 | ],
19 | })
20 | );
21 | }),
22 | rest.get("/pokemon/:name", (req, res, c) => {
23 | if (req.params.name === "error") {
24 | return res(c.delay(RESPONSE_DELAY), c.status(500));
25 | }
26 | if (req.params.name === "unprocessable") {
27 | return res(
28 | c.delay(RESPONSE_DELAY),
29 | c.status(422),
30 | c.json({ some_json_data: "woop" })
31 | );
32 | }
33 | const delay =
34 | req.params.name === "delay"
35 | ? RESPONSE_DELAY + 100
36 | : RESPONSE_DELAY;
37 | return res(
38 | c.delay(delay),
39 | c.status(200),
40 | c.json({
41 | name: req.params.name,
42 | id: req.params.name.length,
43 | })
44 | );
45 | }),
46 | ];
47 |
--------------------------------------------------------------------------------
/testing-app/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/testing-app/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom";
6 | import { setupServer } from "msw/node";
7 | import { handlers } from "./mocks";
8 | import { someApi, store } from "./store";
9 |
10 | const server = setupServer(...handlers);
11 | beforeAll(() => server.listen());
12 | beforeEach(() => store.dispatch(someApi.util.resetApiState()));
13 | afterEach(() => server.resetHandlers());
14 | afterAll(() => {
15 | server.close();
16 | });
17 |
--------------------------------------------------------------------------------
/testing-app/src/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import {
3 | createApi,
4 | fetchBaseQuery,
5 | } from "@reduxjs/toolkit/query/react";
6 |
7 | export type Pokemon = {
8 | id: number;
9 | name: string;
10 | };
11 |
12 | export type Pokemons = {
13 | results: {
14 | name: string;
15 | }[];
16 | };
17 |
18 | export const someApi = createApi({
19 | reducerPath: "pokemonApi",
20 | baseQuery: fetchBaseQuery({
21 | baseUrl: "/",
22 | }),
23 | endpoints: (builder) => ({
24 | getPokemonByName: builder.query({
25 | query: (name) => `pokemon/${name || "charizard"}`,
26 | }),
27 | getPokemons: builder.query({
28 | query: () => `pokemons`,
29 | }),
30 | }),
31 | });
32 |
33 | export const { useGetPokemonByNameQuery, useGetPokemonsQuery } =
34 | someApi;
35 |
36 | export const store = configureStore({
37 | reducer: {
38 | [someApi.reducerPath]: someApi.reducer,
39 | },
40 | middleware: (getdefaultMiddleware) =>
41 | getdefaultMiddleware().concat([someApi.middleware]),
42 | });
43 |
--------------------------------------------------------------------------------
/testing-app/src/testComponents.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/rules-of-hooks */
2 | import { useRef, useState } from "react";
3 | import { aggregateToQuery } from "../../src/aggregateToQuery";
4 | import { createLoader } from "../../src/createLoader";
5 | import { InferLoaderData } from "../../src/types";
6 | import { withLoader } from "../../src/withLoader";
7 | import {
8 | Pokemon,
9 | Pokemons,
10 | useGetPokemonByNameQuery,
11 | useGetPokemonsQuery,
12 | } from "./store";
13 |
14 | const RenderPokemonData = (props: {
15 | pokemon: Pokemon;
16 | pokemons: Pokemons;
17 | }) => (
18 |
19 |
20 | {props.pokemon.name}: {props.pokemon.id}
21 |
22 |
23 | {props.pokemons.results.map((item) => (
24 | {item.name}
25 | ))}
26 |
27 |
28 | );
29 |
30 | const simpleLoader = createLoader({
31 | useQueries: () => ({
32 | queries: {
33 | charizard: useGetPokemonByNameQuery("charizard"),
34 | pokemons: useGetPokemonsQuery(undefined),
35 | },
36 | }),
37 | onLoading: () => Loading
,
38 | });
39 |
40 | export const SimpleLoadedComponent = withLoader(
41 | (_, loader) => (
42 |
46 | ),
47 | simpleLoader
48 | );
49 |
50 | const extendedLoader = simpleLoader.extend({
51 | onLoading: () => Extended loading
,
52 | });
53 |
54 | export const ExtendedLoaderComponent = withLoader(
55 | (_, loaderData) => (
56 |
60 | ),
61 | extendedLoader
62 | );
63 |
64 | const pokemonByNameLoader = createLoader({
65 | queriesArg: (props: { name: string }) => props.name,
66 | useQueries: (name) => {
67 | const pokemon = useGetPokemonByNameQuery(name);
68 | return {
69 | queries: {
70 | pokemon,
71 | },
72 | };
73 | },
74 | });
75 |
76 | export const LoadPokemon = withLoader(
77 | (props, { queries: { pokemon } }) => {
78 | return (
79 |
80 | Loaded: "{pokemon.data.name}", props: "{props.name}"
81 |
82 | );
83 | },
84 | pokemonByNameLoader
85 | );
86 |
87 | export const FailTester = withLoader(
88 | () => Success
,
89 | createLoader({
90 | useQueries: () => ({
91 | queries: {
92 | error: useGetPokemonByNameQuery("error"),
93 | },
94 | }),
95 | onError: () => Error
,
96 | onLoading: () => Loading
,
97 | })
98 | );
99 |
100 | const fetchTestBaseLoader = createLoader({
101 | useQueries: (name: string) => ({
102 | queries: {
103 | pokemon: useGetPokemonByNameQuery(name),
104 | },
105 | }),
106 | queriesArg: (props: {
107 | name: string;
108 | onChange: (name: string) => void;
109 | }) => props.name,
110 | onLoading: () => Loading
,
111 | onFetching: () => Fetching
,
112 | });
113 |
114 | type FetchTestLoader = InferLoaderData<
115 | // ^?
116 | typeof fetchTestBaseLoader
117 | >;
118 |
119 | const FetchTesterComponent = (
120 | props: {
121 | name: string;
122 | onChange: (name: string) => void;
123 | },
124 | loaderData: FetchTestLoader
125 | ) => {
126 | const inputRef = useRef(null);
127 | return (
128 |
129 | #{loaderData.queries.pokemon.data.id}
130 |
131 |
140 |
141 | );
142 | };
143 |
144 | export const FetchTestRenderer = (props: {
145 | while?: boolean;
146 | }) => {
147 | const [name, setName] = useState("charizard");
148 |
149 | if (props.while) {
150 | return ;
151 | }
152 | return ;
153 | };
154 |
155 | export const FetchTester = withLoader(
156 | FetchTesterComponent,
157 | fetchTestBaseLoader
158 | );
159 |
160 | export const WhileFetchTester = withLoader(
161 | FetchTesterComponent,
162 | fetchTestBaseLoader.extend({
163 | whileFetching: { prepend: () => FetchingWhile
},
164 | onFetching: undefined,
165 | })
166 | );
167 |
168 | export const TestAggregateComponent = () => {
169 | const q1 = useGetPokemonByNameQuery("charizard");
170 | const q2 = useGetPokemonsQuery(undefined);
171 | const query = aggregateToQuery([q1, q2] as const);
172 |
173 | if (query.isSuccess) {
174 | return (
175 |
176 |
177 | {q1.data?.name}: {q1?.data?.id}
178 |
179 |
180 | {q2.data?.results?.map((res) => (
181 | {res.name}
182 | ))}
183 |
184 |
185 | );
186 | }
187 |
188 | return Loading
;
189 | };
190 |
191 | const transformLoader = createLoader({
192 | useQueries: () => ({
193 | queries: {
194 | charizard: useGetPokemonByNameQuery("charizard"),
195 | },
196 | }),
197 | transform: (loader) => ({
198 | pokemon: loader.queries.charizard.data,
199 | }),
200 | });
201 |
202 | export const TestTransformed = withLoader((_, loaderData) => {
203 | return {loaderData.pokemon.name}
;
204 | }, transformLoader);
205 |
--------------------------------------------------------------------------------
/testing-app/src/tests.test.tsx:
--------------------------------------------------------------------------------
1 | import userEvent from "@testing-library/user-event";
2 | import * as React from "react";
3 | import { _testLoad } from "../../src/AwaitLoader";
4 | import { createLoader } from "../../src/createLoader";
5 | import { _testCreateUseCreateQuery } from "../../src/createQuery";
6 | import { CustomLoaderProps } from "../../src/types";
7 | import { withLoader } from "../../src/withLoader";
8 | import ErrorBoundary from "./components/ErrorBoundary";
9 | import {
10 | useGetPokemonByNameQuery,
11 | useGetPokemonsQuery,
12 | } from "./store";
13 | import {
14 | ExtendedLoaderComponent,
15 | FailTester,
16 | FetchTestRenderer,
17 | LoadPokemon,
18 | SimpleLoadedComponent,
19 | TestAggregateComponent,
20 | } from "./testComponents";
21 | import {
22 | render,
23 | screen,
24 | waitFor,
25 | waitForElementToBeRemoved,
26 | } from "./utils";
27 |
28 | const { useState } = React;
29 |
30 | // We do this to avoid two conflicting versions of React
31 | const useCreateQuery = _testCreateUseCreateQuery(React);
32 | const AwaitLoader = _testLoad(React);
33 |
34 | describe("aggregateToQuery", () => {
35 | test("It aggregates query status", async () => {
36 | render( );
37 | expect(screen.getByText("Loading")).toBeVisible();
38 | await waitFor(() =>
39 | expect(screen.getByText("charizard: 9")).toBeVisible()
40 | );
41 | });
42 | });
43 |
44 | describe("useCreateQuery", () => {
45 | test("It creates a query", async () => {
46 | const Component = withLoader(
47 | (props, queries) => (
48 | {queries.queries.q.data.name}
49 | ),
50 | createLoader({
51 | useQueries: () => ({
52 | queries: {
53 | q: useCreateQuery(async () => {
54 | await new Promise((resolve) =>
55 | setTimeout(resolve, 100)
56 | );
57 | return {
58 | name: "charizard",
59 | };
60 | }),
61 | },
62 | }),
63 | onLoading: () => Loading
,
64 | })
65 | );
66 | render( );
67 | expect(screen.getByText("Loading")).toBeVisible();
68 | await waitFor(() =>
69 | expect(screen.getByText("charizard")).toBeVisible()
70 | );
71 | });
72 | test("The query can throw error", async () => {
73 | const Component = withLoader(
74 | (props, loader) => {loader.queries.q.data.name}
,
75 | createLoader({
76 | useQueries: () => ({
77 | queries: {
78 | q: useCreateQuery(async () => {
79 | await new Promise((resolve, reject) =>
80 | setTimeout(
81 | () => reject(new Error("error-message")),
82 | 100
83 | )
84 | );
85 | return {
86 | name: "charizard",
87 | };
88 | }),
89 | },
90 | }),
91 | onLoading: () => Loading
,
92 | onError: (props, error) => (
93 | {(error as any)?.message}
94 | ),
95 | })
96 | );
97 | render( );
98 | expect(screen.getByText("Loading")).toBeVisible();
99 | await waitFor(() =>
100 | expect(screen.getByText("error-message")).toBeVisible()
101 | );
102 | });
103 | test("You can refetch the query", async () => {
104 | let count = 0;
105 | const Component = withLoader(
106 | (props, queries) => {
107 | return (
108 |
109 | {queries.queries.q.isFetching ? "fetch" : "success"}{" "}
110 |
111 | refetch
112 |
113 |
{queries.queries.q.data.count}
114 |
115 | );
116 | },
117 | createLoader({
118 | useQueries: () => ({
119 | queries: {
120 | q: useCreateQuery(async () => {
121 | count++;
122 | await new Promise((resolve) =>
123 | setTimeout(resolve, 300)
124 | );
125 | return {
126 | name: "charizard",
127 | count,
128 | };
129 | }),
130 | },
131 | }),
132 | onLoading: () => Loading
,
133 | })
134 | );
135 | render( );
136 | expect(screen.getByText("Loading")).toBeVisible();
137 | await waitFor(() =>
138 | expect(screen.getByText("success")).toBeVisible()
139 | );
140 | expect(screen.getByText("1")).toBeVisible();
141 | await userEvent.click(screen.getByText("refetch"));
142 |
143 | await waitFor(() =>
144 | expect(screen.getByText("fetch")).toBeVisible()
145 | );
146 |
147 | await waitFor(() =>
148 | expect(screen.getByText("success")).toBeVisible()
149 | );
150 | expect(screen.getByText("2")).toBeVisible();
151 | });
152 | });
153 |
154 | describe(" ", () => {
155 | test("Renders loading state until data is available", async () => {
156 | const loader = createLoader({
157 | useQueries: () => ({
158 | queries: {
159 | charizard: useGetPokemonByNameQuery("charizard"),
160 | },
161 | }),
162 | onLoading: () => Loading
,
163 | });
164 | const Component = () => {
165 | return (
166 |
167 |
(
170 | {data.queries.charizard.data.name}
171 | )}
172 | />
173 |
174 | );
175 | };
176 | render( );
177 |
178 | expect(screen.getByText("Loading")).toBeVisible();
179 | await waitFor(() =>
180 | expect(screen.getByText("charizard")).toBeVisible()
181 | );
182 | });
183 |
184 | test("Will pass arguments properly", async () => {
185 | const loader = createLoader({
186 | queriesArg: (props: { name: string }) => props.name,
187 | useQueries: (name) => ({
188 | queries: {
189 | charizard: useGetPokemonByNameQuery(name),
190 | },
191 | }),
192 | onLoading: () => Loading
,
193 | });
194 | const Component = () => {
195 | return (
196 |
197 |
(
200 | {data.queries.charizard.data.name}
201 | )}
202 | args={{
203 | name: "charizard",
204 | }}
205 | />
206 |
207 | );
208 | };
209 | render( );
210 |
211 | expect(screen.getByText("Loading")).toBeVisible();
212 | await waitFor(() =>
213 | expect(screen.getByText("charizard")).toBeVisible()
214 | );
215 | });
216 | });
217 |
218 | describe("withLoader", () => {
219 | test("Renders loading state until data is available", async () => {
220 | render( );
221 | expect(screen.getByText("Loading")).toBeVisible();
222 | await waitFor(() =>
223 | expect(screen.getByText("charizard: 9")).toBeVisible()
224 | );
225 | expect(screen.getByText("pikachu")).toBeVisible();
226 | });
227 |
228 | test("onError renders when applicable", async () => {
229 | render( );
230 | expect(screen.getByText("Loading")).toBeVisible();
231 | await waitFor(() =>
232 | expect(screen.getByText("Error")).toBeVisible()
233 | );
234 | });
235 |
236 | test("onFetching renders when applicable", async () => {
237 | render( );
238 | await waitForElementToBeRemoved(() =>
239 | screen.queryByText("Loading")
240 | );
241 | await waitFor(() =>
242 | expect(screen.getByRole("textbox")).toBeVisible()
243 | );
244 | const input = screen.getByRole("textbox");
245 | userEvent.type(input, "Abc{Enter}");
246 | await waitFor(() =>
247 | expect(screen.getByText("Fetching")).toBeVisible()
248 | );
249 | await waitForElementToBeRemoved(() =>
250 | screen.queryByText("Fetching")
251 | );
252 | await waitFor(() =>
253 | expect(screen.getByText("#3")).toBeVisible()
254 | );
255 | });
256 |
257 | test("Internal state won't reset when using whileFetching", async () => {
258 | render( );
259 | await waitForElementToBeRemoved(() =>
260 | screen.queryByText("Loading")
261 | );
262 | await waitFor(() =>
263 | expect(screen.getByRole("textbox")).toBeVisible()
264 | );
265 | const input = screen.getByRole("textbox");
266 | userEvent.type(input, "Abc{Enter}");
267 | await waitFor(() =>
268 | expect(screen.getByText("FetchingWhile")).toBeVisible()
269 | );
270 | await waitForElementToBeRemoved(() =>
271 | screen.queryByText("FetchingWhile")
272 | );
273 | await waitFor(() =>
274 | expect(screen.getByText("#3")).toBeVisible()
275 | );
276 | expect(screen.getByRole("textbox")).toHaveValue("Abc");
277 | });
278 |
279 | // Not wanted behavior, but expected behavior:
280 | test("Internal state will reset when using onFetching", async () => {
281 | render( );
282 | await waitForElementToBeRemoved(() =>
283 | screen.queryByText("Loading")
284 | );
285 | await waitFor(() =>
286 | expect(screen.getByRole("textbox")).toBeVisible()
287 | );
288 | const input = screen.getByRole("textbox");
289 | userEvent.type(input, "Abc{Enter}");
290 | await waitFor(() =>
291 | expect(screen.getByText("Fetching")).toBeVisible()
292 | );
293 | await waitForElementToBeRemoved(() =>
294 | screen.queryByText("Fetching")
295 | );
296 | await waitFor(() =>
297 | expect(screen.getByText("#3")).toBeVisible()
298 | );
299 | expect(screen.getByRole("textbox")).toHaveValue("");
300 | });
301 |
302 | test("Can use custom loader component", async () => {
303 | const CustomLoader = (props: CustomLoaderProps) => {
304 | if (props.query.isSuccess && props.query.data) {
305 | return props.onSuccess(props.query.data);
306 | }
307 | return Custom loader!
;
308 | };
309 |
310 | const loader = createLoader({
311 | config: { loaderComponent: CustomLoader },
312 | useQueries: () => ({
313 | queries: {
314 | charizard: useGetPokemonByNameQuery("charizard"),
315 | },
316 | }),
317 | });
318 |
319 | const Component = withLoader(
320 | (_, loaderData) => (
321 | {loaderData.queries.charizard.data.name}
322 | ),
323 | loader
324 | );
325 | render( );
326 | expect(screen.getByText("Custom loader!")).toBeVisible();
327 | await waitFor(() =>
328 | expect(screen.getByText("charizard")).toBeVisible()
329 | );
330 | });
331 |
332 | test("loaderComponent is backwards compatible", async () => {
333 | const CustomLoader = (props: CustomLoaderProps) => {
334 | if (props.query.isSuccess && props.query.data) {
335 | return props.onSuccess(props.query.data);
336 | }
337 | return Custom loader!
;
338 | };
339 | const Component = withLoader(
340 | (_, loaderData) => (
341 | {loaderData.queries.charizard.data.name}
342 | ),
343 | createLoader({
344 | loaderComponent: CustomLoader,
345 | useQueries: () => ({
346 | queries: {
347 | charizard: useGetPokemonByNameQuery("charizard"),
348 | },
349 | }),
350 | })
351 | );
352 | render( );
353 | expect(screen.getByText("Custom loader!")).toBeVisible();
354 | await waitFor(() =>
355 | expect(screen.getByText("charizard")).toBeVisible()
356 | );
357 | });
358 |
359 | test("Can defer some queries", async () => {
360 | const Component = withLoader(
361 | (props, { charizard, delay }) => {
362 | return (
363 | <>
364 | {charizard.name}
365 |
366 | {delay ? "loaded-deferred" : "loading-deferred"}
367 |
368 | >
369 | );
370 | },
371 | createLoader({
372 | useQueries: () => ({
373 | queries: {
374 | charizard: useGetPokemonByNameQuery("charizard"),
375 | },
376 | deferredQueries: {
377 | delay: useGetPokemonByNameQuery("delay"),
378 | },
379 | }),
380 | transform: (loader) => ({
381 | charizard: loader.queries.charizard.data,
382 | delay: loader.deferredQueries.delay.data,
383 | }),
384 | onLoading: () => Loading
,
385 | onError: () => Error
,
386 | })
387 | );
388 | render( );
389 | expect(screen.getByText("Loading")).toBeVisible();
390 | await waitFor(() =>
391 | expect(screen.getByText("charizard")).toBeVisible()
392 | );
393 | expect(screen.getByText("loading-deferred")).toBeVisible();
394 | await waitFor(() =>
395 | expect(screen.getByText("loaded-deferred")).toBeVisible()
396 | );
397 | });
398 |
399 | test("Can defer all queries", async () => {
400 | const Component = withLoader(
401 | (props, data) => {
402 | if (data.isLoading) {
403 | return <>Loading>;
404 | }
405 | return <>{data.data?.name}>;
406 | },
407 | createLoader({
408 | useQueries: () => ({
409 | deferredQueries: {
410 | charizard: useGetPokemonByNameQuery("charizard"),
411 | },
412 | }),
413 | transform: (loaderData) =>
414 | loaderData.deferredQueries.charizard,
415 | })
416 | );
417 | render( );
418 | expect(screen.getByText("Loading")).toBeVisible();
419 | await waitFor(() =>
420 | expect(screen.getByText("charizard")).toBeVisible()
421 | );
422 | });
423 |
424 | test("Loaders with no queries render immediately", () => {
425 | const Component = withLoader(
426 | () => Success
,
427 | createLoader({})
428 | );
429 | render( );
430 | expect(screen.getByText("Success")).toBeVisible();
431 | });
432 |
433 | test("Can remount a component that has a failed query", async () => {
434 | const Component = withLoader(
435 | (props, loaderData) => {
436 | return (
437 |
438 | Success{" "}
439 | {loaderData.queries.error.data.name.includes(
440 | "charizard"
441 | )}
442 |
443 | );
444 | },
445 | createLoader({
446 | queriesArg: (props: { error: boolean }) => props.error,
447 | useQueries: (shouldError) => {
448 | const error = useGetPokemonByNameQuery(
449 | shouldError ? "unprocessable" : "charizard"
450 | );
451 | return {
452 | queries: {
453 | error,
454 | },
455 | };
456 | },
457 | onError: () => onError
,
458 | })
459 | );
460 | const Wrapper = () => {
461 | const [shouldError, setShouldError] = useState(true);
462 | return (
463 |
464 |
setShouldError(!shouldError)}>
465 | Toggle
466 |
467 |
468 | {shouldError ? (
469 |
470 | ) : (
471 | Success
472 | )}
473 |
474 |
475 | );
476 | };
477 | render( );
478 | await waitFor(() =>
479 | expect(screen.getByText("onError")).toBeVisible()
480 | );
481 | await userEvent.click(screen.getByRole("button"));
482 | await waitFor(() =>
483 | expect(screen.getByText("Success")).toBeVisible()
484 | );
485 | await userEvent.click(screen.getByRole("button"));
486 | await waitFor(() =>
487 | expect(screen.getByText("onError")).toBeVisible()
488 | );
489 | });
490 | });
491 |
492 | describe("createLoader", () => {
493 | test("Normally, deferred queries do not throw", async () => {
494 | const Component = withLoader(
495 | (props, data) => {
496 | return Hello
;
497 | },
498 | createLoader({
499 | useQueries: () => ({
500 | deferredQueries: {
501 | charizard: useGetPokemonByNameQuery("error"),
502 | },
503 | }),
504 | })
505 | );
506 | render( );
507 | await waitFor(
508 | () => new Promise((res) => setTimeout(res, 200))
509 | );
510 | expect(screen.getByText("Hello")).toBeVisible();
511 | });
512 |
513 | test("Deferred queries throw error when configured to", async () => {
514 | const Component = withLoader(
515 | (props, data) => {
516 | return Hello
;
517 | },
518 | createLoader({
519 | useQueries: () => ({
520 | deferredQueries: {
521 | charizard: useGetPokemonByNameQuery("error"),
522 | },
523 | }),
524 | config: {
525 | deferred: {
526 | shouldThrowError: true,
527 | },
528 | },
529 | onError: () => Error
,
530 | })
531 | );
532 | render( );
533 | await waitFor(
534 | () => new Promise((res) => setTimeout(res, 200))
535 | );
536 | expect(screen.getByText("Error")).toBeVisible();
537 | });
538 |
539 | test("Can send static payload to loader", async () => {
540 | const Component = withLoader(
541 | (_, loader) => {
542 | return {loader.payload.foo}
;
543 | },
544 | createLoader({
545 | useQueries: () => ({
546 | payload: {
547 | foo: "bar" as const,
548 | },
549 | }),
550 | })
551 | );
552 | render( );
553 | expect(screen.getByText("bar")).toBeVisible();
554 | });
555 |
556 | test("Loader passes props through queriesArg to queries", async () => {
557 | render( );
558 | await waitFor(() =>
559 | expect(
560 | screen.getByText(
561 | 'Loaded: "charizard", props: "charizard"'
562 | )
563 | ).toBeVisible()
564 | );
565 | });
566 |
567 | describe(".extend()", () => {
568 | test("Can extend onLoading", async () => {
569 | render( );
570 | expect(screen.getByText("Extended loading")).toBeVisible();
571 | });
572 |
573 | test("Can extend onError", async () => {
574 | const Component = withLoader(
575 | (props, loaderData) => {
576 | return Success
;
577 | },
578 |
579 | createLoader({
580 | useQueries: () => {
581 | const error = useGetPokemonByNameQuery("error");
582 | return {
583 | queries: {
584 | error,
585 | },
586 | };
587 | },
588 | onLoading: () => Loading
,
589 | onError: () => Error
,
590 | }).extend({
591 | onError: () => Extended Error
,
592 | })
593 | );
594 | render( );
595 | expect(screen.getByText("Loading")).toBeVisible();
596 | await waitFor(() =>
597 | expect(screen.getByText("Extended Error")).toBeVisible()
598 | );
599 | });
600 |
601 | test("Can extend onFetching", async () => {
602 | const loader = createLoader({
603 | useQueries: (arg) => ({
604 | queries: {
605 | pokemon: useGetPokemonByNameQuery(arg),
606 | },
607 | }),
608 | queriesArg: (props: {
609 | name: string;
610 | onChange: (name: string) => void;
611 | }) => props.name,
612 | onLoading: () => Loading
,
613 | onFetching: () => Fetching
,
614 | }).extend({
615 | onFetching: () => Extended Fetching
,
616 | });
617 |
618 | const Component = withLoader((props, loaderData) => {
619 | return (
620 |
621 | Success{" "}
622 | {loaderData.queries.pokemon.data.name}
623 | props.onChange(props.name + "a")}
625 | >
626 | Refetch
627 |
628 |
629 | );
630 | }, loader);
631 |
632 | const Controller = () => {
633 | const [name, setName] = useState("a");
634 | return ;
635 | };
636 |
637 | render( );
638 | expect(screen.getByText("Loading")).toBeVisible();
639 | await waitFor(() =>
640 | expect(screen.getByRole("button")).toBeVisible()
641 | );
642 | await userEvent.click(screen.getByRole("button"));
643 | await waitFor(() =>
644 | expect(
645 | screen.getByText("Extended Fetching")
646 | ).toBeVisible()
647 | );
648 | });
649 |
650 | test("Can extend whileFetching", async () => {
651 | const loader = createLoader({
652 | useQueries: (arg: string) => ({
653 | queries: {
654 | pokemon: useGetPokemonByNameQuery(arg),
655 | },
656 | }),
657 | queriesArg: (props: {
658 | name: string;
659 | onChange: (name: string) => void;
660 | }) => props.name,
661 | onLoading: () => Loading
,
662 | whileFetching: {
663 | prepend: () => Fetching
,
664 | },
665 | }).extend({
666 | whileFetching: {
667 | prepend: () => Extended Fetching ,
668 | },
669 | });
670 | expect(loader.whileFetching?.prepend).not.toBeUndefined();
671 |
672 | const Component = withLoader((props, loaderData) => {
673 | return (
674 |
675 | Success{" "}
676 | {loaderData.queries.pokemon.data.name}
677 | props.onChange(props.name + "a")}
679 | >
680 | Refetch
681 |
682 |
683 | );
684 | }, loader);
685 |
686 | const Controller = () => {
687 | const [name, setName] = useState("a");
688 | return ;
689 | };
690 |
691 | render( );
692 | expect(screen.getByText("Loading")).toBeVisible();
693 | await waitFor(() =>
694 | expect(screen.getByRole("button")).toBeVisible()
695 | );
696 | await userEvent.click(screen.getByRole("button"));
697 | await waitFor(() =>
698 | expect(
699 | screen.queryByText(/extended fetching/i)
700 | ).toBeVisible()
701 | );
702 | });
703 |
704 | test("Can extend queries", async () => {
705 | const loader = createLoader({
706 | useQueries: (arg: string) => ({
707 | queries: {
708 | pokemon: useGetPokemonByNameQuery(arg),
709 | },
710 | }),
711 | queriesArg: (props: { name: string }) => props.name,
712 | onLoading: () => Loading
,
713 | }).extend({
714 | useQueries: (arg: string) => ({
715 | queries: {
716 | pokemon: useGetPokemonByNameQuery(arg),
717 | pokemons: useGetPokemonsQuery(undefined),
718 | },
719 | }),
720 | });
721 |
722 | const Component = withLoader((props, loaderData) => {
723 | return (
724 |
725 |
726 | {loaderData.queries.pokemon.data.name}
727 | {loaderData.queries.pokemons.data.results.map(
728 | (pokemon) => (
729 | {pokemon.name}
730 | )
731 | )}
732 |
733 |
734 | );
735 | }, loader);
736 |
737 | render( );
738 | expect(screen.getByText("Loading")).toBeVisible();
739 | await waitFor(() =>
740 | expect(screen.getByText("test")).toBeVisible()
741 | );
742 | expect(screen.getByText(/charizard/i)).toBeVisible();
743 | expect(screen.getByText(/pikachu/i)).toBeVisible();
744 | });
745 |
746 | test("Can extend deferred queries", async () => {
747 | const Component = withLoader(
748 | (props, { charizard, delay }) => {
749 | return (
750 | <>
751 | {charizard.name}
752 |
753 | {delay ? "loaded-deferred" : "loading-deferred"}
754 |
755 | >
756 | );
757 | },
758 | createLoader({
759 | useQueries: () => ({
760 | queries: {
761 | pokemons: useGetPokemonsQuery(undefined),
762 | },
763 | deferredQueries: {
764 | charizard: useGetPokemonByNameQuery("charizard"),
765 | },
766 | }),
767 | }).extend({
768 | useQueries: () => ({
769 | queries: {
770 | charizard: useGetPokemonByNameQuery("charizard"),
771 | },
772 | deferredQueries: {
773 | delay: useGetPokemonByNameQuery("delay"),
774 | },
775 | }),
776 | transform: (loader) => ({
777 | charizard: loader.queries.charizard.data,
778 | delay: loader.deferredQueries.delay.data,
779 | }),
780 | onLoading: () => Loading
,
781 | onError: () => Error
,
782 | })
783 | );
784 | render( );
785 | expect(screen.getByText("Loading")).toBeVisible();
786 | await waitFor(() =>
787 | expect(screen.getByText("charizard")).toBeVisible()
788 | );
789 | expect(screen.getByText("loading-deferred")).toBeVisible();
790 | await waitFor(() =>
791 | expect(screen.getByText("loaded-deferred")).toBeVisible()
792 | );
793 | });
794 |
795 | test("Can extend many times", async () => {
796 | const Component = withLoader(
797 | (props, loader) => {
798 | return (
799 |
800 |
{loader.queries.pokemon.data.name}
801 |
{loader.foobar}
802 |
loader.payload.setName("error")}
804 | >
805 | error
806 |
807 |
808 | );
809 | },
810 | createLoader({
811 | onLoading: () => Loading
,
812 | })
813 | .extend({
814 | useQueries: () => {
815 | const [name, setName] = useState("charizard");
816 | return {
817 | queries: {
818 | pokemon: useGetPokemonByNameQuery(name),
819 | },
820 | payload: {
821 | name,
822 | setName,
823 | },
824 | };
825 | },
826 | })
827 | .extend({
828 | onLoading: () => Extended Loading One
,
829 | })
830 | .extend({
831 | onError: () => Extended Error
,
832 | })
833 | .extend({
834 | transform: (data) => ({ ...data, foobar: "foobar" }),
835 | })
836 | .extend({
837 | onLoading: () => Extended Loading Two
,
838 | })
839 | );
840 |
841 | render( );
842 | expect(
843 | screen.getByText("Extended Loading Two")
844 | ).toBeVisible();
845 | await waitFor(() =>
846 | expect(screen.getByText("charizard")).toBeVisible()
847 | );
848 | expect(screen.getByText("foobar")).toBeVisible();
849 | await userEvent.click(screen.getByRole("button"));
850 | await waitFor(() =>
851 | expect(screen.getByText("Extended Error")).toBeVisible()
852 | );
853 | });
854 |
855 | test("Can extend with only transform", async () => {
856 | const Component = withLoader(
857 | (props, pokemon) => {
858 | return {pokemon.name}
;
859 | },
860 | createLoader({
861 | useQueries: () => ({
862 | queries: {
863 | pokemon: useGetPokemonByNameQuery("charizard"),
864 | },
865 | }),
866 | }).extend({
867 | transform: (data) => data.queries.pokemon.data,
868 | })
869 | );
870 | render( );
871 | await waitFor(() =>
872 | expect(screen.getByText("charizard")).toBeVisible()
873 | );
874 | });
875 |
876 | test("Can partially extend config", async () => {
877 | const CustomLoader = (props: CustomLoaderProps) => {
878 | if (props.query.isError) {
879 | return Custom error!
;
880 | }
881 | if (props.query.isSuccess && props.query.data) {
882 | return props.onSuccess(props.query.data);
883 | }
884 | return Custom loader!
;
885 | };
886 |
887 | const loader = createLoader({
888 | config: {
889 | loaderComponent: CustomLoader,
890 | deferred: { shouldThrowError: true },
891 | },
892 | useQueries: () => ({
893 | queries: {
894 | charizard: useGetPokemonByNameQuery("charizard"),
895 | },
896 | deferredQueries: {
897 | charizard: useGetPokemonByNameQuery("error"),
898 | },
899 | }),
900 | }).extend({
901 | config: {
902 | deferred: { shouldThrowError: false },
903 | },
904 | });
905 |
906 | const Component = withLoader(
907 | (_, loaderData) => (
908 | {loaderData.queries.charizard.data.name}
909 | ),
910 | loader
911 | );
912 | render( );
913 |
914 | // We expect that the custom loader is rendered,
915 | // But that the deferredQuery does not cause component to render error view
916 | expect(screen.getByText("Custom loader!")).toBeVisible();
917 | await waitFor(() =>
918 | expect(screen.getByText("charizard")).toBeVisible()
919 | );
920 | });
921 | });
922 | });
923 |
--------------------------------------------------------------------------------
/testing-app/src/utils.tsx:
--------------------------------------------------------------------------------
1 | import { render, RenderOptions } from "@testing-library/react";
2 | import React, { FC, ReactElement } from "react";
3 | import { Provider } from "react-redux";
4 | import { store } from "./store";
5 |
6 | const AllTheProviders: FC<{ children: React.ReactNode }> = ({
7 | children,
8 | }) => {
9 | return {children} ;
10 | };
11 |
12 | const customRender = (
13 | ui: ReactElement,
14 | options?: Omit
15 | ) => render(ui, { wrapper: AllTheProviders, ...options });
16 |
17 | // re-export everything
18 | export * from "@testing-library/react";
19 | // override render method
20 | export { customRender as render };
21 |
--------------------------------------------------------------------------------
/testing-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "paths": {
19 | "react": ["./node_modules/@types/react"]
20 | }
21 | },
22 | "include": ["src", "jest.polyfills.js"]
23 | }
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src"],
3 | "exclude": ["dist", "node_modules", "docs"],
4 | "compilerOptions": {
5 | "module": "esnext",
6 | "lib": ["dom", "esnext"],
7 | "importHelpers": true,
8 | "declaration": true,
9 | "sourceMap": true,
10 | "rootDir": "./src",
11 | "outDir": "./dist/esm",
12 | "strict": true,
13 | "noImplicitReturns": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "moduleResolution": "node",
18 | "jsx": "react",
19 | "esModuleInterop": true,
20 | "skipLibCheck": true,
21 | "forceConsistentCasingInFileNames": true,
22 | "paths": {
23 | "react": ["./node_modules/@types/react"]
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@reduxjs/toolkit@^2.2.5":
6 | version "2.2.5"
7 | resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.2.5.tgz#c0d2d8482ef80722bebe015ff05b06c34bfb6e0d"
8 | integrity sha512-aeFA/s5NCG7NoJe/MhmwREJxRkDs0ZaSqt0MxhWUrwCf1UQXpwR87RROJEql0uAkLI6U7snBOYOcKw83ew3FPg==
9 | dependencies:
10 | immer "^10.0.3"
11 | redux "^5.0.1"
12 | redux-thunk "^3.1.0"
13 | reselect "^5.1.0"
14 |
15 | "@types/prop-types@*":
16 | version "15.7.12"
17 | resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz"
18 | integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
19 |
20 | "@types/react-dom@^18.2.15":
21 | version "18.3.0"
22 | resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
23 | integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==
24 | dependencies:
25 | "@types/react" "*"
26 |
27 | "@types/react@*", "@types/react@^18.2.37":
28 | version "18.3.3"
29 | resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f"
30 | integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==
31 | dependencies:
32 | "@types/prop-types" "*"
33 | csstype "^3.0.2"
34 |
35 | csstype@^3.0.2:
36 | version "3.1.3"
37 | resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz"
38 | integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
39 |
40 | immer@^10.0.3:
41 | version "10.1.1"
42 | resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc"
43 | integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==
44 |
45 | "js-tokens@^3.0.0 || ^4.0.0":
46 | version "4.0.0"
47 | resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
48 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
49 |
50 | loose-envify@^1.1.0:
51 | version "1.4.0"
52 | resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
53 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
54 | dependencies:
55 | js-tokens "^3.0.0 || ^4.0.0"
56 |
57 | react@^18.2.0:
58 | version "18.3.1"
59 | resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
60 | integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
61 | dependencies:
62 | loose-envify "^1.1.0"
63 |
64 | redux-thunk@^3.1.0:
65 | version "3.1.0"
66 | resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
67 | integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==
68 |
69 | redux@^5.0.1:
70 | version "5.0.1"
71 | resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b"
72 | integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
73 |
74 | reselect@^5.1.0:
75 | version "5.1.0"
76 | resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.0.tgz#c479139ab9dd91be4d9c764a7f3868210ef8cd21"
77 | integrity sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==
78 |
79 | tslib@^2.4.0:
80 | version "2.6.2"
81 | resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
82 | integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
83 |
84 | typescript@^5.4.5:
85 | version "5.4.5"
86 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
87 | integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
88 |
--------------------------------------------------------------------------------