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

Docs: update JSX pragma guide with automatic runtime section #1718

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 28 additions & 13 deletions packages/docs/src/pages/guides/how-it-works.mdx
Expand Up @@ -4,13 +4,16 @@ title: How it Works

# How it Works

Theme UI combines several libraries together to form a mini-framework for styling applications.
Theme UI combines several libraries together to form a mini-framework for
styling applications.

Theme UI is built with:

- [Emotion][]: used to generate isolated CSS with theming context
- [MDX][]: used to provide custom Emotion styled components to MDX documents, without polluting the global CSS scope
- [Typography.js][]: optionally used for creating rich typographic styles with a simple, high-level API
- [MDX][]: used to provide custom Emotion styled components to MDX documents,
without polluting the global CSS scope
- [Typography.js][]: optionally used for creating rich typographic styles with a
simple, high-level API

[emotion]: https://emotion.sh
[mdx]: https://mdxjs.com
Expand All @@ -19,13 +22,16 @@ Theme UI is built with:
## ThemeProvider

The Theme UI `ThemeProvider` component is a wrapper around MDX's `MDXProvider`,
which adds custom React components to context,
and Emotion's `ThemeProvider`, which adds the `theme` object to context for use with Emotion.
which adds custom React components to context, and Emotion's `ThemeProvider`,
which adds the `theme` object to context for use with Emotion.

The Theme UI `ThemeProvider` includes a default `components` object with styled components that pick up values from the `theme.styles` object.
For example, `theme.styles.h1` will be used in its `components.h1` component and rendered in MDX documents.
The Theme UI `ThemeProvider` includes a default `components` object with styled
components that pick up values from the `theme.styles` object. For example,
`theme.styles.h1` will be used in its `components.h1` component and rendered in
MDX documents.

For illustrative purposes, the `ThemeProvider` renders something like the following:
For illustrative purposes, the `ThemeProvider` renders something like the
following:

```jsx
// for demonstration only – this does not reflect the actual source code
Expand All @@ -50,18 +56,20 @@ export default ({ components, theme, children }) => (

## Custom Components

When using the `components` prop to add custom MDX components,
each component is wrapped with the `sx` prop to allow the component to pick up values from the `theme.styles` object.
When using the `components` prop to add custom MDX components, each component is
wrapped with the `sx` prop to allow the component to pick up values from the
`theme.styles` object.

## JSX Pragma

The Theme UI JSX function wraps the Emotion JSX function, converting the `sx` prop to a call to `@theme-ui/css`.
The two examples below yield the same results:
The Theme UI JSX function wraps the Emotion JSX function, converting the `sx`
prop to a call to `@theme-ui/css`. The two examples below yield the same
results:

```jsx
// with Emotion's JSX function
// this is typically handled with the Emotion Babel preset
/** @jsxImportSource @emotion/reaact */
flo-sch marked this conversation as resolved.
Show resolved Hide resolved
/** @jsxImportSource @emotion/react */
import { css } from 'theme-ui'

export default (props) => (
Expand Down Expand Up @@ -91,3 +99,10 @@ export default (props) => (
/>
)
```

<Note>

Confused about this `@jsxImportSource` comment? Read more about
[JSX Pragma](/guides/jsx-pragma).

</Note>
154 changes: 140 additions & 14 deletions packages/docs/src/pages/guides/jsx-pragma.mdx
Expand Up @@ -4,13 +4,15 @@ title: JSX Pragma

# JSX Pragma

Theme UI uses custom JSX functions and JSX import source comments to allow you to style elements with values from your theme using the [`sx` prop][].
Theme UI uses custom JSX functions and JSX import source comments to allow you
to style elements with values from your theme using the [`sx` prop][].

## What is JSX

JSX is an XML-like syntax extension to JavaScript.
It's a syntax sugar usually used for React's `jsx` function.
You don't need to write JSX to use React, but it's meant to make code more readable, especially for tree structures with attributes.
JSX is an XML-like syntax extension to JavaScript. It's a syntax sugar usually
flo-sch marked this conversation as resolved.
Show resolved Hide resolved
used for React's `jsx` function. You don't need to write JSX to use React, but
hasparus marked this conversation as resolved.
Show resolved Hide resolved
it's meant to make code more readable, especially for tree structures with
attributes.

Given the following JSX:

Expand All @@ -34,19 +36,26 @@ jsx('div', {
})
```

Most apps use Babel to compile JSX syntax for use with React or other similar libraries.
JSX can be compiled to _any_ function call. By default the Babel plugin will convert JSX into calls to the `jsx` function imported from `react/jsx-runtime`, but libraries like Preact, MDX, Emotion, and Vuejs use custom functions to create elements with JSX.
Most apps use Babel to compile JSX syntax for use with React or other similar
libraries. JSX can be compiled to _any_ function call. By default the Babel
plugin will convert JSX into calls to the `jsx` function imported from
`react/jsx-runtime`, but libraries like Preact, MDX, Emotion, and Vuejs use
custom functions to create elements with JSX.

Technically Babel also calls `jsxs` from `react/jsx-runtime` and `jsxDEV` from `react/jsx-dev-runtime` in some cases, but the concept still holds.
Technically Babel also calls `jsxs` from `react/jsx-runtime` and `jsxDEV` from
`react/jsx-dev-runtime` in some cases, but the concept still holds.

To change the underlying create element functions, you can either add an option to the Babel plugin or you can set a _pragma comment_ at the beginning of a module.
Changing the function import source in the Babel plugin will transform all JSX in an application into the same set of functions.
Using a pragma comment limits the change to only the modules that it's added to.
This lets you default to the React `jsx` function in most places and use custom functions only where you need it,
giving the author more control over where it's used.
To change the underlying create element functions, you can either add an option
to the Babel plugin or you can set a _pragma comment_ at the beginning of a
module. Changing the function import source in the Babel plugin will transform
hasparus marked this conversation as resolved.
Show resolved Hide resolved
all JSX in an application into the same set of functions. Using a pragma comment
limits the change to only the modules that it's added to. This lets you default
to the React `jsx` function in most places and use custom functions only where
you need it, giving the author more control over where it's used.

Theme UI uses custom create element functions to add the `sx` and `css` props in React.
The preferred way of using these functions is by adding the custom pragma comment to the top of your file.
Theme UI uses custom create element functions to add the `sx` and `css` props in
React. The preferred way of using these functions is by adding the custom pragma
comment to the top of your file.

```js
/** @jsxImportSource theme-ui */
Expand All @@ -55,3 +64,120 @@ The preferred way of using these functions is by adding the custom pragma commen
See the [`sx` prop][] docs to learn more.

[`sx` prop]: /sx-prop

## Automatic JSX runtime

React v17 introduced a
[new JSX transform](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html),
also called "automatic runtime" (backported to React v16.14.0) If you use any of
those versions together with a configurable transpiler (such as Babel or
TypeScript), you can configure JSX to use the automatic runtime (globally, for
your entire app), and no longer need to use the custom pragma comments in your
files to use `sx`.

Here are a few examples showing how to do that depending how you build your
application:

### Using @babel/preset-react

```js
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-react',
{
importSource: 'theme-ui', // or '@theme-ui/core'
runtime: 'automatic',
throwIfNamespace: false,
},
],
],
// ...
}
```

NOTE: this requires
[babel >= 7.9.0](https://babeljs.io/docs/en/babel-preset-react#react-automatic-runtime)

### Using @babel/plugin-transform-react-jsx

This plugin is included in the preset above, but can also used as a standalone
babel plugin.

```js
// babel.config.js
module.exports = {
presets: [
[
'@babel/plugin-transform-react-jsx',
{
importSource: 'theme-ui', // or '@theme-ui/core'
runtime: 'automatic',
throwIfNamespace: false,
},
],
],
// ...
}
```

NOTE: this requires
[babel >= 7.9.0](https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#react-automatic-runtime)

### Using TypeScript

If you use TypeScript to transpile your source code with `tsc` (or only
typecheck), or for instance to run tests with `ts-jest`
Copy link
Member

@hasparus hasparus May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry mate, I was under impression we already merged this PR.

Could we write something like "If you use TypeScript to transpile your JSX"?
A lot of people are using TypeScript together with Babel, and they don't need to configure jsxImportSource globally twice (you only need it in Babel config if you're using Babel)

Copy link
Collaborator Author

@flo-sch flo-sch May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still need it for instance for eslint and other tools like ts-jest that do not use Babel, even if you use @babel/preset-typescript to build, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I assumed people would use babel-jest, but typescript-eslint will probably need it in the tsconfig when it's doing typechecking.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we still be more explicit here then? I don't mind 🤷

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's okay now. @lachlanjc could you take a look at this PR when you have a moment?


```json
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "theme-ui"
}
// ...
}
```

NOTE: this requires
[TypeScript >= 4.1](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#react-17-jsx-factories)

NOTE: in order to typecheck `sx` using automatic runtime, you will also need to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this also the case when "jsxImportSource": "theme-ui" is configured in TypeScript config?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huuuh, I think this is above my TS understanding to be honest, but I can re-try in a project 😁

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was under impression that TS correctly found types from configured importSource, but I'll have to double-check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like you're right actually, good catch :)
I will remove it

Copy link
Collaborator Author

@flo-sch flo-sch May 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hasparus just figured out why I thought this was still necessary:

If you apply sx to a non theme-ui component without manually providing sx types, then it does not typecheck even with jsxImportSource: "theme-ui" inside tsconfig.json (even though sx gets correctly compiled)

// StyledForm.tsx
import { Button, Flex, Input, Label } from 'theme-ui';

// Some interface not explicitly typing "sx"
export interface StyledFormProps {
  onChange(data: Record<string, unknown>): void;
}

const StyledForm: React.FC<StyledFormProps> = ({
  onChange,
  ...props
}) => (
  <form {...props}> // spreading props, including sx --> Emotion correctly compiles it to a className
    <Flex sx={{ flexFlow: 'column', p: 's' }}>
      <Label htmlFor="input.test">Test</Label>
      <Input
        id="input.test"
        type="text"
        onChange={
          (event) => onChange({
            test: event.target.value
          })
        }
      />
    </Flex>
  </form>
);

export default StyledForm;

TS error when forwarding sx

// App.tsx
const App = () => (
  <StyledForm
    onChange={console.log}
    sx={{
      // Type '{ /* ... */ sx: { ...; }; }' is not assignable to type 'IntrinsicAttributes & StyledFormProps & { children?: ReactNode; }'.
      //   Property 'sx' does not exist on type 'IntrinsicAttributes & StyledFormProps & { children?: ReactNode; }'.
      'px': 'm'
    }}
  />
);

This can be easily fixed by adding sx?: ThemeUIStyleObject to StyledFormProps in that case, just slightly sad that we get a TS error even though the className works fine IMHO, but I don't mind adding the types wherever it is needed

Could/Should the automatic sx typings be added to all JSX components instead of only theme-ui ones, or is that anti-pattern? 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @flo-sch 👋 @hasparus doesn't have access to his computer, and he asked me to write this 😄

Theme UI types add sx prop only to components which props already accept className, as sx transpiles to className.

The code responsible for this is at packages/core/src/jsx-namespace.ts#L3.

Here's a CodeSandbox, so you can try it out: https://codesandbox.io/s/frosty-https-wpp7f?file=/src/index.tsx

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @beerose, nice to meet you and thanks for relaying the answer 😁
(not sure if the sandbox link was the correct one though, seems more about GraphQL in there?)

That make sense in term of typings I guess, I did reproduce it (even without automatic runtime):
https://codesandbox.io/s/sx-props-typings-automatic-runtime-dte1v?file=/src/App.tsx

My only claim was that, since Emotion successfully compiles it anyway, it felt like a "false-negative" from TS to complain on missing sx types in that case (in that App.tsx, there is a TS complain on the first <Form sx={{ }} /> since it does not add types for className or sx)

But I guess this is another discussion here, not related to automatic runtime in itself

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! I'm not sure how it happened but yes, the link was wrong 🤦‍♀️
Here's the valid one: https://codesandbox.io/s/affectionate-architecture-utrwm?file=/src/index.tsx
Sorry!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only claim was that, since Emotion successfully compiles it anyway, it felt like a "false-negative" from TS to complain on missing sx types in that case (in that App.tsx, there is a TS complain on the first <Form sx={{ }} /> since it does not add types for className or sx)

IMHO this isn't a false negative — sx is compiled to className which is not handled so we see almost the same error as if we passed a className, meaning "this component cannot be styled from the outside".

add `sx` manually to React types:

```ts
// To add `css` if you're using it somewhere
/// <reference types="@emotion/react/types/css-prop" />

import {} from 'react'
import { ThemeUIStyleObject } from 'theme-ui' // or '@theme-ui/core'

declare module 'react' {
interface Attributes {
sx?: ThemeUIStyleObject
}
}
```

### Using next.js

```js
// babel.config.js
module.exports = {
presets: [
[
'next/babel',
{
'preset-react': {
importSource: 'theme-ui', // or '@theme-ui/core'
runtime: 'automatic',
throwIfNamespace: false,
},
},
],
],
// ...
}
```
hasparus marked this conversation as resolved.
Show resolved Hide resolved