chore(rtk-query): clean up rtk-query-imspector-monitor-demo and add post example

This commit is contained in:
FaberVitale 2021-06-19 12:09:41 +02:00
parent f145746c50
commit d574358c5d
22 changed files with 2880 additions and 212 deletions

View File

@ -70,8 +70,7 @@
"packages/redux-devtools/examples/counter",
"packages/redux-devtools/examples/todomvc",
"packages/redux-devtools/examples/rtk-query-polling",
"packages/redux-devtools-slider-monitor/examples/todomvc",
"packages/redux-devtools-rtk-query-inspector-monitor/demo"
"packages/redux-devtools-slider-monitor/examples/todomvc"
],
"engines": {
"node": ">=10.13.0"

View File

@ -18,13 +18,13 @@ Created by [FaberVitale](https://github.com/FaberVitale)
### npm
```bash
npm i @redux-devtools/rtk-query-inspector-monitor --save # npm
npm i @redux-devtools/rtk-query-inspector-monitor --save
```
### yarn
```bash
yarn add @redux-devtools/rtk-query-inspector-monitor # yarn
yarn add @redux-devtools/rtk-query-inspector-monitor
```
@ -78,11 +78,11 @@ See also
## TODO
- [] display mutations
- [] filter by tags types
- [] download query.data
- [] upload query.data(?)
- [] refetch query button(?)
- [ ] display mutations
- [ ] filter by tags types
- [ ] download query.data
- [ ] upload query.data(?)
- [ ] refetch query button(?)
- ...suggestions are welcome

View File

@ -6,17 +6,19 @@
Run the following commands from redux-devtools monorepo root directory.
### 1. Install depedencies
### 1. Install monorepo depedencies
```bash
yarn
```
### 2. Start demo
### 2. Install demo dependencies
```bash
# working directory is monorepo root
yarn lerna run --parallel start \
--scope '@redux-devtools/rtk-query-inspector-monitor' \
--scope 'rtk-query-imspector-monitor-demo'
yarn exec --cwd 'packages/redux-devtools-rtk-query-inspector-monitor/demo' yarn
```
### 3. Start demo
```bash
yarn lerna run --stream start --scope '@redux-devtools/rtk-query-inspector-monitor'
```

View File

@ -3,20 +3,27 @@
"private": true,
"scripts": {
"start": "cross-env SKIP_PREFLIGHT_CHECK=true react-scripts start",
"build:demo": "cross-env SKIP_PREFLIGHT_CHECK=true react-scripts build"
"build": "cross-env SKIP_PREFLIGHT_CHECK=true react-scripts build"
},
"dependencies": {
"@chakra-ui/react": "1.0.0",
"@emotion/react": "^11.4.0",
"@emotion/styled": "^11.3.0",
"@mswjs/data": "^0.3.0",
"@redux-devtools/core": "^3.9.0",
"@redux-devtools/dock-monitor": "^1.4.0",
"@reduxjs/toolkit": "^1.6.0",
"cross-env": "^7.0.3",
"devui": "^1.0.0-8",
"framer-motion": "^2.9.5",
"lodash.debounce": "^4.0.8",
"msw": "0.28.2",
"react": "^17.0.2",
"react-base16-styling": "^0.8.0",
"react-dom": "^17.0.2",
"react-json-tree": "^0.15.0",
"react-redux": "^7.2.1",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.2",
"redux": "^4.0.5",
"redux-devtools-themes": "^1.0.0"
@ -25,6 +32,7 @@
"@types/react": "17.0.0",
"@types/react-dom": "17.0.0",
"@types/react-redux": "7.1.9",
"@types/react-router-dom": "5.1.6",
"typescript": "~4.0.7"
},
"eslintConfig": {
@ -37,5 +45,8 @@
"not dead",
"not ie <= 11",
"not op_mini all"
]
],
"msw": {
"workerDirectory": "public"
}
}

View File

@ -0,0 +1,322 @@
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
/* eslint-disable */
/* tslint:disable */
const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187'
const bypassHeaderName = 'x-msw-bypass'
const activeClientIds = new Set()
self.addEventListener('install', function () {
return self.skipWaiting()
})
self.addEventListener('activate', async function (event) {
return self.clients.claim()
})
self.addEventListener('message', async function (event) {
const clientId = event.source.id
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll()
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
// Resolve the "master" client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMasterClient(event) {
const client = await self.clients.get(event.clientId)
if (client.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll()
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
async function handleRequest(event, requestId) {
const client = await resolveMasterClient(event)
const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
;(async function () {
const clonedResponse = response.clone()
sendToClient(client, {
type: 'RESPONSE',
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body:
clonedResponse.body === null ? null : await clonedResponse.text(),
headers: serializeHeaders(clonedResponse.headers),
redirected: clonedResponse.redirected,
},
})
})()
}
return response
}
async function getResponse(event, client, requestId) {
const { request } = event
const requestClone = request.clone()
const getOriginalResponse = () => fetch(requestClone)
// Bypass mocking when the request client is not active.
if (!client) {
return getOriginalResponse()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return await getOriginalResponse()
}
// Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === 'true') {
const cleanRequestHeaders = serializeHeaders(requestClone.headers)
// Remove the bypass header to comply with the CORS preflight check.
delete cleanRequestHeaders[bypassHeaderName]
const originalRequest = new Request(requestClone, {
headers: new Headers(cleanRequestHeaders),
})
return fetch(originalRequest)
}
// Send the request to the client-side MSW.
const reqHeaders = serializeHeaders(request.headers)
const body = await request.text()
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: reqHeaders,
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body,
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
})
switch (clientMessage.type) {
case 'MOCK_SUCCESS': {
return delayPromise(
() => respondWithMock(clientMessage),
clientMessage.payload.delay,
)
}
case 'MOCK_NOT_FOUND': {
return getOriginalResponse()
}
case 'NETWORK_ERROR': {
const { name, message } = clientMessage.payload
const networkError = new Error(message)
networkError.name = name
// Rejecting a request Promise emulates a network error.
throw networkError
}
case 'INTERNAL_ERROR': {
const parsedBody = JSON.parse(clientMessage.payload.body)
console.error(
`\
[MSW] Request handler function for "%s %s" has thrown the following exception:
${parsedBody.errorType}: ${parsedBody.message}
(see more detailed error stack trace in the mocked response body)
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error.
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url,
)
return respondWithMock(clientMessage)
}
}
return getOriginalResponse()
}
self.addEventListener('fetch', function (event) {
const { request } = event
// Bypass navigation requests.
if (request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = uuidv4()
return event.respondWith(
handleRequest(event, requestId).catch((error) => {
console.error(
'[MSW] Failed to mock a "%s" request to "%s": %s',
request.method,
request.url,
error,
)
}),
)
})
function serializeHeaders(headers) {
const reqHeaders = {}
headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name]
? [].concat(reqHeaders[name]).concat(value)
: value
})
return reqHeaders
}
function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(JSON.stringify(message), [channel.port2])
})
}
function delayPromise(cb, duration) {
return new Promise((resolve) => {
setTimeout(() => resolve(cb()), duration)
})
}
function respondWithMock(clientMessage) {
return new Response(clientMessage.payload.body, {
...clientMessage.payload,
headers: clientMessage.payload.headers,
})
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0
const v = c == 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}

View File

@ -1,58 +1,65 @@
import PokemonView from 'features/pokemon/PokemonView';
import PostsView from 'features/posts/PostsView';
import { Flex, Heading } from '@chakra-ui/react';
import { Link, UnorderedList, ListItem } from '@chakra-ui/react';
import { Code } from '@chakra-ui/react';
import * as React from 'react';
import { Pokemon } from './Pokemon';
import { PokemonName, POKEMON_NAMES } from './pokemon.data';
import './styles.css';
const getRandomPokemonName = () =>
POKEMON_NAMES[Math.floor(Math.random() * POKEMON_NAMES.length)];
export default function App() {
const [pokemon, setPokemon] = React.useState<PokemonName[]>(['bulbasaur']);
export function App() {
return (
<article>
<h1>RTK Query inspector monitor demo</h1>
<section className="App">
<h2>Pokemon polling demo</h2>
<div className="demo-toolbar">
<button
onClick={() =>
setPokemon((prev) => [...prev, getRandomPokemonName()])
}
>
Add random pokemon
</button>
<button onClick={() => setPokemon((prev) => [...prev, 'bulbasaur'])}>
Add bulbasaur
</button>
</div>
<div className="pokemon-list">
{pokemon.map((name, index) => (
<Pokemon key={index} name={name} />
))}
</div>
</section>
<section>
<h2>Dock controls</h2>
<Heading as="h1">RTK Query inspector monitor demo</Heading>
<PokemonView />
<PostsView />
<Flex p="2" as="section" flexWrap="nowrap" flexDirection="column">
<Heading as="h2">Dock controls</Heading>
<pre>
<code>
<Code>
{`toggleVisibilityKey="ctrl-h"\nchangePositionKey="ctrl-q"`}
</code>
</Code>
</pre>
</section>
<footer>
<p>
<a href="https://github.com/FaberVitale/redux-devtools/tree/feat/rtk-query-monitor/packages/redux-devtools-rtk-query-inspector-monitor/demo">
demo source
</a>
</p>
<p>
<a href="https://github.com/FaberVitale/redux-devtools/tree/feat/rtk-query-monitor/packages/redux-devtools-rtk-query-inspector-monitor">
@redux-devtools/rtk-query-inspector-monitor source
</a>
</p>
</footer>
</Flex>
<Flex p="2" as="footer">
<UnorderedList>
<ListItem>
<Link
className="link"
isExternal
href="https://github.com/FaberVitale/redux-devtools/tree/feat/rtk-query-monitor/packages/redux-devtools-rtk-query-inspector-monitor/demo"
>
demo source
</Link>
</ListItem>
<ListItem>
<Link
className="link"
isExternal
href="https://github.com/FaberVitale/redux-devtools/tree/feat/rtk-query-monitor/packages/redux-devtools-rtk-query-inspector-monitor"
>
@redux-devtools/rtk-query-inspector-monitor source
</Link>
</ListItem>
<ListItem>
<Link
className="link"
isExternal
href="https://github.com/reduxjs/redux-toolkit/tree/master/examples/query/react/polling"
>
polling example
</Link>
</ListItem>
<ListItem>
<Link
className="link"
isExternal
href="https://github.com/reduxjs/redux-toolkit/tree/master/examples/query/react/mutations"
>
mutations example
</Link>
</ListItem>
</UnorderedList>
</Flex>
</article>
);
}

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { useGetPokemonByNameQuery } from './services/pokemon';
import type { PokemonName } from './pokemon.data';
import { Button, Select } from '@chakra-ui/react';
import { useGetPokemonByNameQuery } from '../../services/pokemon';
import type { PokemonName } from '../../pokemon.data';
const intervalOptions = [
{ label: 'Off', value: 0 },
@ -10,9 +11,6 @@ const intervalOptions = [
{ label: '1m', value: 60000 },
];
const getRandomIntervalValue = () =>
intervalOptions[Math.floor(Math.random() * intervalOptions.length)].value;
export function Pokemon({ name }: { name: PokemonName }) {
const [pollingInterval, setPollingInterval] = useState(60000);
@ -44,7 +42,7 @@ export function Pokemon({ name }: { name: PokemonName }) {
</div>
<div>
<label style={{ display: 'block' }}>Polling interval</label>
<select
<Select
value={pollingInterval}
onChange={({ target: { value } }) =>
setPollingInterval(Number(value))
@ -55,12 +53,12 @@ export function Pokemon({ name }: { name: PokemonName }) {
{label}
</option>
))}
</select>
</Select>
</div>
<div>
<button onClick={refetch} disabled={isFetching}>
<Button onClick={refetch} disabled={isFetching}>
{isFetching ? 'Loading' : 'Manually refetch'}
</button>
</Button>
</div>
</>
) : (

View File

@ -0,0 +1,35 @@
import * as React from 'react';
import { Pokemon } from './Pokemon';
import { PokemonName, POKEMON_NAMES } from '../../pokemon.data';
import { Flex, Heading, Button } from '@chakra-ui/react';
const getRandomPokemonName = () =>
POKEMON_NAMES[Math.floor(Math.random() * POKEMON_NAMES.length)];
export default function PokemonView() {
const [pokemon, setPokemon] = React.useState<PokemonName[]>(['bulbasaur']);
return (
<Flex p="2" as="section" flexWrap="nowrap" flexDirection="column">
<Heading as="h2">Pokemon polling demo</Heading>
<div className="demo-toolbar">
<Button
onClick={() =>
setPokemon((prev) => [...prev, getRandomPokemonName()])
}
>
Add random pokemon
</Button>
<Button onClick={() => setPokemon((prev) => [...prev, 'bulbasaur'])}>
Add bulbasaur
</Button>
</div>
<div className="pokemon-list">
{pokemon.map((name, index) => (
<Pokemon key={index} name={name} />
))}
</div>
</Flex>
);
}

View File

@ -0,0 +1,153 @@
import React, { useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import {
useDeletePostMutation,
useGetPostQuery,
useUpdatePostMutation,
} from 'services/posts';
import {
Box,
Button,
Center,
CloseButton,
Flex,
Heading,
Input,
Spacer,
Stack,
useToast,
} from '@chakra-ui/react';
const EditablePostName = ({
name: initialName,
onUpdate,
onCancel,
isLoading = false,
}: {
name: string;
onUpdate: (name: string) => void;
onCancel: () => void;
isLoading?: boolean;
}) => {
const [name, setName] = useState(initialName);
const handleChange = ({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) => setName(value);
const handleUpdate = () => onUpdate(name);
const handleCancel = () => onCancel();
return (
<Flex>
<Box flex={10}>
<Input
type="text"
onChange={handleChange}
value={name}
disabled={isLoading}
/>
</Box>
<Spacer />
<Box>
<Stack spacing={4} direction="row" align="center">
<Button onClick={handleUpdate} isLoading={isLoading}>
Update
</Button>
<CloseButton bg="red" onClick={handleCancel} disabled={isLoading} />
</Stack>
</Box>
</Flex>
);
};
const PostJsonDetail = ({ id }: { id: string }) => {
const { data: post } = useGetPostQuery(id);
return (
<Box mt={5} bg="#eee">
<pre>{JSON.stringify(post, null, 2)}</pre>
</Box>
);
};
export const PostDetail = () => {
const { id } = useParams<{ id: any }>();
const { push } = useHistory();
const toast = useToast();
const [isEditing, setIsEditing] = useState(false);
const { data: post, isLoading } = useGetPostQuery(id);
const [updatePost, { isLoading: isUpdating }] = useUpdatePostMutation();
const [deletePost, { isLoading: isDeleting }] = useDeletePostMutation();
if (isLoading) {
return <div>Loading...</div>;
}
if (!post) {
return (
<Center h="200px">
<Heading size="md">
Post {id} is missing! Try reloading or selecting another post...
</Heading>
</Center>
);
}
return (
<Box p={4}>
{isEditing ? (
<EditablePostName
name={post.name}
onUpdate={async (name) => {
try {
await updatePost({ id, name }).unwrap();
} catch {
toast({
title: 'An error occurred',
description: "We couldn't save your changes, try again!",
status: 'error',
duration: 2000,
isClosable: true,
});
} finally {
setIsEditing(false);
}
}}
onCancel={() => setIsEditing(false)}
isLoading={isUpdating}
/>
) : (
<Flex>
<Box>
<Heading size="md">{post.name}</Heading>
</Box>
<Spacer />
<Box>
<Stack spacing={4} direction="row" align="center">
<Button
onClick={() => setIsEditing(true)}
disabled={isDeleting || isUpdating}
>
{isUpdating ? 'Updating...' : 'Edit'}
</Button>
<Button
onClick={() => deletePost(id).then(() => push('/posts'))}
disabled={isDeleting}
colorScheme="red"
>
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
</Stack>
</Box>
</Flex>
)}
<PostJsonDetail id={post.id} />
</Box>
);
};

View File

@ -0,0 +1,158 @@
import {
Box,
Button,
Center,
Divider,
Flex,
FormControl,
FormLabel,
Heading,
Input,
List,
ListIcon,
ListItem,
Spacer,
Stat,
StatLabel,
StatNumber,
useToast,
} from '@chakra-ui/react';
import { Route, Switch, useHistory } from 'react-router-dom';
import { MdBook } from 'react-icons/md';
import React, { useState } from 'react';
import { Post, useAddPostMutation, useGetPostsQuery } from 'services/posts';
import { PostDetail } from './PostDetail';
const AddPost = () => {
const initialValue = { name: '' };
const [post, setPost] = useState<Pick<Post, 'name'>>(initialValue);
const [addPost, { isLoading }] = useAddPostMutation();
const toast = useToast();
const handleChange = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
setPost((prev) => ({
...prev,
[target.name]: target.value,
}));
};
const handleAddPost = async () => {
try {
await addPost(post).unwrap();
setPost(initialValue);
} catch {
toast({
title: 'An error occurred',
description: "We couldn't save your post, try again!",
status: 'error',
duration: 2000,
isClosable: true,
});
}
};
return (
<Flex p={5}>
<Box flex={10}>
<FormControl isInvalid={Boolean(post.name.length < 3 && post.name)}>
<FormLabel htmlFor="name">Post name</FormLabel>
<Input
id="name"
name="name"
placeholder="Enter post name"
value={post.name}
onChange={handleChange}
/>
</FormControl>
</Box>
<Spacer />
<Box>
<Button
mt={8}
colorScheme="purple"
isLoading={isLoading}
onClick={handleAddPost}
>
Add Post
</Button>
</Box>
</Flex>
);
};
const PostList = () => {
const { data: posts, isLoading } = useGetPostsQuery();
const { push } = useHistory();
if (isLoading) {
return <div>Loading</div>;
}
if (!posts) {
return <div>No posts :(</div>;
}
return (
<List spacing={3}>
{posts.map(({ id, name }) => (
<ListItem key={id} onClick={() => push(`/posts/${id}`)}>
<ListIcon as={MdBook} color="green.500" /> {name}
</ListItem>
))}
</List>
);
};
export const PostsCountStat = () => {
const { data: posts } = useGetPostsQuery();
if (!posts) return null;
return (
<Stat>
<StatLabel>Active Posts</StatLabel>
<StatNumber>{posts?.length}</StatNumber>
</Stat>
);
};
export const PostsManager = () => {
return (
<Box>
<Flex bg="#011627" p={4} color="white">
<Box>
<Heading size="xl">Manage Posts</Heading>
</Box>
<Spacer />
<Box>
<PostsCountStat />
</Box>
</Flex>
<Divider />
<AddPost />
<Divider />
<Flex wrap="wrap">
<Box flex={1} borderRight="1px solid #eee">
<Box p={4} borderBottom="1px solid #eee">
<Heading size="sm">Posts</Heading>
</Box>
<Box p={4}>
<PostList />
</Box>
</Box>
<Box flex={2}>
<Switch>
<Route path="/posts/:id" component={PostDetail} />
<Route>
<Center h="200px">
<Heading size="md">Select a post to edit!</Heading>
</Center>
</Route>
</Switch>
</Box>
</Flex>
</Box>
);
};
export default PostsManager;

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import { Switch, Route } from 'react-router-dom';
import { PostsManager } from 'features/posts/PostsManager';
import { Box, Heading } from '@chakra-ui/react';
function PostsView() {
return (
<Box as="section" p="2">
<Heading as="h2">Posts Demo</Heading>
<Switch>
<Route path="/" component={PostsManager} />
</Switch>
</Box>
);
}
export default PostsView;

View File

@ -13,6 +13,7 @@ code {
}
h1 {
font-weight: 700;
font-size: 1.4em;
}
@ -40,11 +41,11 @@ h6 {
section {
display: block;
max-width: 67vw;
}
.pokemon-list {
display: flex;
max-width: 80vw;
flex-flow: row wrap;
overflow-x: hidden;
}
@ -59,5 +60,9 @@ pre code {
}
article {
padding: 0.4em;
padding: 0 0 0.5em;
}
.link.link {
color: #805ad5;
}

View File

@ -1,17 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import { ChakraProvider } from '@chakra-ui/react';
import './index.css';
import { store } from './store';
import DevTools from './DevTools';
import { BrowserRouter } from 'react-router-dom';
import { App } from 'App';
import { worker } from './mocks/browser';
const rootElement = document.getElementById('root');
function renderApp() {
const rootElement = document.getElementById('root');
ReactDOM.render(
<Provider store={store}>
<App />
<DevTools />
</Provider>,
rootElement
);
ReactDOM.render(
<Provider store={store}>
<ChakraProvider>
<BrowserRouter>
<App />
<DevTools />
</BrowserRouter>
</ChakraProvider>
</Provider>,
rootElement
);
}
worker.start({ quiet: true }).then(renderApp);

View File

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4a43.8 43.8 0 00-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9a487.8 487.8 0 00-41.6-50c32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9a467 467 0 00-63.6 11c-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4a44 44 0 0022.5 5.6c27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7a450.4 450.4 0 01-13.5 39.5 473.3 473.3 0 00-27.5-47.4c14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5a532.7 532.7 0 01-24.1 38.2 520.3 520.3 0 01-90.2.1 551.2 551.2 0 01-45-77.8 521.5 521.5 0 0144.8-78.1 520.3 520.3 0 0190.2-.1 551.2 551.2 0 0145 77.8 560 560 0 01-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8a448.8 448.8 0 01-41.2 8 552.4 552.4 0 0027.4-47.8zM421.2 430a412.3 412.3 0 01-27.8-32 619 619 0 0055.3 0c-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9a451.2 451.2 0 01-41-7.9c3.7-12.9 8.3-26.2 13.5-39.5a473.3 473.3 0 0027.5 47.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32a619 619 0 00-55.3 0c9-11.7 18.3-22.4 27.5-32zm-74 58.9a552.4 552.4 0 00-27.4 47.7c-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9a473.5 473.5 0 00-22.2 60.6c-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9a487.8 487.8 0 0041.6 50c-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9a467 467 0 0063.6-11 280 280 0 015.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9a473.5 473.5 0 0022.2-60.6c9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,4 @@
import { setupWorker } from 'msw';
import { handlers } from './db';
export const worker = setupWorker(...handlers);

View File

@ -0,0 +1,59 @@
import { factory, primaryKey } from '@mswjs/data';
import { nanoid } from '@reduxjs/toolkit';
import { rest } from 'msw';
import { Post } from '../services/posts';
const db = factory({
post: {
id: primaryKey(String),
name: String,
},
});
[
'A sample post',
'A post about RTK Query',
'How to randomly throw errors, a novella',
].forEach((name) => {
db.post.create({ id: nanoid(), name });
});
export const handlers = [
rest.post('/posts', async (req, res, ctx) => {
const { name } = req.body as Partial<Post>;
if (Math.random() < 0.3) {
return res(
ctx.json({ error: 'Oh no, there was an error, try again.' }),
ctx.status(500),
ctx.delay(300)
);
}
const post = db.post.create({
id: nanoid(),
name,
});
return res(ctx.json(post), ctx.delay(300));
}),
rest.put('/posts/:id', (req, res, ctx) => {
const { name } = req.body as Partial<Post>;
if (Math.random() < 0.3) {
return res(
ctx.json({ error: 'Oh no, there was an error, try again.' }),
ctx.status(500),
ctx.delay(300)
);
}
const post = db.post.update({
where: { id: { equals: req.params.id } },
data: { name },
});
return res(ctx.json(post), ctx.delay(300));
}),
...db.post.toHandlers('rest'),
] as const;

View File

@ -0,0 +1,63 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export interface Post {
id: string;
name: string;
}
type PostsResponse = Post[];
export const postsApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Post'],
endpoints: (build) => ({
getPosts: build.query<PostsResponse, void>({
query: () => 'posts',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Post' as const, id })),
{ type: 'Post', id: 'LIST' },
]
: [{ type: 'Post', id: 'LIST' }],
}),
addPost: build.mutation<Post, Partial<Post>>({
query: (body) => ({
url: `posts`,
method: 'POST',
body,
}),
invalidatesTags: [{ type: 'Post', id: 'LIST' }],
}),
getPost: build.query<Post, string>({
query: (id) => `posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }],
}),
updatePost: build.mutation<void, Pick<Post, 'id'> & Partial<Post>>({
query: ({ id, ...patch }) => ({
url: `posts/${id}`,
method: 'PUT',
body: patch,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }],
}),
deletePost: build.mutation<{ success: boolean; id: number }, number>({
query(id) {
return {
url: `posts/${id}`,
method: 'DELETE',
};
},
invalidatesTags: (result, error, id) => [{ type: 'Post', id }],
}),
}),
});
export const {
useGetPostQuery,
useGetPostsQuery,
useAddPostMutation,
useUpdatePostMutation,
useDeletePostMutation,
} = postsApi;

View File

@ -1,13 +1,18 @@
import { configureStore } from '@reduxjs/toolkit';
import { configureStore, Middleware } from '@reduxjs/toolkit';
import { pokemonApi } from './services/pokemon';
import { postsApi } from 'services/posts';
import DevTools from './DevTools';
export const store = configureStore({
reducer: {
[pokemonApi.reducerPath]: pokemonApi.reducer,
[postsApi.reducerPath]: postsApi.reducer,
},
devTools: false,
// adding the api middleware enables caching, invalidation, polling and other features of `rtk-query`
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(pokemonApi.middleware),
getDefaultMiddleware().concat([
pokemonApi.middleware,
postsApi.middleware,
] as Middleware[]),
enhancers: [DevTools.instrument()],
});

View File

@ -4,8 +4,9 @@
"strict": true,
"esModuleInterop": true,
"lib": ["dom", "es2015"],
"jsx": "react-jsx",
"jsx": "react",
"target": "es5",
"baseUrl": "src",
"allowJs": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,

View File

@ -32,7 +32,9 @@
"scripts": {
"build": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"start": "tsc -p ./tsconfig.dev.json --watch",
"start:demo": "cd demo && yarn start",
"start:ts": "tsc -p ./tsconfig.dev.json --watch",
"start": "run-p start:ts start:demo",
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
"clean": "rimraf lib",
"lint": "eslint . --ext .ts,.tsx",
@ -56,6 +58,7 @@
"@redux-devtools/core": "^3.9.0",
"@reduxjs/toolkit": "^1.6.0",
"@types/react": "^16.9.46",
"npm-run-all": "^4.1.5",
"react": "^16.13.1",
"redux": "^4.0.5"
},

126
yarn.lock
View File

@ -4008,6 +4008,7 @@ __metadata:
"@types/redux-devtools-themes": ^1.0.0
devui: ^1.0.0-8
lodash.debounce: ^4.0.8
npm-run-all: ^4.1.5
prop-types: ^15.7.2
react: ^16.13.1
react-json-tree: ^0.15.0
@ -19787,6 +19788,13 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"memorystream@npm:^0.3.1":
version: 0.3.1
resolution: "memorystream@npm:0.3.1"
checksum: 825bcc7d3eb8bd021a1b0f8c81e4d7a8dc2eced1f8bb79d41ec978547cf118146d6863f5e6134f02bb55ee5d963a8689793e6e82ce8eb989bac339ae782728bb
languageName: node
linkType: hard
"meow@npm:^3.3.0":
version: 3.7.0
resolution: "meow@npm:3.7.0"
@ -21092,46 +21100,24 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"npm-registry-client@npm:~7.2.1":
version: 7.2.1
resolution: "npm-registry-client@npm:7.2.1"
"npm-run-all@npm:^4.1.5":
version: 4.1.5
resolution: "npm-run-all@npm:4.1.5"
dependencies:
concat-stream: ^1.5.2
graceful-fs: ^4.1.6
normalize-package-data: ~1.0.1 || ^2.0.0
npm-package-arg: ^3.0.0 || ^4.0.0
npmlog: ~2.0.0 || ~3.1.0
once: ^1.3.3
request: ^2.74.0
retry: ^0.10.0
semver: 2 >=2.2.1 || 3.x || 4 || 5
slide: ^1.1.3
dependenciesMeta:
npmlog:
optional: true
checksum: 2f172e9d4a90e7c5172a705af0db10c96d8ec55ede6141c7fe4116382b492c0e6744b759347111bff0cc1a7d9e4e8b5185007b74b2dbb912de16b1bb4331959d
languageName: node
linkType: hard
"npm-registry-client@npm:~8.4.0":
version: 8.4.0
resolution: "npm-registry-client@npm:8.4.0"
dependencies:
concat-stream: ^1.5.2
graceful-fs: ^4.1.6
normalize-package-data: ~1.0.1 || ^2.0.0
npm-package-arg: ^3.0.0 || ^4.0.0 || ^5.0.0
npmlog: 2 || ^3.1.0 || ^4.0.0
once: ^1.3.3
request: ^2.74.0
retry: ^0.10.0
semver: 2 >=2.2.1 || 3.x || 4 || 5
slide: ^1.1.3
ssri: ^4.1.2
dependenciesMeta:
npmlog:
optional: true
checksum: f16d83cf8eed879080dd758a00995864c77613ed964cc36ab0e9d9ed00d246e6dc31bf2959caf09284710676efb2e8cbc90e30f8b1d30860494a17c1b83dc5f3
ansi-styles: ^3.2.1
chalk: ^2.4.1
cross-spawn: ^6.0.5
memorystream: ^0.3.1
minimatch: ^3.0.4
pidtree: ^0.3.0
read-pkg: ^3.0.0
shell-quote: ^1.6.1
string.prototype.padend: ^3.0.0
bin:
npm-run-all: bin/npm-run-all/index.js
run-p: bin/run-p/index.js
run-s: bin/run-s/index.js
checksum: ef1b5b5a5fe7864d2b45c13de6dbffacde956bfc265117e0d1c8b05ee34264d494e5e65474d46592228e3a00857eae58359782fe7889d73de0a8714e6f9c0e83
languageName: node
linkType: hard
@ -22384,6 +22370,15 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"pidtree@npm:^0.3.0":
version: 0.3.1
resolution: "pidtree@npm:0.3.1"
bin:
pidtree: bin/pidtree.js
checksum: 8a48f063cb60e188bc94c307a309d309e20e9a3c3ca3537a035baf66dba2315f7b175d3a13a3b816db349dad270e347877b5aeae6d763360be650b3d1b1ca9b3
languageName: node
linkType: hard
"pify@npm:^2.0.0, pify@npm:^2.3.0":
version: 2.3.0
resolution: "pify@npm:2.3.0"
@ -24392,7 +24387,7 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"react-dom@npm:^17.0.0, react-dom@npm:^17.0.2":
"react-dom@npm:^17.0.0":
version: 17.0.2
resolution: "react-dom@npm:17.0.2"
dependencies:
@ -24645,7 +24640,7 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"react-redux@npm:^7.2.1, react-redux@npm:^7.2.4":
"react-redux@npm:^7.2.4":
version: 7.2.4
resolution: "react-redux@npm:7.2.4"
dependencies:
@ -24890,7 +24885,7 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"react@npm:^17.0.0, react@npm:^17.0.2":
"react@npm:^17.0.0":
version: 17.0.2
resolution: "react@npm:17.0.2"
dependencies:
@ -26159,31 +26154,6 @@ resolve@^2.0.0-next.3:
languageName: node
linkType: hard
"rtk-query-imspector-monitor-demo@workspace:packages/redux-devtools-rtk-query-inspector-monitor/demo":
version: 0.0.0-use.local
resolution: "rtk-query-imspector-monitor-demo@workspace:packages/redux-devtools-rtk-query-inspector-monitor/demo"
dependencies:
"@redux-devtools/core": ^3.9.0
"@redux-devtools/dock-monitor": ^1.4.0
"@reduxjs/toolkit": ^1.6.0
"@types/react": 17.0.0
"@types/react-dom": 17.0.0
"@types/react-redux": 7.1.9
cross-env: ^7.0.3
devui: ^1.0.0-8
lodash.debounce: ^4.0.8
react: ^17.0.2
react-base16-styling: ^0.8.0
react-dom: ^17.0.2
react-json-tree: ^0.15.0
react-redux: ^7.2.1
react-scripts: 4.0.2
redux: ^4.0.5
redux-devtools-themes: ^1.0.0
typescript: ~4.0.7
languageName: unknown
linkType: soft
"rtk-query-polling@workspace:packages/redux-devtools/examples/rtk-query-polling":
version: 0.0.0-use.local
resolution: "rtk-query-polling@workspace:packages/redux-devtools/examples/rtk-query-polling"
@ -26848,7 +26818,7 @@ resolve@^2.0.0-next.3:
languageName: node
linkType: hard
"shell-quote@npm:1.7.2":
"shell-quote@npm:1.7.2, shell-quote@npm:^1.6.1":
version: 1.7.2
resolution: "shell-quote@npm:1.7.2"
checksum: 3b3d06814ca464cde8594c27bdd57a1f4c06b26ad2988b08b5819f97ac1edfd7cb7313fda1c909da33211972c72c5a7906b7da2b62078109f9d3274d3f404fa9
@ -29314,26 +29284,6 @@ typescript@^4.3.4:
languageName: node
linkType: hard
"typescript@patch:typescript@~4.0.7#builtin<compat/typescript>":
version: 4.0.8
resolution: "typescript@patch:typescript@npm%3A4.0.8#builtin<compat/typescript>::version=4.0.8&hash=ddfc1b"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: d12e73e6fb00f0ed42b10b42493d2eb907f31b8c6eb6cfb896be45d79d8fcbf46c9bc1e2aced88898f91191e3f49c5a13d3f86d01bb386ee29f502c7ccfe0b6a
languageName: node
linkType: hard
typescript@~4.0.7:
version: 4.0.8
resolution: "typescript@npm:4.0.8"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: f7789f9c531dffcf4c849a806627562ad6297f608aab85c0514d87a2ab3e060bcfadd63963735994796c45326eebeb479c004065af47e72ee44ba8c935fc9a54
languageName: node
linkType: hard
"uglify-js@npm:^3.1.4":
version: 3.13.9
resolution: "uglify-js@npm:3.13.9"