32
loading...
This website collects cookies to deliver better user experience
ping
function:export const ping = (url) => {
return new Promise((res) => {
fetch(url)
.then(() => res(true))
.catch(() => res(false))
})
}
ping
function in a modern browser would work just fine.import { ping } from "./ping"
ping("https://logrocket.com").then((status) => {
console.log(status ? "site is up" : "site is down")
})
fetch
is not implemented in Node.js. However, there are many fetch
implementations and polyfills for Node.js that we can use.fetch
into an injectable dependency of ping
, like so:export const ping = (url, fetch = window.fetch) => {
return new Promise((res) => {
fetch(url)
.then(() => res(true))
.catch(() => res(false))
})
}
fetch
a default value of window.fetch
, but not requiring us to include it every time we use ping
makes for a better development experience.import fetch from "node-fetch"
import { ping } from "./ping"
ping("https://logrocket.com", fetch).then((status) => {
console.log(status ? "site is up" : "site is down")
})
func(param, dep1, dep2, dep3,…)
. Instead, a better option is to have an object for dependencies:const ping = (url, deps) => {
const { fetch, log } = { fetch: window.fetch, log: console.log, ...deps }
log("ping")
return new Promise((res) => {
fetch(url)
.then(() => res(true))
.catch(() => res(false))
})
}
ping("https://logrocket.com", {
log(str) {
console.log("logging: " + str)
}
})
deps
will be spread into an implementation object and will override the functions that it provides. By destructuring from this modified object, the surviving properties will be used as dependencies.import { useTrack } from '~/hooks'
function Save() {
const { track } = useTrack()
const handleClick = () => {
console.log("saving...")
track("saved")
}
return <button onClick={handleClick}>Save</button>
}
useTrack
(and by extension, track
) is something to avoid. Therefore, we will convert useTrack
into a dependency of the Save
component via props:import { useTracker as _useTrack } from '~/hooks'
function Save({ useTrack = _useTrack }) {
const { track } = useTrack()
/* ... */
}
useTracker
to avoid name collision and using it as a default value of a prop, we preserve the hook in our app and have the ability to override it whenever the need arises._useTracker
is one naming convention out of many: useTrackImpl
, useTrackImplementation
, and useTrackDI
are all widely used conventions when trying to avoid collision.import Save from "./Save"
export default {
component: Save,
title: "Save"
}
const Template = (args) => <Save {...args} />
export const Default = Template.bind({})
Default.args = {
useTrack() {
return { track() {} }
}
}
typeof
implementation to retain type safety:function App({ useTrack = _useTrack }: Props) {
/* ... */
}
interface Props {
/**
* For testing and storybook only.
*/
useTrack?: typeof _useTrack
}
MemoryRouter
, while Apollo Client provides a MockedProvider
. But, if we employ a DI-powered approach, such mocked providers are not necessary.queryClient
with all the default options intact.import { QueryClient, QueryClientProvider } from "react-query"
import { useUserQuery } from "~/api"
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<User />
</QueryClientProvider>
)
}
function User() {
const { data } = useUserQuery()
return <p>{JSON.stringify(data)}</p>
}
// storybook/preview.js
import { QueryClient, QueryClientProvider } from "react-query"
const queryClient = new QueryClient({
queries: {
retry: false,
cacheTime: Number.POSITIVE_INFINITY
}
})
/** @type import('@storybook/addons').DecoratorFunction[] */
export const decorators = [
(Story) => {
return (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
)
},
]