Skip to content

Commit

Permalink
refactor: react-core docs, cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Aug 13, 2024
1 parent 54d1490 commit e1a7a23
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 22 deletions.
6 changes: 6 additions & 0 deletions .changeset/dry-windows-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@siafoundation/design-system': minor
'@siafoundation/react-core': minor
---

App setting allowCustomApi is now more accurately named loginWithCustomApi.
10 changes: 10 additions & 0 deletions .changeset/long-pens-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@siafoundation/hostd-react': minor
'@siafoundation/react-core': minor
'@siafoundation/renterd-react': minor
'@siafoundation/units': minor
'@siafoundation/walletd-react': minor
---

The network block height calculation methods have been moved to the units
package.
23 changes: 12 additions & 11 deletions libs/design-system/src/app/AppLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ function getDefaultValues(api: string) {
}

function getFields({
allowCustomApi,
loginWithCustomApi,
}: {
allowCustomApi: boolean
loginWithCustomApi: boolean
}): ConfigFields<ReturnType<typeof getDefaultValues>, never> {
return {
api: {
Expand All @@ -37,7 +37,8 @@ function getFields({
placeholder: 'http://127.0.0.1:9980',
validation: {
validate: {
required: (value) => !allowCustomApi || !!value || 'API is required',
required: (value) =>
!loginWithCustomApi || !!value || 'API is required',
url: (value) => {
try {
const url = new URL(value)
Expand Down Expand Up @@ -123,7 +124,7 @@ type Props = {
export function AppLogin({ appName, route, routes }: Props) {
const router = usePagesRouter()
const { settings, setSettings } = useAppSettings()
const { allowCustomApi } = settings
const { loginWithCustomApi } = settings

const defaultValues = useMemo(
() => getDefaultValues(settings.api),
Expand All @@ -139,12 +140,12 @@ export function AppLogin({ appName, route, routes }: Props) {
useEffect(() => {
form.clearErrors()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allowCustomApi])
}, [loginWithCustomApi])

const onValid = useCallback(
async (values: typeof defaultValues) => {
let api = ''
if (allowCustomApi) {
if (loginWithCustomApi) {
const url = new URL(values.api)
api = `${url.protocol}//${url.host}`
}
Expand All @@ -171,7 +172,7 @@ export function AppLogin({ appName, route, routes }: Props) {
}
},
[
allowCustomApi,
loginWithCustomApi,
form,
router,
routes,
Expand All @@ -182,7 +183,7 @@ export function AppLogin({ appName, route, routes }: Props) {
]
)

const fields = getFields({ allowCustomApi })
const fields = getFields({ loginWithCustomApi })
const onInvalid = useOnInvalid(fields)

const error = form.formState.errors.api || form.formState.errors.password
Expand Down Expand Up @@ -211,18 +212,18 @@ export function AppLogin({ appName, route, routes }: Props) {
<DropdownMenuItem
onSelect={() =>
setSettings({
allowCustomApi: !allowCustomApi,
loginWithCustomApi: !loginWithCustomApi,
})
}
>
{allowCustomApi ? 'Hide custom API' : 'Show custom API'}
{loginWithCustomApi ? 'Hide custom API' : 'Show custom API'}
</DropdownMenuItem>
</DropdownMenu>
</div>
<Separator className="w-full mt-2 mb-3" />
<form onSubmit={form.handleSubmit(onValid, onInvalid)}>
<div className="flex flex-col gap-1.5">
{allowCustomApi ? (
{loginWithCustomApi ? (
<ControlGroup>
<FieldText
name="api"
Expand Down
6 changes: 4 additions & 2 deletions libs/hostd-react/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import {
HookArgsSwr,
HookArgsCallback,
HookArgsWithPayloadSwr,
getTestnetZenBlockHeight,
getMainnetBlockHeight,
usePutSwr,
useDeleteFunc,
delay,
Expand Down Expand Up @@ -116,6 +114,10 @@ import {
walletSendRoute,
walletTransactionsRoute,
} from '@siafoundation/hostd-types'
import {
getMainnetBlockHeight,
getTestnetZenBlockHeight,
} from '@siafoundation/units'

// state

Expand Down
122 changes: 120 additions & 2 deletions libs/react-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,124 @@

Core library for building React hooks for interacting with a Sia daemon.

## Running unit tests
## usage

Run `nx test react-core` to execute the unit tests via [Jest](https://jestjs.io).
react-core APIs are for building [swr](https://swr.vercel.app) interacting with
a Sia daemon.

### Declarative data fetching

To build a hook for making a GET request to renterd for a contract we would
create types for the params and response and use them with `HookArgsSwr` to
ensure the resulting hook is correctly typed.

```ts
type ContractParams = { id: string; extra?: string }
export function useContract(
args: HookArgsSwr<ContractParams, ContractResponse>
) {
return useGetSwr({ ...args, route: '/contracts/:id' })
}
```

The resulting hook is then used like any other swr hook, the return value is an
`SWRResponse` where data is of type `ConsensusStateResponse` and error is of
type `SWRError`.

```ts
const { data, isValidating, error } = useContract({
params: { id: '123', extra: 'abc' },
})
```

The hook must be called with any required params. Params that are specified in
the route string are automatically replaced with the provided values, any others
are added to the query string. The above hook would make a request to
`/contracts/123?extra=abc`.

#### Configure swr and axios

`useGetSwr` and its siblings all accept a config object which can be used to
configure swr and axios. For example, to refresh the data every 10 seconds and
dedupe the request:

```ts
useGetSwr({
route: '/contracts/:id',
config: {
swr: {
refreshInterval: 10_000,
dedupingInterval: 10_000,
},
},
})
```

To use an abort signal with a long-running request:

```ts
useGetSwr({
route: '/contracts/:id',
config: {
axios: {
signal: controller.signal,
},
},
})
```

### Imperative mutations

Declarative hooks are great for fetching data, but sometimes you need a method
that can be used more imperatively, for example when submitting a form.

`usePostFunc` and its siblings are used for imperative mutations. Instead of
declaratively fetching, the following hook returns a method that can be called.
Besides the `HookArgsCallback` and route string, the method takes another
callback argument which provides access to a `mutate` function. The callback is
triggered any time the contract add method is called successfully. The `mutate`
function can be used to trigger revalidation on dependent keys to refresh data
that is affected by `useContractAdd`, such as the contracts list.

```ts
export function useContractAdd(
args?: HookArgsCallback<
ContractsAddParams,
ContractsAddPayload,
ContractsAddResponse
>
) {
return usePostFunc({ ...args, route: busContractIdNewRoute }, async (mutate) => {
mutate((key) => {
return key.startsWith(busContractRoute)
})
})
}

const addContract = useContractAdd()

function handleSubmit(values: Values) {
const { status, data, error } = await addContract({
params: { id: '123', extra: 'abc' },
payload: {
contract: contractRevision
startHeight: 40000
totalCost: '2424242421223232'
}
})
}
```

### Typing hooks

When building hooks, use the following types for the args parameter:

| name | args type |
| ------------- | ------------------------------------------------- |
| useGetSwr | HookArgsSwr<Params, Response> |
| usePostSwr | HookArgsWithPayloadSwr<Params, Payload, Response> |
| usePutSwr | HookArgsWithPayloadSwr<Params, Payload, Response> |
| useGetFunc | HookArgsCallback<Params, void, Response> |
| usePostFunc | HookArgsCallback<Params, Payload, Response> |
| usePutFunc | HookArgsCallback<Params, Payload, Response> |
| useDeleteFunc | HookArgsCallback<Params, void, Response> |
3 changes: 3 additions & 0 deletions libs/react-core/src/coreProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ type Props = {
children: React.ReactNode
}

/**
* The core provider includes the workflows provider and the swr config provider.
*/
export function CoreProvider({ fallback, cacheProvider, children }: Props) {
return (
<WorkflowsProvider>
Expand Down
1 change: 0 additions & 1 deletion libs/react-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export * from './useGetDownload'
export * from './useAppSettings/currency'
export * from './useAppSettings'
export * from './useTryUntil'
export * from './blockHeight'
export * from './userPrefersReducedMotion'
export * from './mutate'

Expand Down
8 changes: 6 additions & 2 deletions libs/react-core/src/useAppSettings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type CurrencyDisplay = 'sc' | 'fiat' | 'bothPreferSc' | 'bothPreferFiat'

export type AppSettings = {
api: string
allowCustomApi: boolean
loginWithCustomApi: boolean
siaCentral: boolean
password?: string
currency: CurrencyOption
Expand All @@ -30,7 +30,7 @@ export type AppSettings = {

const defaultSettings: AppSettings = {
api: '',
allowCustomApi: false,
loginWithCustomApi: false,
siaCentral: true,
password: undefined,
currency: currencyOptions[0],
Expand Down Expand Up @@ -172,6 +172,10 @@ function useAppSettingsMain({
type State = ReturnType<typeof useAppSettingsMain>

const SettingsContext = createContext({} as State)
/**
* The app settings context allows you to configure all app settings and
* preferences such as the api address, password, currency, etc.
*/
export const useAppSettings = () => useContext(SettingsContext)

export function AppSettingsProvider({ children, ...props }: Props) {
Expand Down
5 changes: 5 additions & 0 deletions libs/react-core/src/workflows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ function useWorkflowsMain() {
type State = ReturnType<typeof useWorkflowsMain>

const WorkflowsContext = createContext({} as State)
/**
* The workflows context is a generic way to mark and track any long-running as in-progress.
* For example, all mutation hooks (eg: usePostFunc) automatically are automatically tracked
* as workflows using their unique route key.
*/
export const useWorkflows = () => useContext(WorkflowsContext)

type Props = {
Expand Down
7 changes: 5 additions & 2 deletions libs/renterd-react/src/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
HookArgsSwr,
HookArgsCallback,
HookArgsWithPayloadSwr,
getMainnetBlockHeight,
getTestnetZenBlockHeight,
delay,
} from '@siafoundation/react-core'
import {
getMainnetBlockHeight,
getTestnetZenBlockHeight,
} from '@siafoundation/units'
import {
AccountResetDriftParams,
AccountResetDriftPayload,
Expand Down Expand Up @@ -520,6 +522,7 @@ export function useHostsInteractionAdd(
route: busHostsHostKeyRoute,
})
}

export function useHostsBlocklist(
args?: HookArgsSwr<HostsBlocklistParams, HostsBlocklistResponse>
) {
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions libs/units/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export * from './address'
export * from './humanUnits'
export * from './currency'
export * from './blockTime'
export * from './blockHeight'
export * from './bytes'
export * from './valuePer'
6 changes: 4 additions & 2 deletions libs/walletd-react/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import {
HookArgsSwr,
HookArgsCallback,
delay,
getMainnetBlockHeight,
getTestnetZenBlockHeight,
useDeleteFunc,
} from '@siafoundation/react-core'
import {
getMainnetBlockHeight,
getTestnetZenBlockHeight,
} from '@siafoundation/units'
import {
ConsensusNetworkParams,
ConsensusNetworkResponse,
Expand Down

0 comments on commit e1a7a23

Please sign in to comment.