Skip to content
Permalink
Browse files

Fix ambiguous import on standalone helper components (fixes #72) (#73)

* Fix ambiguous import issue by renaming the standalone helper components. Improved the TS type definitions.

* Update example to use standalone helpers.

* Add codemods for breaking API changes.

* ES modules don't work from a codemod URL.

* Ignore node_modules in cwd.

* Improve types for promiseFn/deferFn props.

* Bump and align all package versions.
  • Loading branch information...
ghengeveld committed Aug 9, 2019
1 parent 839ff17 commit 5bed28d764bc0033b41295cf741e8cd80c092145
@@ -50,9 +50,10 @@ error states, without assumptions about the shape of your data or the type of re

[abortable fetch]: https://developers.google.com/web/updates/2017/09/abortable-fetch

> ## Upgrading to v6
> ## Upgrading to v8
>
> Version 6 comes with a breaking change. See [Upgrading](#upgrading) for details.
> Version 8 comes with breaking changes. See [Upgrading](#upgrading) for details.
> A [codemod](https://github.com/ghengeveld/react-async/tree/master/codemods) is available.
# Table of Contents

@@ -121,11 +122,26 @@ yarn add react-async
### Upgrading

#### Upgrade to v8

All standalone helper components were renamed to avoid import naming collision.

- `<Initial>` was renamed to `<IfInitial>`.
- `<Pending>` was renamed to `<IfPending>`.
- `<Fulfilled>` was renamed to `<IfFulfilled>`.
- `<Rejected>` was renamed to `<IfRejected`.
- `<Settled>` was renamed to `<IfSettled>`.

> A [codemod](https://github.com/ghengeveld/react-async/tree/master/codemods) is available to automate the upgrade.
#### Upgrade to v6

- `<Async.Pending>` was renamed to `<Async.Initial>`.
- Some of the other helpers were also renamed, but the old ones remain as alias.
- Don't forget to deal with any custom instances of `<Async>` when upgrading.

> A [codemod](https://github.com/ghengeveld/react-async/tree/master/codemods) is available to automate the upgrade.
#### Upgrade to v4

- `deferFn` now receives an `args` array as the first argument, instead of arguments to `run` being spread at the front
@@ -267,7 +283,7 @@ by passing in the state, or with `<Async>` by using Context. Each of these compo
rendering of its children based on the current state.

```jsx
import { useAsync, Pending, Fulfilled, Rejected } from "react-async"
import { useAsync, IfPending, IfFulfilled, IfRejected } from "react-async"
const loadCustomer = async ({ customerId }, { signal }) => {
// ...
@@ -277,16 +293,16 @@ const MyComponent = () => {
const state = useAsync({ promiseFn: loadCustomer, customerId: 1 })
return (
<>
<Pending state={state}>Loading...</Pending>
<Rejected state={state}>{error => `Something went wrong: ${error.message}`}</Rejected>
<Fulfilled state={state}>
<IfPending state={state}>Loading...</IfPending>
<IfRejected state={state}>{error => `Something went wrong: ${error.message}`}</IfRejected>
<IfFulfilled state={state}>
{data => (
<div>
<strong>Loaded some data:</strong>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)}
</Fulfilled>
</IfFulfilled>
</>
)
}
@@ -607,7 +623,7 @@ invoked after the state update is completed. Returns the error to enable chainin
React Async provides several helper components that make your JSX more declarative and less cluttered.
They don't have to be direct children of `<Async>` and you can use the same component several times.

### `<Initial>` / `<Async.Initial>`
### `<IfInitial>` / `<Async.Initial>`

Renders only while the deferred promise is still waiting to be run, or you have not provided any promise.

@@ -622,9 +638,9 @@ Renders only while the deferred promise is still waiting to be run, or you have
```jsx
const state = useAsync(...)
return (
<Initial state={state}>
<IfInitial state={state}>
<p>This text is only rendered while `run` has not yet been invoked on `deferFn`.</p>
</Initial>
</IfInitial>
)
```

@@ -650,7 +666,7 @@ return (
</Async.Initial>
```

### `<Pending>` / `<Async.Pending>`
### `<IfPending>` / `<Async.Pending>`

This component renders only while the promise is pending (loading / unsettled).

@@ -667,9 +683,9 @@ Alias: `<Async.Loading>`
```jsx
const state = useAsync(...)
return (
<Pending state={state}>
<IfPending state={state}>
<p>This text is only rendered while performing the initial load.</p>
</Pending>
</IfPending>
)
```

@@ -683,7 +699,7 @@ return (
<Async.Pending>{({ startedAt }) => `Loading since ${startedAt.toISOString()}`}</Async.Pending>
```

### `<Fulfilled>` / `<Async.Fulfilled>`
### `<IfFulfilled>` / `<Async.Fulfilled>`

This component renders only when the promise is fulfilled (resolved to a value, could be `undefined`).

@@ -700,9 +716,9 @@ Alias: `<Async.Resolved>`
```jsx
const state = useAsync(...)
return (
<Fulfilled state={state}>
<IfFulfilled state={state}>
{data => <pre>{JSON.stringify(data)}</pre>}
</Fulfilled>
</IfFulfilled>
)
```

@@ -716,7 +732,7 @@ return (
</Async.Fulfilled>
```

### `<Rejected>` / `<Async.Rejected>`
### `<IfRejected>` / `<Async.Rejected>`

This component renders only when the promise is rejected.

@@ -730,7 +746,7 @@ This component renders only when the promise is rejected.

```jsx
const state = useAsync(...)
return <Rejected state={state}>Oops.</Rejected>
return <IfRejected state={state}>Oops.</IfRejected>
```

```jsx
@@ -741,7 +757,7 @@ return <Rejected state={state}>Oops.</Rejected>
<Async.Rejected>{error => `Unexpected error: ${error.message}`}</Async.Rejected>
```

### `<Settled>` / `<Async.Settled>`
### `<IfSettled>` / `<Async.Settled>`

This component renders only when the promise is fulfilled or rejected.

@@ -755,7 +771,7 @@ This component renders only when the promise is fulfilled or rejected.

```jsx
const state = useAsync(...)
return <Settled state={state}>{state => `Finished at ${state.finishedAt.toISOString()}`</Settled>
return <IfSettled state={state}>{state => `Finished at ${state.finishedAt.toISOString()}`</IfSettled>
```

## Usage examples
@@ -0,0 +1,33 @@
# React Async codemods

These codemods enable you to automatically upgrade your codebase to handle breaking changes in
React Async's API.

## Warning

Be aware: **codemods transform your source code in place**. Make sure that your files are in
version control before running a codemod.

These codemods come without warranty. They will work fine most of the time, but you should always
verify their output. Also, **do not run a codemod more than once.**

## Running a codemod

These codemods are based on [jscodeshift](https://github.com/facebook/jscodeshift). Refer to their
docs for specifics.

```bash
npx jscodeshift <target_dir> -t <transform_script>
```

Where `<target_dir>` should be replaced with the path to your project's source directory and
`<transform_script>` should be replaced by the URL of the codemod.

For example:

```bash
npx jscodeshift . -t https://raw.githubusercontent.com/ghengeveld/react-async/master/codemods/v6.js
```

This will apply the codemod for [v6](https://github.com/ghengeveld/react-async/blob/master/codemods/v6.js)
to the current working directory (`.`).
@@ -0,0 +1,54 @@
/**
* This renames:
* - <Async.Pending> to <Async.Initial>
* - <Async.Loading> to <Async.Pending>
* - <Async.Resolved> to <Async.Fulfilled>
*
* This includes any custom instances created with createInstance().
*/

module.exports = function transform({ path, source }, api) {
if (path.includes("node_modules/")) return

const j = api.jscodeshift
const root = j(source)

const renameJsxMembers = parentName => {
root
.find(j.JSXMemberExpression, { object: { name: parentName }, property: { name: "Pending" } })
.forEach(node => (node.value.property.name = "Initial"))
root
.find(j.JSXMemberExpression, { object: { name: parentName }, property: { name: "Loading" } })
.forEach(node => (node.value.property.name = "Pending"))
root
.find(j.JSXMemberExpression, { object: { name: parentName }, property: { name: "Resolved" } })
.forEach(node => (node.value.property.name = "Fulfilled"))
}

// Rename instances using default import
root
.find(j.ImportDeclaration, { source: { value: "react-async" } })
.find(j.ImportDefaultSpecifier)
.forEach(node => renameJsxMembers(node.value.local.name))

// Rename instances using named `Async` import
root
.find(j.ImportDeclaration, { source: { value: "react-async" } })
.find(j.ImportSpecifier, { imported: { name: "Async" } })
.forEach(node => renameJsxMembers(node.value.local.name))

// Rename instances created with `createInstance`
root
.find(j.ImportDeclaration, { source: { value: "react-async" } })
.find(j.ImportSpecifier, { imported: { name: "createInstance" } })
.forEach(node => {
const createInstance = node.value.local.name
root
.find(j.VariableDeclarator)
.filter(node => node.value.init.type === "CallExpression")
.filter(node => node.value.init.callee.name === createInstance)
.forEach(node => renameJsxMembers(node.value.id.name))
})

return root.toSource()
}
@@ -0,0 +1,33 @@
/**
* This renames the standalone helper components:
* - <Initial> to <IfInitial>
* - <Pending> to <IfPending>
* - <Fulfilled> to <IfFulfilled>
* - <Rejected> to <IfRejected>
* - <Settled> to <IfSettled>
*/

const helperNames = ["Initial", "Pending", "Fulfilled", "Rejected", "Settled"]

module.exports = function transform({ path, source }, api) {
if (path.includes("node_modules/")) return

const j = api.jscodeshift
const root = j(source)

// Rename imports
root
.find(j.ImportDeclaration, { source: { value: "react-async" } })
.find(j.ImportSpecifier)
.filter(node => helperNames.includes(node.value.imported.name))
.forEach(node => (node.value.imported.name = `If${node.value.imported.name}`))

// Rename JSX elements
root
.find(j.JSXIdentifier)
.filter(node => helperNames.includes(node.value.name))
.filter(node => node.parentPath.value.type !== "JSXMemberExpression")
.forEach(node => (node.value.name = `If${node.value.name}`))

return root.toSource()
}
@@ -1,6 +1,6 @@
{
"name": "basic-fetch-example",
"version": "1.0.2",
"version": "8.0.0-alpha.0",
"private": true,
"homepage": "https://react-async.ghengeveld.now.sh/examples/basic-fetch",
"scripts": {
@@ -15,8 +15,8 @@
},
"dependencies": {
"react": "^16.8.6",
"react-async": "^7.0.6",
"react-async-devtools": "^1.0.4",
"react-async": "^8.0.0-alpha.0",
"react-async-devtools": "^8.0.0-alpha.0",
"react-dom": "^16.8.6",
"react-scripts": "^3.0.1"
},
@@ -1,6 +1,6 @@
{
"name": "basic-hook-example",
"version": "1.0.3",
"version": "8.0.0-alpha.0",
"private": true,
"homepage": "https://react-async.ghengeveld.now.sh/examples/basic-hook",
"scripts": {
@@ -15,8 +15,8 @@
},
"dependencies": {
"react": "^16.8.6",
"react-async": "^7.0.6",
"react-async-devtools": "^1.0.4",
"react-async": "^8.0.0-alpha.0",
"react-async-devtools": "^8.0.0-alpha.0",
"react-dom": "^16.8.6",
"react-scripts": "^3.0.1"
},
@@ -1,5 +1,5 @@
import React from "react"
import { useAsync } from "react-async"
import { useAsync, IfPending, IfFulfilled, IfRejected } from "react-async"
import ReactDOM from "react-dom"
import DevTools from "react-async-devtools"
import "./index.css"
@@ -27,15 +27,16 @@ const UserDetails = ({ data }) => (
)

const User = ({ userId }) => {
const { data, error, isPending } = useAsync({
promiseFn: loadUser,
debugLabel: `User ${userId}`,
userId,
})
if (isPending) return <UserPlaceholder />
if (error) return <p>{error.message}</p>
if (data) return <UserDetails data={data} />
return null
const state = useAsync({ promiseFn: loadUser, debugLabel: `User ${userId}`, userId })
return (
<>
<IfPending state={state}>
<UserPlaceholder />
</IfPending>
<IfFulfilled state={state}>{data => <UserDetails data={data} />}</IfFulfilled>
<IfRejected state={state}>{error => <p>{error.message}</p>}</IfRejected>
</>
)
}

export const App = () => (
@@ -1,6 +1,6 @@
{
"name": "custom-instance-example",
"version": "1.0.2",
"version": "8.0.0-alpha.0",
"private": true,
"homepage": "https://react-async.ghengeveld.now.sh/examples/custom-instance",
"scripts": {
@@ -15,8 +15,8 @@
},
"dependencies": {
"react": "^16.8.6",
"react-async": "^7.0.6",
"react-async-devtools": "^1.0.4",
"react-async": "^8.0.0-alpha.0",
"react-async-devtools": "^8.0.0-alpha.0",
"react-dom": "^16.8.6",
"react-scripts": "^3.0.1"
},

0 comments on commit 5bed28d

Please sign in to comment.
You can’t perform that action at this time.