Skip to content

Commit

Permalink
Merge branch 'release-next'
Browse files Browse the repository at this point in the history
  • Loading branch information
chaance committed Sep 16, 2022
2 parents f5b5ce2 + 88f6720 commit b35e135
Show file tree
Hide file tree
Showing 61 changed files with 756 additions and 191 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ on:
paths-ignore:
- "docs/**"
- "scripts/**"
- "**/README.md"
- "contributors.yml"
- "**/*.md"
pull_request:
paths-ignore:
- "docs/**"
- "scripts/**"
- "contributors.yml"
- "**/*.md"

jobs:
Expand Down
100 changes: 39 additions & 61 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,76 +33,54 @@ yarn test react --watch

## Releases

New releases should be created from release branches originating from the `dev`
branch. To simplify this process, use the `release.js` Node script.
New releases should be created from release branches originating from `dev`. When you are ready to begin the release process:

```bash
# Ensure you are on the dev branch
git checkout dev

# This command will create a new release branch, merge all changes from main, and
# create a prerelease tag.
yarn release start patch|minor|major

# At this point you can push to GitHub...
git push origin/release-<version> --follow-tags
# ...then publish the pre-release by creating a release in the GitHub UI. Don't
# forget to check the pre-release checkbox!

# If there are any issues with the pre-release, fix the bugs and commit directly
# to the release branch. You can iterate with a new pre-release with the following # command, then publish via GitHub the same as before.
yarn release bump

# Once all tests have passed and the release is ready to be made stable, the following
# command will create a new stable release tag, merge changes back into the dev branch,
# and prompt you to push the changes and tags to GitHub
yarn release finish
git push origin/release-<version> --follow-tags
```
- Check out the `dev` branch.
- Pull all of the changes from GitHub.
- Create a new release branch with the `release-` prefix (eg, `git checkout -b release-next`)
- **IMPORTANT:** The `release-` prefix is important, as this is what triggers our GitHub CI workflow that will ultimately publish the release.
- Branches named `release-experimental` will not trigger our release workflow, as experimental releases handled differently (outlined below).

Once the release is finished, you should see tests run in GitHub actions. Assuming there are no issues (you should also run tests locally before pushing) you can trigger publishing by creating a new release in the GitHub UI, this time using the stable release tag.
Changesets will do most of the heavy lifting for our releases. When changes are made to the codebase, an accompanying changeset file should be included to document the change. Those files will dictate how Changesets will version our packages and what shows up in the changelogs.

After the release process is complete, be sure to merge the release branch back into `dev` and `main` and push both branches to GitHub.
### Starting a new pre-release

### `create-remix`
- Ensure you are on the new `release-*` branch.
- Enter Changesets pre-release mode using the `pre` tag: `yarn changeset pre enter pre`.
- Commit the change and push the the `release-*` branch to GitHub.
- Wait for the release workflow to finish. The Changesets action in the workflow will open a PR that will increment all versions and generate the changelogs.
- Review the PR, make any adjustments necessary, and merge it into the `release-*` branch.
- Once the PR is merged, the release workflow will publish the updated packages to npm.

All packages are published together except for `create-remix`, which is
versioned and published separately. To publish `create-remix`, run the build and
publish it manually.
### Incrementing a pre-release

```bash
yarn build
npm publish build/node_modules/create-remix
```
You may need to make changes to a pre-release prior to publishing a final stable release. To do so:

### Experimental releases and hot-fixes
- Make whatever changes you need.
- Create a new changeset: `yarn changeset`.
- **IMPORTANT:** This is required even if you ultimately don't want to include these changes in the logs. Remember, changelogs can be edited prior to publishing, but the Changeset version script needs to see new changesets in order to create a new version.
- Commit the changesets and push the the `release-*` branch to GitHub.
- Wait for the release workflow to finish and the Changesets action to open its PR.
- Review the PR, make any adjustments necessary, and merge it into the `release-*` branch.
- Once the PR is merged, the release workflow will publish the updated packages to npm.

Experimental releases and hot-fixes do not need to be branched off of `dev`.
Experimental releases can be branched from anywhere as they are not intended for
general use. Hot-fixes are typically applied directly to main. In either case,
the release process here is a bit simpler:
### Publishing the stable release

```bash
# for experimental releases:
git checkout -b release/experimental
yarn run version experimental
yarn run publish

## clean up
git checkout <previous-branch>
git branch -d release/experimental
git push origin --delete release/experimental

# for hot-fix:
git checkout main
## fix changes and commit
git add .
git commit -m "fix: squashed a super gnarly bug"

## version + publish
yarn run version patch
yarn run publish
```
- Exit Changesets pre-release mode: `yarn changeset pre exit`.
- Commit the deleted pre-release file along with any unpublished changesets, and push the the `release-*` branch to GitHub.
- Wait for the release workflow to finish. The Changesets action in the workflow will open a PR that will increment all versions and generate the changelogs.
- Review the PR. We should remove the changelogs for all pre-releases ahead of publishing the stable version. [TODO: We should automate this]
- Once the PR is merged, the release workflow will publish the updated packages to npm.

### Experimental releases

Experimental releases do not need to be branched off of `dev`. Experimental releases can be branched from anywhere as they are not intended for general use.

- Create a new branch for the release: `git checkout -b release-experimental`.
- Make whatever changes you need and commit them: `git add . && git commit "experimental changes!"`.
- Update version numbers and create a release tag: `yarn version:experimental`.
- Push to GitHub: `git push origin --follow-tags`.
- Create a new release for the tag on GitHub to trigger the CI workflow that will publish the release to npm. Make sure you check the "prerelease" checkbox so it is not mistaken for a stable release.

## Local Development Tips and Tricks

Expand Down
2 changes: 2 additions & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@
- juwiragiye
- jveldridge
- jvnm-dev
- jwnx
- kalch
- kanermichael
- karimsan
Expand Down Expand Up @@ -341,6 +342,7 @@
- realjokele
- redabacha
- reggie3
- remix-run-bot
- riencoertjens
- rkulinski
- rlfarman
Expand Down
193 changes: 193 additions & 0 deletions decisions/0004-streaming-apis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
title: Remix (and React Router) Streaming APIs
---

# Title

Date: 2022-07-27

Status: accepted

## Context

Remix aims to provide first-class support for React 18's streaming capabilities. Throughout the development process we went through many iterations and naming schemes around the APIs we plan to build into Remix to support streaming, so this document aims to lay out the final names we chose and the reasons behind it.

It's also worth nothing that even in a single-page-application without SSR-streaming, the same concepts still apply so these decisions were made with React Router 6.4.0 in mind as well - which will support the same Data APIs from Remix.

## Decision

Streaming in Remix can be thought of as having 3 touch points with corresponding APIs:

1. _Initiating_ a streamed response in your `loader` can be done by returning a `defer(object)` call from your `loader` in which some of the keys on `object` are `Promise` instances
2. _Accessing_ a streamed response from `useLoaderData`
1. No new APIs here - when you return a `defer()` response from your loader, you'll get `Promise` values inside your `useLoaderData` object 👌
3. _Rendering_ a streamed value (with fallback and error handling) in your component
1. You can render a `Promise` from `useLoaderData()` with the `<Await resolve={data.promise}>` component
2. `<Await>` accepts an `errorElement` prop to handle error UI
3. `<Await>` should be wrapped with a `<React.Suspense>` component to handle your loading UI

## Details

In the spirit of `#useThePlatform` we've chosen to leverage the `Promise` API to represent these "eventually available" values. When Remix receives a `defer()` response back from a `loader`, it needs to serialize that `Promise` over the network to the client application (prompting Jacob to coin the phrase [_"promise teleportation over the network"_][promise teleportation] 🔥).

### Initiating

In order to initiate a streamed response in your `loader`, you can use the `defer()` utility which accepts a JSON object with `Promise` values from your `loader`.

```js
async function loader() {
return defer({
// Await this, don't stream
critical: await fetchCriticalData(),
// Don't await this - stream it!
lazy: fetchLazyData(),
});
}
```

By not using `await` on `fetchLazyData()` Remix knows that this value is not ready yet _but eventually will be_ and therefore Remix will leverage a streamed HTTP response allowing it to send up the resolved/rejected value when available. Essentially serializing/teleporting that Promise over the network via a streamed HTTP response.

Just like `json()`, the `defer()` will accept a second optional `responseInit` param that lets you customize the resulting `Response` (i.e., in case you need to set custom headers).

The name `defer` was settled on as a corollary to `<script defer>` which essentially tells the browser to _"fetch this script now but don't delay document parsing"_. In a similar vein, with `defer()` we're telling Remix to _"fetch this data now but don't delay the HTTP response"_.

We decided _not_ to support naked objects due to the ambiguity that would be introduced:

```js
// NOT VALID CODE - This is just an example of the ambiguity that would have
// been introduced had we chosen to support naked objects :)

// This would NOT be streamed
function exampleLoader1() {
return Promise.resolve(5);
}

// This WOULD be streamed
function exampleLoader2() {
return {
value: Promise.resolve(5),
};
}

// This would NOT be streamed
function exampleLoader3() {
return {
value: {
nested: Promise.resolve(5),
},
};
}
```

<details>
<summary>Other considered API names:</summary>
<br/>
<ul>
<li><code>deferred()</code> - This is just a bit of a weird word that doesn't have much pre-existing semantic meaning. Is this the <code>jQuery.Deferred</code> thing from back in the day? Remix in general wants to avoid needlessly introducing net-new language to an already convoluted landscape!</li>
<li><code>stream()</code> - We also thought <code>stream</code> might be a good name since that's what the call is telling Remix to do - stream the responses down to the browser. But - this is also potentially misleading because stream is ambiguous in ths case. Developers may mistakenly think that this gives them back a <code>Stream</code> instance and they can arbitrarily send multiple chunks of data down to the browser over time. This is not how the current API works - but also seems like a really interesting idea for Remix to consider in the future, so we wanted to keep the <code>stream()</code> name available for future use cases.</li>
</ul>
</details>

### Accessing

No new APIs are needed for the "Accessing" stage 🎉. Since we've "teleported" these promises over the network, you can access them in your components just as you would with any other data returned from your loader. This value will always be a `Promise`, even after it's been settled.

```js
function Component() {
let data = useLoaderData();
// data.critical is a resolved value
// data.lazy is a Promise
}
```

### Rendering

In order to render your `Promise` values from `useLoaderData()`, Remix provides a new `<Await>` component which handles rendering the resolved value, or propagating the rejected value through an `errorElement` or further upwards to the Route-level error boundaries. In order to access the resolved or rejected values, there are two new hooks that only work in the context of an `<Await>` component - `useAsyncValue()` and `useAsyncError()`.

This examples shows the full set of render-time APIs:

```jsx
function Component() {
let data = useLoaderData(); // data.lazy is a Promise

return (
<React.Suspense fallback={<p>Loading...</p>}>
<Await resolve={data.lazy} errorElement={<MyError />}>
<MyData />
</Await>
</React.Suspense>
);
}

function MyData() {
let value = useAsyncValue(); // Get the resolved value
return <p>Resolved: {value}</p>;
}

function MyError() {
let error = useAsyncError(); // Get the rejected value
return <p>Error: {error.message}</p>;
}
```

Note that `useAsyncValue` and `useAsyncError` only work in the context of an `<Await>` component.

The `<Await>` name comes from the fact that for these lazily-rendered promises, we're not `await`-ing the promise in our loader, so instead we need to `<Await>` the promise in our render function and provide a fallback UI. The `resolve` prop is intended to mimic how you'd await a resolved value in plain Javascript:

```jsx
// This JSX:
<Await resolve={promiseOrValue} />;

// Aims to resemble this Javascript:
let value = await Promise.resolve(promiseOrValue);
```

Just like `Promise.resolve` can accept a promise or a value, `<Await resolve>` can also accept a promise or a value. This is really useful in case you want to AB test `defer()` responses in the loader - you don't need to change the UI code to render the data.

```jsx
async function loader({ request }) {
let shouldAwait = isUserInTestGroup(request);
return {
maybeLazy: shouldAwait ? await fetchData() : fetchData(),
};
}

function Component() {
let data = useLoaderData();

// No code forks even if data.maybeLazy is not a Promise!
return (
<React.Suspense fallback={<p>Loading...</p>}>
<Await resolve={data.maybeLazy} errorElement={<MyError />}>
<MyData />
</Await>
</React.Suspense>
);
}
```

**Additional Notes on `<Await>`**

If you prefer the render props pattern, you can bypass `useAsyncValue()` and just grab the value directly:

```jsx
<Await resolve={data.lazy}>{(value) => <p>Resolved: {value}</p>}</Await>
```

If you do not provide an `errorElement`, then promise rejections will bubble up to the nearest Route-level error boundary and be accessible via `useRouteError()`.

<details>
<summary>Other considered API names:</summary>
<br>
<p>We originally implemented this as a <code>&lt;Deferred value={promise} fallback={&lt;Loader /&gt;} errorElement={&lt;MyError/&gt;} /></code>, but eventually we chose to remove the built-in <code>&lt;Suspense&gt;</code> boundary for better composability and eventual use with <code>&lt;SuspenseList&gt;</code>. Once that was removed, and we were only using a <code>Promise</code> it made sense to move to a generic <code>&lt;Await&gt;</code> component that could be used with <em>any</em> promise, not just those coming from <code>defer()</code> in a <code>loader</code></p>

<p>We also considered various alternatives for the hook names - most notably `useResolvedValue`/`useRejectedValue`. However, these were a bit too tightly coupled to the `Promise` nomenclature. Remember, `Await` supports non-Promise values as well as render-errors, so it would be confusing if `useResolvedValue` was handing you a non-Promise value, or if `useRejectedValue` was handing you a render error from a resolved `Promise`. `useAsyncValue`/`useAsyncError` better encompasses those scenarios as well.</p>
</details>

## React Router Notes

With the presence of the `<Await>` component in React Router and because the Promise's don't have to be serialized over the network - you can _technically_ just return raw Promise values on a naked object from your loader. However, this is strongly discouraged because the router will be unaware of these promises and thus won't be able to cancel them if the user navigates away prior to the promise settling.

By forcing users to call the `defer()` utility, we ensure that the router is able to track the in-flight promises and properly cancel them. It also allows us to handle synchronous rendering of promises that resolve prior to other critical data. Without the `defer()` utility these raw Promises would need to be thrown by the `<Await>` component to the `<Suspense>` boundary a single time to unwrap the value, resulting in a UI flicker.

[promise teleportation]: https://twitter.com/ebey_jacob/status/1548817107546095616
Loading

0 comments on commit b35e135

Please sign in to comment.