Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 9 commits into from Aug 9, 2019
56 changes: 36 additions & 20 deletions README.md
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }) => {
// ...
Expand All @@ -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>
</>
)
}
Expand Down Expand Up @@ -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.

Expand All @@ -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>
)
```

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

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

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

Expand All @@ -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>
)
```

Expand All @@ -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`).

Expand All @@ -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>
)
```

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

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

This component renders only when the promise is rejected.

Expand All @@ -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
Expand All @@ -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.

Expand All @@ -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
Expand Down
33 changes: 33 additions & 0 deletions codemods/README.md
@@ -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 (`.`).
54 changes: 54 additions & 0 deletions codemods/v6.js
@@ -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()
}
33 changes: 33 additions & 0 deletions codemods/v8.js
@@ -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()
}
6 changes: 3 additions & 3 deletions examples/basic-fetch/package.json
@@ -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": {
Expand All @@ -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"
},
Expand Down
6 changes: 3 additions & 3 deletions examples/basic-hook/package.json
@@ -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": {
Expand All @@ -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"
},
Expand Down
21 changes: 11 additions & 10 deletions examples/basic-hook/src/index.js
@@ -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"
Expand Down Expand Up @@ -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 = () => (
Expand Down
6 changes: 3 additions & 3 deletions examples/custom-instance/package.json
@@ -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": {
Expand All @@ -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"
},
Expand Down