This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
+
+## Similar projects
+
+- [@slorber/react-async-hook](https://github.com/slorber/react-async-hook)
+- [@dai-shi/react-hooks-async](https://github.com/dai-shi/react-hooks-async)
+- [@cristovao-trevisan/async-resource](https://github.com/cristovao-trevisan/async-resource)
+- [@ilyalesik/react-fetch-hook](https://github.com/ilyalesik/react-fetch-hook)
+- [@marcin-piela/react-fetching-library](https://github.com/marcin-piela/react-fetching-library)
diff --git a/babel.config.js b/babel.config.js
index 49a48f94..4e9750dd 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,11 +1,11 @@
module.exports = {
presets: ["@babel/preset-react"],
- plugins: ["@babel/plugin-proposal-object-rest-spread"],
+ plugins: ["@babel/plugin-proposal-object-rest-spread", "@babel/plugin-proposal-class-properties"],
env: {
test: {
- presets: ["@babel/preset-env", "@babel/preset-react"],
- plugins: ["@babel/plugin-transform-runtime"],
+ presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"],
+ plugins: ["@babel/plugin-transform-runtime", "@babel/plugin-proposal-class-properties"],
},
},
}
diff --git a/docs/_summary.md b/docs/_summary.md
index 08efbe34..199c2ae4 100644
--- a/docs/_summary.md
+++ b/docs/_summary.md
@@ -4,14 +4,30 @@
## Getting started
-- [Installation](installation.md)
-- [Upgrading](upgrading.md)
-- [Usage](usage.md)
-- [DevTools](devtools.md)
+- [Installation](getting-started/installation.md)
+- [Upgrading](getting-started/upgrading.md)
+- [Usage](getting-started/usage.md)
+- [DevTools](getting-started/devtools.md)
## API
-- [Interfaces](interfaces.md)
-- [Configuration options](options.md)
-- [State properties](state.md)
-- [Helper components](helpers.md)
+- [Interfaces](api/interfaces.md)
+- [Configuration options](api/options.md)
+- [State properties](api/state.md)
+- [Helper components](api/helpers.md)
+
+## Guide
+
+- [Async components](guide/async-components.md)
+- [Separating view and logic](guide/separating-view-logic.md)
+- [Async actions](guide/async-actions.md)
+- [Optimistic updates](guide/optimistic-updates.md)
+- [Server-side rendering](guide/server-side-rendering.md)
+
+## Contributing
+
+- [Introduction](contributing/introduction.md)
+- [Setting up](contributing/setting-up.md)
+- [Development](contributing/development.md)
+- [Testing](contributing/testing.md)
+- [Releasing](contributing/releasing.md)
diff --git a/docs/helpers.md b/docs/api/helpers.md
similarity index 100%
rename from docs/helpers.md
rename to docs/api/helpers.md
diff --git a/docs/interfaces.md b/docs/api/interfaces.md
similarity index 95%
rename from docs/interfaces.md
rename to docs/api/interfaces.md
index dd6601f5..5ae00ffe 100644
--- a/docs/interfaces.md
+++ b/docs/api/interfaces.md
@@ -2,7 +2,8 @@
React Async provides several ways to use it. The classic interface is through the `` component, which is
backwards compatible to React v16.3. More recent React applications will be using hooks, of which two are provided:
-`useAsync` and `useFetch`. Functionally, `` and `useAsync` are equivalent. `useFetch` is a special type of `useAsync` which is tied to the native `fetch` API.
+`useAsync` and `useFetch`. Functionally, `` and `useAsync` are equivalent. `useFetch` is a special version of
+`useAsync` which is tied to the native `fetch` API.
React Async accepts a wide range of [configuration options](options.md) and returns a set of [state props](state.md).
The way you use these differs slightly between the `useAsync` and `useFetch` hooks, and the `` component.
diff --git a/docs/options.md b/docs/api/options.md
similarity index 100%
rename from docs/options.md
rename to docs/api/options.md
diff --git a/docs/state.md b/docs/api/state.md
similarity index 100%
rename from docs/state.md
rename to docs/api/state.md
diff --git a/docs/contributing/development.md b/docs/contributing/development.md
new file mode 100644
index 00000000..8e2da052
--- /dev/null
+++ b/docs/contributing/development.md
@@ -0,0 +1,45 @@
+# Development
+
+React Async is a library without visual parts. Only the DevTools have a user interface you can spin up in a browser.
+Therefore the development workflow for the core library might be different from what you're used to. Generally, we use a
+TDD approach:
+
+- Write a unit test for the new feature or bug you want to fix. Sometimes you can just extend an existing test.
+- Fix the test by implementing the feature or bugfix. Now all tests should pass.
+- Optionally refactor the code for performance, readability and style. Probably this will come up during PR review.
+
+We use the GitHub pull request workflow. In practice this means your workflow looks like this:
+
+- Fork the repo (or pull the latest upstream) under your own account.
+- Make your changes, commit and push them. We don't enforce any commit message format.
+- Open a pull request on the main repository against the `next` branch. Make sure to follow the template.
+- We'll review your PR and will probably ask for some changes.
+- Once ready, we'll merge your PR.
+- Your changes will be in the next release.
+
+## Working with Storybook
+
+We use Storybook as a development environment for the DevTools. Spin it up using:
+
+```sh
+yarn start:storybook
+```
+
+This should open up Storybook in a browser at http://localhost:6006/
+Run it side-by-side with `yarn test --watch` during development. See [Testing](#testing).
+
+## Working with the examples
+
+In the `examples` folder, you will find sample React applications that use React Async in various ways with various other libraries. Please add a new example when introducing a major new feature. Make sure to add it to `now.json` so it is automatically deployed when merged to `master`.
+
+To run sample examples on your local environments
+
+```sh
+yarn build:examples
+yarn test:examples
+yarn start:examples
+```
+
+## Resolving issues
+
+Sometimes your dependencies might end up in a weird state, causing random issues, especially when working with the examples. In this case it often helps to run `yarn clean -y && yarn bootstrap`. This will delete `node_modules` from all packages/examples and do a clean install.
diff --git a/docs/contributing/introduction.md b/docs/contributing/introduction.md
new file mode 100644
index 00000000..d3303808
--- /dev/null
+++ b/docs/contributing/introduction.md
@@ -0,0 +1,92 @@
+# Contributing to React Async
+
+Thanks for your interest in improving React Async! Contributions of any kind are welcome. Please refer to this guide before opening an issue or pull request.
+
+This repo relies on Yarn workspaces, so you should [install](https://yarnpkg.com/en/docs/install) and use `yarn@1.3.2` or higher as the package manager for this project.
+
+## Development guide
+
+Please have the **_latest_** stable versions of the following on your machine
+
+- node
+- yarn
+
+### Initial setup
+
+To start working on React Async, clone the repo and bootstrap the project:
+
+```sh
+git clone https://github.com/async-library/react-async.git
+cd react-async
+yarn && yarn bootstrap && yarn test
+```
+
+Note that all work is done against the `next` branch, we only merge to `master` when doing a release.
+
+### Working with Storybook
+
+We use Storybook as a development environment, particularly for the DevTools. Spin it up using:
+
+```sh
+yarn start:storybook
+```
+
+This should open up Storybook in a browser at http://localhost:6006/
+Run it side-by-side with `yarn test --watch` during development. See [Testing](#testing).
+
+### Linting
+
+Use `yarn lint` to verify your code style before committing. It's highly recommended to install the Prettier and ESLint plugins for your IDE. Travis CI will fail your build on lint errors. Configure VS Code with the following settings:
+
+```plaintext
+"eslint.autoFixOnSave": true,
+"eslint.packageManager": "yarn",
+"eslint.options": {
+ "cache": true,
+ "cacheLocation": ".cache/eslint",
+ "extensions": [".js", ".jsx", ".mjs", ".json", ".ts", ".tsx"]
+},
+"eslint.validate": [
+ "javascript",
+ "javascriptreact",
+ {"language": "typescript", "autoFix": true },
+ {"language": "typescriptreact", "autoFix": true }
+],
+"eslint.alwaysShowStatus": true
+```
+
+This should enable auto-fix for all source files, and give linting warnings and errors within your editor.
+
+### Testing
+
+Use the following command to test all packages in watch mode. Refer to the [Jest CLI options](https://jestjs.io/docs/en/cli#options) for details.
+
+```sh
+yarn test:watch
+```
+
+In general, this is sufficient during development. Travis CI will apply a more rigorous set of tests.
+
+#### Testing for compatibility
+
+```sh
+yarn test:compat
+```
+
+This runs all tests using various versions of `react` and `react-dom`, to check for compatibility with older/newer versions of React. This is what CircleCI and Travis run.
+
+### Working with the examples
+
+In the `examples` folder, you will find sample React applications that use React Async in various ways with various other libraries. Please add a new example when introducing a major new feature. Make sure to add it to `now.json` so it is automatically deployed when merged to `master`.
+
+To run sample examples on your local environments
+
+```sh
+yarn build:examples
+yarn test:examples
+yarn start:examples
+```
+
+### Resolving issues
+
+Sometimes your dependencies might end up in a weird state, causing random issues, especially when working with the examples. In this case it often helps to run `yarn clean -y && yarn bootstrap`. This will delete `node_modules` from all packages/examples and do a clean install.
diff --git a/docs/contributing/releasing.md b/docs/contributing/releasing.md
new file mode 100644
index 00000000..d5b325ae
--- /dev/null
+++ b/docs/contributing/releasing.md
@@ -0,0 +1,26 @@
+# Releasing
+
+All ongoing development is done on the `next` branch. When preparing for a release, we'll create a `release` branch
+which will eventually be merged into `master`. This way, what's on `master` is always what's published on `npm`.
+
+Release management is currently a manual process, to be performed by core team members only. Here's the process:
+
+1. Create a `release` branch, usually based on `next`.
+2. Open a pull request for `release` -> `master`
+3. Write the release notes in the PR description.
+4. Decide on the version number, taking care to follow semver. Do a pre-release before doing the actual release.
+5. Run `yarn bump` to increment the version number and commit it as "Release vX.X.X" (using the correct version number).
+6. Tag the release commit with `git tag vX.X.X` (using the correct version number).
+7. Push the release commit AND tag: `git push --follow-tags`
+8. Publish each package (in `./packages`) to npm using the script below.
+9. Create a new release on GitHub and copy the release notes there.
+
+```
+yarn build:packages
+cd packages/react-async
+npm publish pkg
+cd ../react-async-devtools
+npm publish pkg
+```
+
+Take care to publish the `pkg` directory!
diff --git a/docs/contributing/setting-up.md b/docs/contributing/setting-up.md
new file mode 100644
index 00000000..196c0cba
--- /dev/null
+++ b/docs/contributing/setting-up.md
@@ -0,0 +1,61 @@
+# Setting up your development environment
+
+## Prerequisites
+
+In order to develop React Async on your local machine, you'll need `git`, `node` and `yarn`.
+
+### Git
+
+To clone the repository, commit your changes and push them upstream, you'll need to have `git` [installed][install git].
+
+[install git]: https://www.atlassian.com/git/tutorials/install-git
+
+### Node.js
+
+As a JavaScript project, we rely heavily on Node.js. It's recommended to use a version manager such as [fnm] for Mac /
+Linux or [nvm-windows] for Windows to install the latest Node.js with.
+
+[fnm]: https://github.com/Schniz/fnm
+[nvm-windows]: https://github.com/coreybutler/nvm-windows
+
+### Yarn
+
+This repo relies on Yarn workspaces, so you should [install][install yarn] and use `yarn@1.3.2` or higher as the package
+manager for this project.
+
+[install yarn]: https://yarnpkg.com/en/docs/install
+
+## Project setup
+
+To start working on React Async, clone the repository and bootstrap the project by running the following commands
+one-by-one:
+
+```sh
+git clone https://github.com/async-library/react-async.git
+cd react-async
+yarn install
+yarn bootstrap
+yarn test
+```
+
+This should install all dependencies, build and link the react-async and react-async-devtools packages to the examples,
+and finally run the unit tests. In the end it should succeed with a message (numbers may change):
+
+```
+Test Suites: 6 passed, 6 total
+Tests: 136 passed, 136 total
+```
+
+> Note that all work is done against the `next` branch, we only merge to `master` when doing a release.
+
+## Editor setup
+
+We recommend using [Visual Studio Code](https://code.visualstudio.com/) with the following extensions:
+
+- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
+- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
+- [DeepScan](https://marketplace.visualstudio.com/items?itemName=DeepScan.vscode-deepscan)
+- [Oceanic Plus](https://marketplace.visualstudio.com/items?itemName=marcoms.oceanic-plus)
+
+Make sure to enable `editor.formatOnSave`, so Prettier will automatically apply the right code style. For the full
+immersive experience you can also install and use the [Overpass Mono](https://overpassfont.org/) font.
diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md
new file mode 100644
index 00000000..67f6c834
--- /dev/null
+++ b/docs/contributing/testing.md
@@ -0,0 +1,36 @@
+# Testing
+
+Use the following command to test all packages in watch mode. Refer to the [Jest CLI options][jest options] for details.
+
+[jest options]: https://jestjs.io/docs/en/cli#options
+
+```sh
+yarn test:watch
+```
+
+In general, this is sufficient during development. CircleCI and Travis will eventually apply a more rigorous set of
+tests against your pull request, including the ones below.
+
+## Testing the examples
+
+Because React Async is only a piece in a bigger puzzle, testing for integration with other libraries is very important.
+You can run the tests for all examples against your local changes with the following command:
+
+```sh
+yarn test:examples
+```
+
+If you want to add integration tests for compatibility with another library, please add an example for it.
+
+## Testing for compatibility
+
+```sh
+yarn test:compat
+```
+
+This runs all tests using various versions of `react` and `react-dom`, to check for compatibility with older/newer
+versions of React. This is what CircleCI and Travis run.
+
+## Linting
+
+Use `yarn lint` to verify your code style before committing. It's highly recommended to install the Prettier and ESLint plugins for your IDE. CircleCI and Travis will fail your build on lint errors.
diff --git a/docs/devtools.md b/docs/getting-started/devtools.md
similarity index 82%
rename from docs/devtools.md
rename to docs/getting-started/devtools.md
index 226a97df..af01f89d 100644
--- a/docs/devtools.md
+++ b/docs/getting-started/devtools.md
@@ -1,6 +1,7 @@
# DevTools
-React Async comes with a separate DevTools package which helps you Debug and develop your asynchronous application states. You can install it from npm:
+React Async comes with a separate DevTools package which helps you Debug and develop your asynchronous application
+states. You can install it from npm:
```text
npm install --save react-async-devtools
diff --git a/docs/installation.md b/docs/getting-started/installation.md
similarity index 66%
rename from docs/installation.md
rename to docs/getting-started/installation.md
index eea0d1e0..1f87cdcb 100644
--- a/docs/installation.md
+++ b/docs/getting-started/installation.md
@@ -12,4 +12,5 @@ Or if you're using Yarn:
yarn add react-async
```
-> This package requires `react` as a peer dependency. Please make sure to install that as well. If you want to use the `useAsync` hook, you'll need `react@16.8.0` or later.
+> This package requires `react` as a peer dependency. Please make sure to install that as well. If you want to use the
+> `useAsync` hook, you'll need `react@16.8.0` or later.
diff --git a/docs/upgrading.md b/docs/getting-started/upgrading.md
similarity index 100%
rename from docs/upgrading.md
rename to docs/getting-started/upgrading.md
diff --git a/docs/usage.md b/docs/getting-started/usage.md
similarity index 100%
rename from docs/usage.md
rename to docs/getting-started/usage.md
diff --git a/docs/guide/async-actions.md b/docs/guide/async-actions.md
new file mode 100644
index 00000000..3e96c7a1
--- /dev/null
+++ b/docs/guide/async-actions.md
@@ -0,0 +1,76 @@
+# Async actions
+
+Fetching data for display alone isn't sufficient for most applications. You'll often also want to submit data back to
+the server, or handle other types of asynchronous actions. To enable this, React Async has the concept of a
+[`deferFn`](../api/options.md#deferfn).
+
+Like `promiseFn`, a `deferFn` is a function that returns a Promise. The difference is that `deferFn` will not be
+automatically invoked by React Async when rendering the component. Instead it will have to be triggered by calling the
+[`run`](../api/state.md#run) function provided by React Async.
+
+```jsx
+import React, { useState } from "react"
+import { useAsync } from "react-async"
+
+const subscribe = ([email], props, { signal }) =>
+ fetch("/newsletter", { method: "POST", body: JSON.stringify({ email }), signal })
+
+const NewsletterForm = () => {
+ const { isPending, error, run } = useAsync({ deferFn: subscribe })
+ const [email, setEmail] = useState("")
+
+ const handleSubmit = event => {
+ event.preventDefault()
+ run(email)
+ }
+
+ return (
+
+ )
+}
+```
+
+As you can see, the `deferFn` is invoked with 3 arguments: `args`, `props` and the AbortController. `args` is an array
+representing the arguments that were passed to `run`. In this case we passed the `email`, so we can extract that from
+the `args` array at the first index using [array destructuring] and pass it along to our `fetch` request.
+
+[array destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Array_destructuring
+
+## Sending data with `useFetch`
+
+The above example can be simplified when we rely on [`useFetch`](../api/interfaces.md#usefetch-hook) instead of
+constructing the request manually.
+
+```jsx
+import React, { useState } from "react"
+import { useFetch } from "react-async"
+
+const NewsletterForm = () => {
+ const { isPending, error, run } = useFetch("/newsletter", { method: "POST" })
+ const [email, setEmail] = useState("")
+
+ const handleSubmit = event => {
+ event.preventDefault()
+ run({ body: JSON.stringify({ email }) })
+ }
+
+ return (
+
+ )
+}
+```
+
+The [`run`](../api/state.md#run) function for `useFetch` is a little special because it allows you to override the
+request's resource and other params. This way you can pass in the body, add dynamic headers or override the URL.
diff --git a/docs/guide/async-components.md b/docs/guide/async-components.md
new file mode 100644
index 00000000..e2b129ba
--- /dev/null
+++ b/docs/guide/async-components.md
@@ -0,0 +1,81 @@
+# Async components
+
+The most common use case for React Async is data fetching. In single-page applications it's very common to dynamically
+load some data from a backend. React Async makes it incredibly easy to set this up, without having to worry about the
+details.
+
+The mental model of React Async is component-first. Rather than loading data high up in your application and passing it
+down to a component for display, you perform the data loading at the component level. Such a component is called an
+async component. An async component can render its state in a meaningful way like any other component, or be logic-only.
+In that case it doesn't render any UI but instead passes its state down to its children. Such separation of concerns is
+good practice.
+
+## Creating an async component with `useFetch`
+
+The easiest way to create an async component for data fetching is through the
+[`useFetch` hook](../api/interfaces.md#usefetch-hook):
+
+```jsx
+import React from "react"
+import { useFetch } from "react-async"
+
+const Person = ({ id }) => {
+ const { data, error } = useFetch(`https://swapi.co/api/people/${id}/`, {
+ headers: { accept: "application/json" },
+ })
+ if (error) return error.message
+ if (data) return `Hi, my name is ${data.name}!`
+ return null
+}
+
+const App = () => {
+ return
+}
+```
+
+## More flexibility with `useAsync`
+
+For most data fetching needs, `useFetch` is sufficient. However, sometimes you may want to take full control, for
+example if you want to combine multiple requests. In this case you can use the
+[`useAsync` hook](../api/interfaces.md#useasync-hook).
+
+The core concept of `useAsync` (and React Async in general), is the [`promiseFn`](../api/options.md#promisefn): a
+function that returns a `Promise`. It's the fundamental concept for modelling asynchronous operations. It enables React
+Async to take control over scheduling, the Promise lifecycle and things like (re)starting an operation on user action or
+other changes. We've deliberately chosen the `Promise` as our primitive, because it's natively supported and has various
+utility methods like `Promise.all`. That's also why you'll find our terminology closely follows the Promise [states and
+fates].
+
+The above example, written with `useAsync`, would look like this:
+
+```jsx
+import React from "react"
+import { useAsync } from "react-async"
+
+const fetchPerson = async ({ id }, { signal }) => {
+ const response = await fetch(`https://swapi.co/api/people/${id}/`, { signal })
+ if (!response.ok) throw new Error(response.status)
+ return response.json()
+}
+
+const Person = ({ id }) => {
+ const { data, error } = useAsync({ promiseFn: fetchPerson, id })
+ if (error) return error.message
+ if (data) return `Hi, my name is ${data.name}!`
+ return null
+}
+
+const App = () => {
+ return
+}
+```
+
+Notice the incoming parameters to `fetchPerson`. The `promiseFn` will be invoked with a `props` object and an
+`AbortController`. `props` are the options you passed to `useAsync`, which is why you can access the `id` property
+using [object destructuring]. The `AbortController` is created by React Async to enable [abortable fetch], so the
+underlying request will be aborted when the promise is cancelled (e.g. when a new one starts or we leave the page). We
+have to pass its `AbortSignal` down to `fetch` in order to wire this up.
+
+[states and fates]: https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md
+[object destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring
+[abortable fetch]: https://developers.google.com/web/updates/2017/09/abortable-fetch
diff --git a/docs/guide/optimistic-updates.md b/docs/guide/optimistic-updates.md
new file mode 100644
index 00000000..0a92ba8c
--- /dev/null
+++ b/docs/guide/optimistic-updates.md
@@ -0,0 +1,40 @@
+# Optimistic updates
+
+A powerful pattern to improve your app's perceived performance is optimistic updates. When building an async action, you
+might be able to predict the outcome of the operation. If so, you can implement optimistic updates by proactively
+setting the `data` to the predicted value, when starting the async action. Once the action completes, it will update
+`data` to the actual value, probably the same value as predicted.
+
+The following example uses both `promiseFn` and `deferFn` along with [`setData`](../api/state.md#setdata) to implement
+optimistic updates.
+
+```jsx
+import Async from "react-async"
+
+const getAttendance = () => fetch("/attendance").then(() => true, () => false)
+const updateAttendance = ([attend]) =>
+ fetch("/attendance", { method: attend ? "POST" : "DELETE" }).then(() => attend, () => !attend)
+
+const AttendanceToggle = () => (
+
+ {({ isPending, data: isAttending, run, setData }) => (
+ {
+ setData(!isAttending)
+ run(!isAttending)
+ }}
+ disabled={isPending}
+ />
+ )}
+
+)
+```
+
+Here we have a switch to toggle attentance for an event. Clicking the toggle will most likely succeed, so we can predict
+the value it will have after completion (because we're just flipping a boolean).
+
+Notice that React Async accepts both a `promiseFn` and a `deferFn` at the same time. This allows you to combine data
+fetching with performing actions. A typical example of where this is useful is with forms, where you first want to
+populate the fields with current values from the database, and send the new values back when submitting the form. Do
+note that `promiseFn` and `deferFn` operate on the same `data`, so they should both resolve to a similar kind of value.
diff --git a/docs/guide/separating-view-logic.md b/docs/guide/separating-view-logic.md
new file mode 100644
index 00000000..cb828ac7
--- /dev/null
+++ b/docs/guide/separating-view-logic.md
@@ -0,0 +1,75 @@
+# Separating view and logic
+
+It's generally good practice to separate view components from logic components. Async components should preferably be
+logic-only. That means they don't render anything by themselves. Instead you can use the [render props] pattern to pass
+down the async state:
+
+```jsx
+import React from "react"
+import { useAsync } from "react-async"
+
+const fetchPerson = async ({ id }, { signal }) => {
+ const response = await fetch(`https://swapi.co/api/people/${id}/`, { signal })
+ if (!response.ok) throw new Error(response.statusText)
+ return response.json()
+}
+
+const Person = ({ id }) => {
+ const state = useAsync({ promiseFn: fetchPerson, id })
+ return children(state)
+}
+
+const App = () => {
+ return (
+
+ {({ isPending, data, error }) => {
+ if (isPending) return "Loading..."
+ if (error) return
+ if (data) return
+ return null
+ }}
+
+ )
+}
+```
+
+> `ErrorMessage` and `Greeting` would be separate view components defined elsewhere.
+
+[render props]: https://reactjs.org/docs/render-props.html
+
+## Cleaning up the JSX
+
+You'll notice the render props pattern is very powerful, but can also lead to code that's hard to read and understand.
+To make your JSX more declarative and less cluttered, you can use the [``](../api/interfaces.md#async-component)
+component and its [state helpers](../api/helpers.md). These take away the need for `if/else` statements and `return`
+keywords in your JSX.
+
+```jsx
+import React from "react"
+import Async from "react-async"
+
+const fetchPerson = async ({ id }, { signal }) => {
+ const response = await fetch(`https://swapi.co/api/people/${id}/`, { signal })
+ if (!response.ok) throw new Error(response.statusText)
+ return response.json()
+}
+
+const App = () => {
+ return (
+
+ Loading...
+ {error => }
+ {data => }
+
+ )
+}
+```
+
+You should know that these helper components do not have to be direct children of the `` component. Because they
+are automatically wired up using [Context], they can be placed anywhere down the component tree, so long as they are
+descendants. You can also use helpers of the same type, multiple times.
+
+Stand-alone versions of `` and the like are also available. However, these must be wired up manually by
+passing the `state` prop and are therefore only really useful when combined with one of the async hooks.
+
+[context]: https://reactjs.org/docs/context.html
diff --git a/docs/guide/server-side-rendering.md b/docs/guide/server-side-rendering.md
new file mode 100644
index 00000000..2daf6a82
--- /dev/null
+++ b/docs/guide/server-side-rendering.md
@@ -0,0 +1,33 @@
+# Server-side rendering
+
+There's a good chance you're using React with Server-side rendering (SSR), as many applications require this to be
+successful. If you happen to be using Next.js, it's really easy to integrate React Async. The crux is in setting a
+[`initialValue`](../api/options.md#initialvalue), which is fetched server-side for initial page loads and passed along
+through rehydration.
+
+```jsx
+import fetch from "isomorphic-unfetch"
+
+const fetchPerson = async ({ id }) => {
+ const response = await fetch(`https://swapi.co/api/people/${id}/`)
+ if (!response.ok) throw new Error(response.status)
+ return response.json()
+}
+
+const Person = ({ id, person }) => (
+
+ Loading...
+ {error => }
+ {data => }
+
+)
+
+Person.getInitialProps = async ({ req }) => {
+ const id = req.params.id
+ const person = await fetchPerson({ id })
+ return { id, person }
+}
+```
+
+If React Async is provided an `initialValue`, it will not invoke the `promiseFn` on mount. Instead it will use the
+`initialValue` to immediately set `data` or `error`, and render accordingly.
diff --git a/docs/introduction.md b/docs/introduction.md
index 695a4e37..7b3e2879 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -1,17 +1,32 @@
# Introduction
-React Async is a utility belt for declarative promise resolution and data fetching. It makes it easy to handle asynchronous UI states, without assumptions about the shape of your data or the type of request. React Async consists of a React component and several hooks. You can use it with `fetch`, Axios or other data fetching libraries, even GraphQL.
+React Async is a utility belt for declarative promise resolution and data fetching. It makes it easy to handle
+asynchronous UI states, without assumptions about the shape of your data or the type of request. React Async consists of
+a React component and several hooks. You can use it with `fetch`, Axios or other data fetching libraries, even GraphQL.
## Rationale
-React Async is different in that it tries to resolve data as close as possible to where it will be used, while using declarative syntax, using just JSX and native promises. This is in contrast to systems like Redux where you would configure any data fetching or updates on a higher \(application global\) level, using a special construct \(actions/reducers\).
+React Async is different in that it tries to resolve data as close as possible to where it will be used, while using
+declarative syntax, using just JSX and native promises. This is in contrast to systems like Redux where you would
+configure any data fetching or updates on a higher (application global) level, using a special construct
+(actions/reducers).
-React Async works well even in larger applications with multiple or nested data dependencies. It encourages loading data on-demand and in parallel at component level instead of in bulk at the route/page level. It's entirely decoupled from your routes, so it works well in complex applications that have a dynamic routing model or don't use routes at all.
+React Async works well even in larger applications with multiple or nested data dependencies. It encourages loading data
+on-demand and in parallel at component level instead of in bulk at the route/page level. It's entirely decoupled from
+your routes, so it works well in complex applications that have a dynamic routing model or don't use routes at all.
React Async is promise-based, so you can resolve anything you want, not just `fetch` requests.
## Concurrent React and Suspense
-The React team is currently working on a large rewrite called [Concurrent React](https://github.com/sw-yx/fresh-concurrent-react/blob/master/Intro.md#introduction-what-is-concurrent-react), previously known as "Async React". Part of this rewrite is Suspense, which is a generic way for components to suspend rendering while they load data from a cache. It can render a fallback UI while loading data, much like ``.
+The React team is currently working on a large rewrite called [Concurrent React], previously known as "Async React".
+Part of this rewrite is Suspense, which is a generic way for components to suspend rendering while they load data from a
+cache. It can render a fallback UI while loading data, much like ``.
-React Async has no direct relation to Concurrent React. They are conceptually close, but not the same. React Async is meant to make dealing with asynchronous business logic easier. Concurrent React will make those features have less impact on performance and usability. When Suspense lands, React Async will make full use of Suspense features. In fact, you can already **start using React Async right now**, and in a later update, you'll **get Suspense features for free**. In fact, React Async already has experimental support for Suspense, by passing the `suspense` option.
+React Async has no direct relation to Concurrent React. They are conceptually close, but not the same. React Async is
+meant to make dealing with asynchronous business logic easier. Concurrent React will make those features have less
+impact on performance and usability. When Suspense lands, React Async will make full use of Suspense features. In fact,
+you can already **start using React Async right now**, and in a later update, you'll **get Suspense features for free**.
+In fact, React Async already has experimental support for Suspense, by passing the `suspense` option.
+
+[concurrent react]: https://github.com/sw-yx/fresh-concurrent-react/blob/master/Intro.md#introduction-what-is-concurrent-react
diff --git a/examples/basic-fetch/package.json b/examples/basic-fetch/package.json
index 4eaff85a..36dad833 100644
--- a/examples/basic-fetch/package.json
+++ b/examples/basic-fetch/package.json
@@ -1,6 +1,6 @@
{
"name": "basic-fetch-example",
- "version": "9.0.0",
+ "version": "10.0.0-alpha.0",
"private": true,
"homepage": "https://react-async.async-library.now.sh/examples/basic-fetch",
"scripts": {
@@ -14,14 +14,14 @@
"now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build"
},
"dependencies": {
- "react": "16.10.2",
- "react-async": "^9.0.0",
- "react-async-devtools": "^9.0.0",
- "react-dom": "16.10.2",
+ "react": "16.11.0",
+ "react-async": "^10.0.0-alpha.0",
+ "react-async-devtools": "^10.0.0-alpha.0",
+ "react-dom": "16.11.0",
"react-scripts": "3.2.0"
},
"devDependencies": {
- "relative-deps": "0.1.2"
+ "relative-deps": "0.2.0"
},
"relativeDependencies": {
"react-async": "../../packages/react-async/pkg",
diff --git a/examples/basic-hook/package.json b/examples/basic-hook/package.json
index 02e9ee0b..400111a5 100644
--- a/examples/basic-hook/package.json
+++ b/examples/basic-hook/package.json
@@ -1,6 +1,6 @@
{
"name": "basic-hook-example",
- "version": "9.0.0",
+ "version": "10.0.0-alpha.0",
"private": true,
"homepage": "https://react-async.async-library.now.sh/examples/basic-hook",
"scripts": {
@@ -14,14 +14,14 @@
"now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build"
},
"dependencies": {
- "react": "16.10.2",
- "react-async": "^9.0.0",
- "react-async-devtools": "^9.0.0",
- "react-dom": "16.10.2",
+ "react": "16.11.0",
+ "react-async": "^10.0.0-alpha.0",
+ "react-async-devtools": "^10.0.0-alpha.0",
+ "react-dom": "16.11.0",
"react-scripts": "3.2.0"
},
"devDependencies": {
- "relative-deps": "0.1.2"
+ "relative-deps": "0.2.0"
},
"relativeDependencies": {
"react-async": "../../packages/react-async/pkg",
diff --git a/examples/custom-instance/package.json b/examples/custom-instance/package.json
index 09606bf4..6c16d51b 100644
--- a/examples/custom-instance/package.json
+++ b/examples/custom-instance/package.json
@@ -1,6 +1,6 @@
{
"name": "custom-instance-example",
- "version": "9.0.0",
+ "version": "10.0.0-alpha.0",
"private": true,
"homepage": "https://react-async.async-library.now.sh/examples/custom-instance",
"scripts": {
@@ -14,14 +14,14 @@
"now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build"
},
"dependencies": {
- "react": "16.10.2",
- "react-async": "^9.0.0",
- "react-async-devtools": "^9.0.0",
- "react-dom": "16.10.2",
+ "react": "16.11.0",
+ "react-async": "^10.0.0-alpha.0",
+ "react-async-devtools": "^10.0.0-alpha.0",
+ "react-dom": "16.11.0",
"react-scripts": "3.2.0"
},
"devDependencies": {
- "relative-deps": "0.1.2"
+ "relative-deps": "0.2.0"
},
"relativeDependencies": {
"react-async": "../../packages/react-async/pkg",
diff --git a/examples/movie-app/package.json b/examples/movie-app/package.json
index 4481c126..0b68cbfc 100644
--- a/examples/movie-app/package.json
+++ b/examples/movie-app/package.json
@@ -1,6 +1,6 @@
{
"name": "movie-app-example",
- "version": "9.0.0",
+ "version": "10.0.0-alpha.0",
"private": true,
"homepage": "https://react-async.async-library.now.sh/examples/movie-app",
"scripts": {
@@ -14,14 +14,14 @@
"now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build"
},
"dependencies": {
- "react": "16.10.2",
- "react-async": "^9.0.0",
- "react-async-devtools": "^9.0.0",
- "react-dom": "16.10.2",
+ "react": "16.11.0",
+ "react-async": "^10.0.0-alpha.0",
+ "react-async-devtools": "^10.0.0-alpha.0",
+ "react-dom": "16.11.0",
"react-scripts": "3.2.0"
},
"devDependencies": {
- "relative-deps": "0.1.2"
+ "relative-deps": "0.2.0"
},
"relativeDependencies": {
"react-async": "../../packages/react-async/pkg",
diff --git a/examples/with-abortcontroller/package.json b/examples/with-abortcontroller/package.json
index 0ea0793c..9b2a128e 100644
--- a/examples/with-abortcontroller/package.json
+++ b/examples/with-abortcontroller/package.json
@@ -1,6 +1,6 @@
{
"name": "with-abortcontroller-example",
- "version": "9.0.0",
+ "version": "10.0.0-alpha.0",
"private": true,
"homepage": "https://react-async.async-library.now.sh/examples/with-abortcontroller",
"scripts": {
@@ -14,14 +14,14 @@
"now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build"
},
"dependencies": {
- "react": "16.10.2",
- "react-async": "^9.0.0",
- "react-async-devtools": "^9.0.0",
- "react-dom": "16.10.2",
+ "react": "16.11.0",
+ "react-async": "^10.0.0-alpha.0",
+ "react-async-devtools": "^10.0.0-alpha.0",
+ "react-dom": "16.11.0",
"react-scripts": "3.2.0"
},
"devDependencies": {
- "relative-deps": "0.1.2"
+ "relative-deps": "0.2.0"
},
"relativeDependencies": {
"react-async": "../../packages/react-async/pkg",
diff --git a/examples/with-graphql/.env b/examples/with-graphql/.env
new file mode 100644
index 00000000..7d910f14
--- /dev/null
+++ b/examples/with-graphql/.env
@@ -0,0 +1 @@
+SKIP_PREFLIGHT_CHECK=true
\ No newline at end of file
diff --git a/examples/with-graphql/README.md b/examples/with-graphql/README.md
new file mode 100644
index 00000000..1fdc7d4e
--- /dev/null
+++ b/examples/with-graphql/README.md
@@ -0,0 +1,7 @@
+# GraphQL with useAsync
+
+This demonstrates how to use the `useAsync` hook with GraphQL.
+
+
+
+
diff --git a/examples/with-graphql/package.json b/examples/with-graphql/package.json
new file mode 100644
index 00000000..5880908a
--- /dev/null
+++ b/examples/with-graphql/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "with-graphql-example",
+ "version": "10.0.0-alpha.0",
+ "private": true,
+ "homepage": "https://react-async.async-library.now.sh/examples/with-graphql",
+ "scripts": {
+ "postinstall": "relative-deps",
+ "prestart": "relative-deps",
+ "prebuild": "relative-deps",
+ "pretest": "relative-deps",
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build"
+ },
+ "dependencies": {
+ "graphql-request": "1.8.2",
+ "react": "16.11.0",
+ "react-async": "^10.0.0-alpha.0",
+ "react-async-devtools": "^10.0.0-alpha.0",
+ "react-dom": "16.11.0",
+ "react-scripts": "3.2.0"
+ },
+ "devDependencies": {
+ "relative-deps": "0.2.0"
+ },
+ "relativeDependencies": {
+ "react-async": "../../packages/react-async/pkg",
+ "react-async-devtools": "../../packages/react-async-devtools/pkg"
+ },
+ "eslintConfig": {
+ "extends": "react-app"
+ },
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ],
+ "engines": {
+ "node": ">=8"
+ }
+}
diff --git a/examples/with-graphql/public/favicon.ico b/examples/with-graphql/public/favicon.ico
new file mode 100644
index 00000000..a11777cc
Binary files /dev/null and b/examples/with-graphql/public/favicon.ico differ
diff --git a/examples/with-graphql/public/index.html b/examples/with-graphql/public/index.html
new file mode 100644
index 00000000..b8317902
--- /dev/null
+++ b/examples/with-graphql/public/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
diff --git a/examples/with-graphql/src/index.css b/examples/with-graphql/src/index.css
new file mode 100644
index 00000000..a1f38b9e
--- /dev/null
+++ b/examples/with-graphql/src/index.css
@@ -0,0 +1,19 @@
+body {
+ margin: 20px;
+ padding: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
+ "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.movie {
+ margin-bottom: 40px;
+ line-height: 1.5em;
+}
+.movie dt {
+ font-weight: bold;
+}
+.movie dd {
+ margin-left: 10px;
+}
diff --git a/examples/with-graphql/src/index.js b/examples/with-graphql/src/index.js
new file mode 100644
index 00000000..62f8e136
--- /dev/null
+++ b/examples/with-graphql/src/index.js
@@ -0,0 +1,62 @@
+import React from "react"
+import { useAsync, IfPending, IfFulfilled, IfRejected } from "react-async"
+import ReactDOM from "react-dom"
+import DevTools from "react-async-devtools"
+import { request } from "graphql-request"
+import "./index.css"
+
+const query = /* GraphQL */ `
+ query getMovie($slug: String!) {
+ Movie(slug: $slug) {
+ title
+ releaseDate
+ actors {
+ id
+ name
+ }
+ }
+ }
+`
+
+const loadMovie = async variables => {
+ const { Movie } = await request("https://api.graph.cool/simple/v1/movies", query, variables)
+ return Movie
+}
+
+const MovieDetails = ({ data }) => (
+