Skip to content

Commit 5bed28d

Browse files
authored
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.
1 parent 839ff17 commit 5bed28d

File tree

22 files changed

+365
-131
lines changed

22 files changed

+365
-131
lines changed

README.md

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ error states, without assumptions about the shape of your data or the type of re
5050

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

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

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

125+
#### Upgrade to v8
126+
127+
All standalone helper components were renamed to avoid import naming collision.
128+
129+
- `<Initial>` was renamed to `<IfInitial>`.
130+
- `<Pending>` was renamed to `<IfPending>`.
131+
- `<Fulfilled>` was renamed to `<IfFulfilled>`.
132+
- `<Rejected>` was renamed to `<IfRejected`.
133+
- `<Settled>` was renamed to `<IfSettled>`.
134+
135+
> A [codemod](https://github.com/ghengeveld/react-async/tree/master/codemods) is available to automate the upgrade.
136+
124137
#### Upgrade to v6
125138

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

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

131147
- `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
267283
rendering of its children based on the current state.
268284

269285
```jsx
270-
import { useAsync, Pending, Fulfilled, Rejected } from "react-async"
286+
import { useAsync, IfPending, IfFulfilled, IfRejected } from "react-async"
271287

272288
const loadCustomer = async ({ customerId }, { signal }) => {
273289
// ...
@@ -277,16 +293,16 @@ const MyComponent = () => {
277293
const state = useAsync({ promiseFn: loadCustomer, customerId: 1 })
278294
return (
279295
<>
280-
<Pending state={state}>Loading...</Pending>
281-
<Rejected state={state}>{error => `Something went wrong: ${error.message}`}</Rejected>
282-
<Fulfilled state={state}>
296+
<IfPending state={state}>Loading...</IfPending>
297+
<IfRejected state={state}>{error => `Something went wrong: ${error.message}`}</IfRejected>
298+
<IfFulfilled state={state}>
283299
{data => (
284300
<div>
285301
<strong>Loaded some data:</strong>
286302
<pre>{JSON.stringify(data, null, 2)}</pre>
287303
</div>
288304
)}
289-
</Fulfilled>
305+
</IfFulfilled>
290306
</>
291307
)
292308
}
@@ -607,7 +623,7 @@ invoked after the state update is completed. Returns the error to enable chainin
607623
React Async provides several helper components that make your JSX more declarative and less cluttered.
608624
They don't have to be direct children of `<Async>` and you can use the same component several times.
609625

610-
### `<Initial>` / `<Async.Initial>`
626+
### `<IfInitial>` / `<Async.Initial>`
611627

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

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

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

653-
### `<Pending>` / `<Async.Pending>`
669+
### `<IfPending>` / `<Async.Pending>`
654670

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

@@ -667,9 +683,9 @@ Alias: `<Async.Loading>`
667683
```jsx
668684
const state = useAsync(...)
669685
return (
670-
<Pending state={state}>
686+
<IfPending state={state}>
671687
<p>This text is only rendered while performing the initial load.</p>
672-
</Pending>
688+
</IfPending>
673689
)
674690
```
675691

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

686-
### `<Fulfilled>` / `<Async.Fulfilled>`
702+
### `<IfFulfilled>` / `<Async.Fulfilled>`
687703

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

@@ -700,9 +716,9 @@ Alias: `<Async.Resolved>`
700716
```jsx
701717
const state = useAsync(...)
702718
return (
703-
<Fulfilled state={state}>
719+
<IfFulfilled state={state}>
704720
{data => <pre>{JSON.stringify(data)}</pre>}
705-
</Fulfilled>
721+
</IfFulfilled>
706722
)
707723
```
708724

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

719-
### `<Rejected>` / `<Async.Rejected>`
735+
### `<IfRejected>` / `<Async.Rejected>`
720736

721737
This component renders only when the promise is rejected.
722738

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

731747
```jsx
732748
const state = useAsync(...)
733-
return <Rejected state={state}>Oops.</Rejected>
749+
return <IfRejected state={state}>Oops.</IfRejected>
734750
```
735751

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

744-
### `<Settled>` / `<Async.Settled>`
760+
### `<IfSettled>` / `<Async.Settled>`
745761

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

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

756772
```jsx
757773
const state = useAsync(...)
758-
return <Settled state={state}>{state => `Finished at ${state.finishedAt.toISOString()}`</Settled>
774+
return <IfSettled state={state}>{state => `Finished at ${state.finishedAt.toISOString()}`</IfSettled>
759775
```
760776
761777
## Usage examples

codemods/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# React Async codemods
2+
3+
These codemods enable you to automatically upgrade your codebase to handle breaking changes in
4+
React Async's API.
5+
6+
## Warning
7+
8+
Be aware: **codemods transform your source code in place**. Make sure that your files are in
9+
version control before running a codemod.
10+
11+
These codemods come without warranty. They will work fine most of the time, but you should always
12+
verify their output. Also, **do not run a codemod more than once.**
13+
14+
## Running a codemod
15+
16+
These codemods are based on [jscodeshift](https://github.com/facebook/jscodeshift). Refer to their
17+
docs for specifics.
18+
19+
```bash
20+
npx jscodeshift <target_dir> -t <transform_script>
21+
```
22+
23+
Where `<target_dir>` should be replaced with the path to your project's source directory and
24+
`<transform_script>` should be replaced by the URL of the codemod.
25+
26+
For example:
27+
28+
```bash
29+
npx jscodeshift . -t https://raw.githubusercontent.com/ghengeveld/react-async/master/codemods/v6.js
30+
```
31+
32+
This will apply the codemod for [v6](https://github.com/ghengeveld/react-async/blob/master/codemods/v6.js)
33+
to the current working directory (`.`).

codemods/v6.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* This renames:
3+
* - <Async.Pending> to <Async.Initial>
4+
* - <Async.Loading> to <Async.Pending>
5+
* - <Async.Resolved> to <Async.Fulfilled>
6+
*
7+
* This includes any custom instances created with createInstance().
8+
*/
9+
10+
module.exports = function transform({ path, source }, api) {
11+
if (path.includes("node_modules/")) return
12+
13+
const j = api.jscodeshift
14+
const root = j(source)
15+
16+
const renameJsxMembers = parentName => {
17+
root
18+
.find(j.JSXMemberExpression, { object: { name: parentName }, property: { name: "Pending" } })
19+
.forEach(node => (node.value.property.name = "Initial"))
20+
root
21+
.find(j.JSXMemberExpression, { object: { name: parentName }, property: { name: "Loading" } })
22+
.forEach(node => (node.value.property.name = "Pending"))
23+
root
24+
.find(j.JSXMemberExpression, { object: { name: parentName }, property: { name: "Resolved" } })
25+
.forEach(node => (node.value.property.name = "Fulfilled"))
26+
}
27+
28+
// Rename instances using default import
29+
root
30+
.find(j.ImportDeclaration, { source: { value: "react-async" } })
31+
.find(j.ImportDefaultSpecifier)
32+
.forEach(node => renameJsxMembers(node.value.local.name))
33+
34+
// Rename instances using named `Async` import
35+
root
36+
.find(j.ImportDeclaration, { source: { value: "react-async" } })
37+
.find(j.ImportSpecifier, { imported: { name: "Async" } })
38+
.forEach(node => renameJsxMembers(node.value.local.name))
39+
40+
// Rename instances created with `createInstance`
41+
root
42+
.find(j.ImportDeclaration, { source: { value: "react-async" } })
43+
.find(j.ImportSpecifier, { imported: { name: "createInstance" } })
44+
.forEach(node => {
45+
const createInstance = node.value.local.name
46+
root
47+
.find(j.VariableDeclarator)
48+
.filter(node => node.value.init.type === "CallExpression")
49+
.filter(node => node.value.init.callee.name === createInstance)
50+
.forEach(node => renameJsxMembers(node.value.id.name))
51+
})
52+
53+
return root.toSource()
54+
}

codemods/v8.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* This renames the standalone helper components:
3+
* - <Initial> to <IfInitial>
4+
* - <Pending> to <IfPending>
5+
* - <Fulfilled> to <IfFulfilled>
6+
* - <Rejected> to <IfRejected>
7+
* - <Settled> to <IfSettled>
8+
*/
9+
10+
const helperNames = ["Initial", "Pending", "Fulfilled", "Rejected", "Settled"]
11+
12+
module.exports = function transform({ path, source }, api) {
13+
if (path.includes("node_modules/")) return
14+
15+
const j = api.jscodeshift
16+
const root = j(source)
17+
18+
// Rename imports
19+
root
20+
.find(j.ImportDeclaration, { source: { value: "react-async" } })
21+
.find(j.ImportSpecifier)
22+
.filter(node => helperNames.includes(node.value.imported.name))
23+
.forEach(node => (node.value.imported.name = `If${node.value.imported.name}`))
24+
25+
// Rename JSX elements
26+
root
27+
.find(j.JSXIdentifier)
28+
.filter(node => helperNames.includes(node.value.name))
29+
.filter(node => node.parentPath.value.type !== "JSXMemberExpression")
30+
.forEach(node => (node.value.name = `If${node.value.name}`))
31+
32+
return root.toSource()
33+
}

examples/basic-fetch/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "basic-fetch-example",
3-
"version": "1.0.2",
3+
"version": "8.0.0-alpha.0",
44
"private": true,
55
"homepage": "https://react-async.ghengeveld.now.sh/examples/basic-fetch",
66
"scripts": {
@@ -15,8 +15,8 @@
1515
},
1616
"dependencies": {
1717
"react": "^16.8.6",
18-
"react-async": "^7.0.6",
19-
"react-async-devtools": "^1.0.4",
18+
"react-async": "^8.0.0-alpha.0",
19+
"react-async-devtools": "^8.0.0-alpha.0",
2020
"react-dom": "^16.8.6",
2121
"react-scripts": "^3.0.1"
2222
},

examples/basic-hook/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "basic-hook-example",
3-
"version": "1.0.3",
3+
"version": "8.0.0-alpha.0",
44
"private": true,
55
"homepage": "https://react-async.ghengeveld.now.sh/examples/basic-hook",
66
"scripts": {
@@ -15,8 +15,8 @@
1515
},
1616
"dependencies": {
1717
"react": "^16.8.6",
18-
"react-async": "^7.0.6",
19-
"react-async-devtools": "^1.0.4",
18+
"react-async": "^8.0.0-alpha.0",
19+
"react-async-devtools": "^8.0.0-alpha.0",
2020
"react-dom": "^16.8.6",
2121
"react-scripts": "^3.0.1"
2222
},

examples/basic-hook/src/index.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react"
2-
import { useAsync } from "react-async"
2+
import { useAsync, IfPending, IfFulfilled, IfRejected } from "react-async"
33
import ReactDOM from "react-dom"
44
import DevTools from "react-async-devtools"
55
import "./index.css"
@@ -27,15 +27,16 @@ const UserDetails = ({ data }) => (
2727
)
2828

2929
const User = ({ userId }) => {
30-
const { data, error, isPending } = useAsync({
31-
promiseFn: loadUser,
32-
debugLabel: `User ${userId}`,
33-
userId,
34-
})
35-
if (isPending) return <UserPlaceholder />
36-
if (error) return <p>{error.message}</p>
37-
if (data) return <UserDetails data={data} />
38-
return null
30+
const state = useAsync({ promiseFn: loadUser, debugLabel: `User ${userId}`, userId })
31+
return (
32+
<>
33+
<IfPending state={state}>
34+
<UserPlaceholder />
35+
</IfPending>
36+
<IfFulfilled state={state}>{data => <UserDetails data={data} />}</IfFulfilled>
37+
<IfRejected state={state}>{error => <p>{error.message}</p>}</IfRejected>
38+
</>
39+
)
3940
}
4041

4142
export const App = () => (

examples/custom-instance/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "custom-instance-example",
3-
"version": "1.0.2",
3+
"version": "8.0.0-alpha.0",
44
"private": true,
55
"homepage": "https://react-async.ghengeveld.now.sh/examples/custom-instance",
66
"scripts": {
@@ -15,8 +15,8 @@
1515
},
1616
"dependencies": {
1717
"react": "^16.8.6",
18-
"react-async": "^7.0.6",
19-
"react-async-devtools": "^1.0.4",
18+
"react-async": "^8.0.0-alpha.0",
19+
"react-async-devtools": "^8.0.0-alpha.0",
2020
"react-dom": "^16.8.6",
2121
"react-scripts": "^3.0.1"
2222
},

0 commit comments

Comments
 (0)