SWR, SWC, and MSW, three similar names, are always mentioned in the context of web development, but they are totally different things. In this article, we will learn each of them and where they are used.
The name “SWR” is derived from stale-while-revalidate
, a cache invalidation strategy. SWR first returns the data from cache (stale), then sends the request (revalidate), and finally comes with the up-to-date data again.
useSWR
accepts a key and a fetcher function. The key is a unique identifier of the request, normally the URL of the API. And the fetcher accepts key as its parameter and returns the data asynchronously. The fetcher can be any asynchronous function, you can use your favourite data-fetching library to handle that part.
import useSWR from 'swr'
// you can use the native fetch or tools like Axios
const fetcher = (...args) => fetch(...args).then(res => res.json())
function Profile () {
const { data, error, isLoading } = useSWR('/api/user/123', fetcher)
if (error) return <div>failed to load</div>
if (isLoading) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
When building a web app, you might need to reuse the data in many places of the UI. It is incredibly easy to create reusable data hooks on top of SWR:
function useUser (id) {
const { data, error, isLoading } = useSWR(`/api/user/${id}`, fetcher)
return {
user: data,
isLoading,
isError: error
}
}
// use it in your components
function Content () {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <h1>Welcome back, {user.name}</h1>
}
function Avatar () {
const { user, isLoading } = useUser()
if (isLoading) return <Spinner />
return <img src={user.avatar} alt={user.name} />
}
By adopting this pattern, you can forget about fetching data in the imperative way: start the request, update the loading state, and return the final result. Instead, your code is more declarative: you just need to specify what data is used by the component.
The most beautiful thing is that there will be only 1 request sent to the API, because they use the same SWR key (normally the API URL) and the request is cached and shared automatically. Also, the application now has the ability to refetch the data on user focus or network reconnect.
When you re-focus a page or switch between tabs, SWR automatically revalidates data. This can be useful to immediately synchronize to the latest state. This is helpful for refreshing data in scenarios like stale mobile tabs, or laptops that went to sleep.
SWR will give you the option to revalidate on interval. You can enable it by setting a refreshInterval
value.
It's useful to also revalidate when the user is back online. This feature is enabled by default.
There're 2 ways to use the mutate API to mutate the data, the global mutate API which can mutate any key and the bound mutate API which only can mutate the data of corresponding SWR hook.
When you call mutate(key)
or just mutate()
with the bound mutate API without any data, it will trigger a revalidation (mark the data as expired and trigger a refetch) for the resource.
// global mutate
import { useSWRConfig } from "swr"
function App() {
const { mutate } = useSWRConfig()
mutate(key, data, options)
}
// bound mutate
function Profile () {
const { data, mutate } = useSWR('/api/user', fetcher)
return (
<div>
<h1>My name is {data.name}.</h1>
<button onClick={async () => {
const newName = data.name.toUpperCase()
// send a request to the API to update the data
await requestUpdateUsername(newName)
// update the local data immediately and revalidate (refetch)
mutate({ ...data, name: newName })
}}>Uppercase my name!</button>
</div>
)
}
SWR also provides useSWRMutation
as a hook for remote mutations.
import useSWRMutation from 'swr/mutation'
async function sendRequest(url, { arg }: { arg: { username: string }}) {
return fetch(url, {
method: 'POST',
body: JSON.stringify(arg)
}).then(res => res.json())
}
function App() {
const { trigger, isMutating } = useSWRMutation('/api/user', sendRequest, /* options */)
return (
<button
disabled={isMutating}
onClick={async () => {
try {
const result = await trigger({ username: 'johndoe' }, /* options */)
} catch (e) {
// error handling
}
}}
>
Create User
</button>
)
}
swrv is a port of SWR for Vue, a Vue library for data fetching. It supports both Vue2 and Vue3.
SWR and React Query (new name: TanStack Query) are the two most popular libraries that can be used to manage data fetching in a React application. SWR is a smaller library that focuses on providing a simple way to fetch and cache data, while React Query is a more comprehensive library that offers a wider range of features.
// Standard fetch in useEffect example
function Bookmarks({ category }) {
const [data, setData] = useState([])
const [error, setError] = useState()
useEffect(() => {
fetch(`${endpoint}/${category}`)
.then(res => res.json())
.then(d => setData(d))
.catch(e => setError(e))
}, [category])
// Return JSX based on data and error state
}
Bugs from the above code:
category
from books
to movies
and the response for movies
arrives before the response for books
, you'll end up with the wrong data in your component. See https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect to know how to fix the useEffect
race condition.category
changes. If we check for error first, we'll render the error UI with the old message even though we have valid data. If we check data first, we have the same problem if the second request fails.<React.StrictMode>
, React will intentionally call your effect twice in development mode to help you find bugs like missing cleanup functions.fetch
doesn't reject on HTTP errors, so you'd have to check for res.ok
and throw an error yourself.If you're going to fetch in useEffect()
, you should at least make sure that you're handling:
import * as React from "react"
export default function useQuery(url) {
const [data, setData] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(true)
const [error, setError] = React.useState(null)
React.useEffect(() => {
let ignore = false // isCancelled
const handleFetch = async () => {
setData(null)
setIsLoading(true)
setError(null)
try {
const res = await fetch(url)
if (ignore) {
return
}
if (res.ok === false) {
throw new Error(`A network error occurred.`)
}
const json = await res.json()
setData(json)
setIsLoading(false)
} catch (e) {
setError(e.message)
setIsLoading(false)
}
}
handleFetch()
return () => {
ignore = true
}
}, [url])
return { data, isLoading, error }
}
In reality, we still need to think about:
That's why React Query was created. With React Query, the above Bookmarks
example code becomes:
const useBookmarks = (category) => {
return useQuery({
queryKey: ['bookmarks', category],
queryFn: async () => {
const response = await fetch(`${endpoint}/${category}`);
if (!response.ok) {
throw new Error('Failed to fetch');
}
return response.json();
},
});
};
const Bookmarks = ({ category }) => {
const { isLoading, data, error } = useBookmarks(category);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>{category} Bookmarks</h2>
<ul>
{data.map((bookmark) => (
<li key={bookmark.id}>{bookmark.title}</li>
))}
</ul>
</div>
);
};
To manage client state in a React app, we have lots of options available, starting from the built-in hooks like useState
and useReducer
, all the way up to community maintained solutions like redux or zustand. But what are our options for managing server state in a React app? Historically, there weren't many. That is, until React Query came along.
A better way to describe React Query is as an async state manager that is also acutely aware of the needs of server state. In fact, React Query doesn't fetch any data for you. You provide it a promise (whether from fetch, axios, graphql, etc.), and React Query will then take the data that the promise resolves with and make it available wherever you need it throughout your entire application.
A common mistake people do is try to combine useEffect and useQuery. useQuery already handles the state for you. If you're using a useEffect to somehow manage what you get from useQuery, you're doing it wrong.
The library operates on well-chosen defaults. staleTime
is the duration until a query transitions from fresh to stale. As long as the query is fresh, data will always be read from the cache only - no network request will happen. If the query is stale (which per default is: instantly), you will still get data from the cache, but a background refetch can happen.
As long as a query is being actively used, the cached data will be kept in memory. What about inactive queries? gcTime
is the duration until inactive queries will be removed from the cache. This defaults to 5 minutes, which means that if a query is not being used for 5 minutes, the cache for that query will be cleaned up. Queries transition to the inactive state as soon as there are no observers registered, so when all components which use that query have unmounted.
staleTime
: How long before data is considered stale, when should revalidation happen? (default: 0)gcTime
: How long before inactive data is garbage collected, when should the cache be cleared? (default: 5 minutes)
function TodoList() {
// This query is "active" because the component is using it
const { data } = useQuery({
queryKey: ['todos'],
gcTime: 1000 * 60 * 5 // 5 minutes
})
return <div>{data.map(...)}</div>
}
// When TodoList unmounts (user navigates away), the query becomes "inactive"
// If user doesn't come back to TodoList within 5 minutes (gcTime),
// the data is removed from cache
// If they return within 5 minutes, the cached data is still there!
If you see a refetch that you are not expecting, it is likely because you went to a different browser tab, and then came back to your app. React Query is doing a refetchOnWindowFocus
, and data on the screen will be updated if something has changed on the server in the meantime.
The enabled
option is a very powerful one that can be used in Dependent Queries—queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the enabled
option to tell a query when it is ready to run.
export const useContactDetails = (contactId: string | undefined) =>
useQuery({
queryKey: ["contacts", contactId],
queryFn: () => getContact(contactId!),
enabled: !!contactId,
});
For most queries const { isPending, isError, data, error } = useQuery()
, it's usually sufficient to check for the isPending
state, then the isError
state, then finally, assume that the data is available and render the successful state.
isPending
or status === 'pending'
: If there's no cached data and no query attempt was finished yet.isFetching
is true whenever the queryFn
is executing, which includes initial pending as well as background refetches.isLoading
Is true whenever the first fetch for a query is in-flight. Is same as isFetching && isPending
.Query keys are reactive. When a key changes, React Query knows it needs fresh data. You don't manually trigger refetches, you just change the key, and React Query handles the rest. Your UI becomes a reflection of your query keys. (I don't think I have ever passed a variable to the queryFn
that was not part of the queryKey
)
function TodoList({ filter }) {
const queryClient = useQueryClient();
const { data } = useQuery({
queryKey: ["todos", filter],
queryFn: () => fetchTodos(filter),
// To prevent loading state when you switch for the first time,
// we can pre-fill the newly created cache entry with `initialData`.
initialData: () => {
return queryClient.getQueryData(['todos', 'all']);
},
// `initialData` goes straight to the cache,
// while `placeholderData` is not persisted to the cache.
placeholderData: (previousData) => previousData,
// Transform or select a part of the data returned by the query function
select: (data) => { ... },
// Refetch every 5 seconds
refetchInterval: 5000,
});
}
// Search with URL state
const { search } = useSearchParams();
useQuery({
queryKey: ["search", search],
queryFn: () => searchItems(search),
});
// besides `useQuery`, there's also `useMutation`
function App() {
const postQuery = useQuery({
queryKey: ['post'],
queryFn: () => fetch(...).then(res => res.json()),
})
// const queryClient = useQueryClient()
const newPostMutation = useMutation({
mutationFn: async (newTitle) => {
const response = await fetch(...)
return response.json()
},
onSuccess: (data) => {
// update the cache
queryClient.invalidateQueries({ queryKey: ['post'] })
},
onError: () => {
// roll back the optimistic update
},
onSettled: () => {
// always run this, regardless of success or error
},
})
return (
<div>
{ postQuery.data.map(post => <div key={post.id}>{post.title}</div>) }
<button
disabled={newPostMutation.isLoading}
onClick={() => newPostMutation.mutate('My new post')}>
Create new
</button>
</div>
)
}
// pagination example
const {
data,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['users'],
queryFn: getUsers,
initialPageParam: 1,
// fetch('/api/users?cursor=0')
// { data: [...], nextCursor: 3}
// fetch('/api/users?cursor=3')
// { data: [...], nextCursor: 6}
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
// prefetching example
const queryClient = useQueryClient()
const prefetch = () => {
queryClient.prefetchQuery({...})
}
return (
<button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}>
Show Details
</button>
)
Pinia Colada is the smart data fetching layer for Vue.js. You don't even need to learn Pinia to use Pinia Colada because it exposes its own composables.
Pinia Colada shares similarities with TanStack Query and has adapted some of its APIs for easier migration. However, Pinia Colada is tailored specifically for Vue, resulting in a lighter library with better and official integrations like Data Loaders. If you're familiar with TanStack Query, you'll find Pinia Colada intuitive and easy to use.
SWC (stands for Speedy Web Compiler) is a super-fast TypeScript / JavaScript compiler written in Rust, and can be used for both compilation and bundling. SWC is 20x faster than babel on a single-core benchmark, 68x faster than babel on a multicore benchmark.
JavaScript can only work on one core at a time. Languages like Go and Rust have multi-threading support built-in, which means they can use multiple CPU cores to parallelize as much work as possible.
npm i -D @swc/cli @swc/core
# Transpile one file and emit to stdout
npx swc ./file.js
# Transpile one file and emit to `output.js`
npx swc ./file.js -o output.js
SWC is able to bundle multiple JavaScript or TypeScript files into one. This feature is currently named spack
. ——This feature is still under construction. Also, the main author of SWC works for Turbopack by Vercel, so this feature is not a something that will be actively developed.
SWC is now a mature replacement for Babel, which was used in Vite 3.0. Vite 4.0 adds support for SWC. From Vite 4, two plugins are available for React projects with different tradeoffs.
@vitejs/plugin-react
is the default Vite plugin for React projects, which uses esbuild and Babel.@vitejs/plugin-react-swc
uses SWC to transform your code.
- SWC is a compiler, whereas esbuild is a bundler. SWC has limited bundling capabilities, so if you're looking for something to traverse your code and generate a single file, esbuild is what you want.
tsup
is the simplest way to bundle your TypeScript libraries with no config, powered by esbuild. It can bundle anything that's supported by Node.js natively, namely.js
,.json
,.mjs
, and TypeScript.ts
,.tsx
.
// npm install @swc/core @swc/cli --save-dev
const { transformFileSync } = require('@swc/core');
const fs = require('fs');
const path = require('path');
const inputFilePath = path.join(__dirname, 'example.jsx');
const outputFilePath = path.join(__dirname, 'example.js');
const output = transformFileSync(inputFilePath, {
jsc: {
parser: {
syntax: 'ecmascript',
jsx: true
},
transform: {
react: {
runtime: 'classic', // use React.createElement
}
}
}
});
fs.writeFileSync(outputFilePath, output.code);
console.log('Transformation complete. Output written to example.js');
Oxc is building a parser, linter, formatter, transpiler, minifier, resolver ... all written in Rust. This project shares the same philosophies as Biome. JavaScript tooling could be rewritten in a more performant language.
Oxlint is a JavaScript linter designed to catch erroneous or useless code without requiring any configurations by default. It is generally available at December 12, 2023.
Rolldown is a Rust-based next-generation bundler with Rollup-compatible API. Oxc acts as foundational layer for Rolldown, providing the necessary building blocks for efficient JavaScript and TypeScript processing.
Rolldown is primary designed to serve as the underlying bundler in Vite, with the goal to replace esbuild and Rollup (which are currently used in Vite as dependencies) with one unified build tool. Although designed for Vite, Rolldown is also fully capable of being used as a standalone, general-purpose bundler. It can serve as a drop-in replacement for Rollup in most cases.
A deep analysis on why bundlers are still needed: https://rolldown.rs/guide/in-depth/why-bundlers
Try out the Rolldown-powered Vite today by using the rolldown-vite package instead of the default vite package. It is a drop-in replacement, as Rolldown will become the default bundler for Vite in the future.
tsdown is built on top of Rolldown. While Rolldown is a powerful and general-purpose tool, tsdown is optimized specifically for building libraries. It includes features like automatic TypeScript declaration generation and multiple output formats.
tsdown
was heavily inspired by tsup
, and even incorporates parts of its codebase. While tsup
is built on top of esbuild, tsdown
leverages the power of Rolldown to deliver a faster and more powerful bundling experience.
npx @biomejs/biome init
npx @biomejs/biome format path/to/file
npx @biomejs/biome lint
npx @biomejs/biome check
# https://biomejs.dev/guides/migrate-eslint-prettier
biome migrate eslint --write
biome migrate prettier --write
Mock Service Worker is an API mocking library for browser and Node.js that uses a Service Worker to intercept requests that actually happened. Developers come to MSW for various reasons: to establish proper testing boundaries, to prototype applications, debug network-related issues, or monitor production traffic.
Mock Service Worker intercepts requests on the network level. It respects the Fetch API specification, which means that the mocked responses you construct are the same responses you would receive when making a fetch call.
// MSW 2.0 new syntax
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.text('Hello world!')
}),
]
With MSW, we no longer need to worry about mocking specific libraries like Axios or the fetch method. It provides a library-agnostic solution, enabling consistent tests regardless of the underlying HTTP library used in our projects.