Skip to content

Commit

Permalink
add more examples
Browse files Browse the repository at this point in the history
  • Loading branch information
KurtGokhan committed May 13, 2023
1 parent 2196d20 commit 91e8051
Show file tree
Hide file tree
Showing 22 changed files with 667 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/context.ts
Expand Up @@ -82,7 +82,7 @@ function createMiddlewareContextWithDefaults(

function jsxClassic(type: any, props: any, ...children: any[]) {
if (props == null) props = {};
if (children != null) props.children = children;
if (children != null) props.children = children.length === 1 ? children[0] : children;

const key = props.key;
delete props.key;
Expand Down
130 changes: 130 additions & 0 deletions website/docs/best-practices.mdx
@@ -0,0 +1,130 @@
---
sidebar_position: 3
---

import WithClickCountExample from '@site/src/examples/with-click-count';

# Best Practices

If you are reading this documentation, it is probably because JSX Middlewares are a new concept for you. There may be some pitfalls that you may not be aware of. This section will help you to avoid them, and use JSX Middlewares in the best way possible.

## Use responsibly

JSX Middlewares give you a lot of control over your JSX. However, this can be a double-edged sword. If you are not careful, you can end up with a lot of middlewares that are hard to understand. It is recommended to use JSX Middlewares only when you need them, and to keep them simple.

Middlewares can't do anything React itself can't do. But it can do some things the JSX syntax alone can't do.

If you see yourself writing a lot of the same code in your JSX, it may be a good idea to write a middleware for it. In this regard, JSX Middlewares are similar to Higher Order Components (HOCs). It is also similar to Angular directives.

## Understand the JSX

JSX Middlewares are called for every JSX element in your application. It is important to understand how JSX works to be able to avoid pitfalls. It is especially important if you are a library developer, and you want to write middlewares that work with any JSX. You can't assume that the JSX will be written in a certain way.

Some of the things to keep in mind are:

- The `type` is the rendered component. It can be a `string`, `function` or `class`. But it can also be a React Fragment, in which case it is a `symbol`.
- The `props` is the object which includes all the props passed to the rendered component. Remember that `key` can't be accessed from `props`. It is a separate argument.
- `props` can include the `children` passed to a component. It is important to understand that the `children` is not always a `string` or `ReactNode`. It can be any JavaScript value, including `Array`, `null`, `undefined` or even functions.
- The `key` is the key passed to the rendered component. You should generally pass it to the next middleware as is.

## Understand the order of middlewares

The order of middlewares is important. Middlewares are called in the reverse order they were added (LIFO). So, if you add middlewares like this:

```jsx
addMiddlewares(middleware1, middleware2);
addMiddlewares(middleware3);
```

The order of execution will be `middleware3`, `middleware2`, `middleware1` and lastly the original JSX factory.

## Avoid side effects

JSX Middlewares are not meant to be used for side effects. They need to be pure functions. They are meant to transform JSX elements into other JSX elements.

If you need to do side effects, you can wrap your JSX in a component that does the side effects. Check the example in [Rules of hooks](#remember-the-rules-of-hooks) section to see how to do this.

## Avoid infinite loops

It is possible to create infinite loops with JSX Middlewares if you are not careful. This often happens when you unconditionally create new JSX elements in a middleware.

For example, a middleware like this will cause infinite loops, because creating a `div` will call this middleware again, and so on.

```jsx
function wrapMiddleware(next, type, props, key) {
return <div>{next(type, props, key)}</div>;
}
```

There are several ways to fix this. For example, by adding a condition to wrap in a `div` only if the `type` is not already a `div`.

It is also a good idea to run the logic of the middleware only if a specific prop is passed to the element. To distinguish such props, it can be a good idea to prefix them with `$`.

Another way to solve this is to use the `original` property of `next` to create JSX elements without middlewares.

```jsx
function wrapMiddleware(next, type, props, key) {
const children = next(type, props, key);
return next.original('div', { children }, key);
}
```

## Remember the rules of hooks

You cannot use hooks in JSX middlewares. But you can wrap your JSX in a components that uses hooks.

For example, let's say we want to write a middleware that adds the click count to an element. Writing it like this would be wrong:

```jsx
// error-start

function withClickCountMiddleware(next, type, props, key) {
const [count, setCount] = useState(0);

if (props.$withClickCount) {
const { $withClickCount, ...restProps } = props;
props = {
...restProps,
onClick: () => setCount(count + 1),
children: [props.children, ` - Clicked ${count} times`],
};
}

return next(type, props, key);
}

// error-end
```

Instead, you can write it with a component:

```jsx
function WithClickCount({ render, props, type }) {
const [count, setCount] = useState(0);

props = {
...props,
onClick: () => setCount(count + 1),
children: [props.children, ` - Clicked ${count} times`],
};

return render(type, props);
}

function withClickCountMiddleware(next, type, props, key) {
if (props.$withClickCount) {
const { $withClickCount, ...restProps } = props;
return <WithClickCount render={next} type={type} props={restProps} key={key} />;
}

return next(type, props, key);
}

addMiddlewares(withClickCountMiddleware);

function App() {
return <button $withClickCount>Click me</button>;
}
```

<WithClickCountExample />
2 changes: 1 addition & 1 deletion website/docs/examples/_category_.json
Expand Up @@ -3,6 +3,6 @@
"position": 2,
"link": {
"type": "generated-index",
"description": "Various examples to show how this library can be useful."
"description": "Various examples to show how this library can be useful. These examples are pretty basic, but they should give you a good idea of how to use this library."
}
}
50 changes: 50 additions & 0 deletions website/docs/examples/if-directive.mdx
@@ -0,0 +1,50 @@
---
sidebar_position: 1
---

import IfDirectiveExample from '@site/src/examples/if-directive';

# If Directive

This example shows how to implement a `$if` directive that allows you to conditionally render a component.

```jsx
function ifDirectiveMiddleware(next, type, props, key) {
if ('$if' in props) {
if (!props.$if) return null;
delete props.$if;
}

return next(type, props, key);
}

addMiddlewares(ifDirectiveMiddleware);
```

## Usage

```jsx
function App() {
const [show, setShow] = useState(false);

return (
<div>
<button onClick={() => setShow(!show)}>Toggle</button>

<div $if={show}>Show 'em</div>
</div>
);
}
```
<IfDirectiveExample />
## Typescript types
```ts
declare module 'react' {
interface Attributes {
$if?: any;
}
}
```
38 changes: 38 additions & 0 deletions website/docs/examples/ripple.mdx
@@ -0,0 +1,38 @@
---
sidebar_position: 3
---

import RippleExample from '@site/src/examples/ripple';

# Ripple

This example shows how to implement a middleware that shows a ripple effect on all buttons and all elements that have `$ripple` property.
`$ripple` property can be set to false to disable ripple effect on buttons.

Visit the source code of this example to see the implementation.

## Usage

```jsx
function App() {
return (
<div>
<button>Click to see ripple</button>

<button $ripple={false}>This doesn't have ripple though</button>
</div>
);
}
```
<RippleExample />
## Typescript types
```ts
declare module 'react' {
interface Attributes {
$ripple?: boolean;
}
}
```
35 changes: 35 additions & 0 deletions website/docs/examples/tooltip.mdx
@@ -0,0 +1,35 @@
---
sidebar_position: 2
---

import TooltipExample from '@site/src/examples/tooltip';

# Tooltip

This example shows how to implement a `$tooltip` directive that shows a custom tooltip when element is hovered.

Visit the source code of this example to see the implementation.

## Usage

```jsx
function App() {
return (
<div>
<button $tooltip={'Secret tooltip here'}>Hover the mouse over me to see a tooltip</button>
</div>
);
}
```

<TooltipExample />

## Typescript types

```ts
declare module 'react' {
interface Attributes {
$tooltip?: ReactNode;
}
}
```
8 changes: 3 additions & 5 deletions website/docs/index.mdx
Expand Up @@ -3,7 +3,7 @@ sidebar_position: 1
title: Introduction
---

import IndexExample from '@site/src/components/examples/intro';
import IndexExample from '@site/src/examples/intro';

# JSX Middlewares

Expand Down Expand Up @@ -124,7 +124,7 @@ import { addMiddlewares } from 'jsx-middlewares/react';
addMiddlewares(function outlineMiddleware(next, type, props, key) {
// Do something with the type and props

// This will modify the props to add a border and margin to all HTML elements
// Modify the props to add a border and margin to all HTML elements
if (typeof type === 'string') {
props = {
...props,
Expand Down Expand Up @@ -157,6 +157,4 @@ export function App() {

The output will be like this:

<BrowserWindow>
<IndexExample />
</BrowserWindow>
<IndexExample />
12 changes: 12 additions & 0 deletions website/docusaurus.config.js
Expand Up @@ -77,6 +77,18 @@ const config = {
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
magicComments: [
{
className: 'theme-code-block-highlighted-line',
line: 'highlight-next-line',
block: { start: 'highlight-start', end: 'highlight-end' },
},
{
className: 'code-block-error-line',
line: 'error-next-line',
block: { start: 'error-start', end: 'error-end' },
},
],
},
}),
};
Expand Down
3 changes: 2 additions & 1 deletion website/package.json
Expand Up @@ -23,7 +23,8 @@
"jsx-middlewares": "link:..",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"react-is": "^18.2.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.4.0",
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/BrowserWindow/index.tsx
Expand Up @@ -6,7 +6,7 @@ import styles from './styles.module.css';
interface Props {
children: ReactNode;
minHeight?: number;
url: string;
url?: string;
style?: CSSProperties;
bodyStyle?: CSSProperties;
}
Expand Down
12 changes: 12 additions & 0 deletions website/src/css/custom.css
Expand Up @@ -28,3 +28,15 @@
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}

.code-block-error-line {
background-color: #ff000020;
display: block;
margin: 0 calc(-1 * var(--ifm-pre-padding));
padding: 0 var(--ifm-pre-padding);
border-left: 3px solid #ff000080;
}

.footer {
display: none;
}
39 changes: 39 additions & 0 deletions website/src/examples/if-directive.tsx
@@ -0,0 +1,39 @@
/** @jsx jsx */

import { useState } from 'react';
import BrowserWindow from '../components/BrowserWindow';
import { createLocalJsxContext } from './setup';

const ctx = createLocalJsxContext();
const jsx = ctx.jsxClassic;

function ifDirectiveMiddleware(next, type, props, key) {
if ('$if' in props) {
if (!props.$if) return null;
delete props.$if;
}

return next(type, props, key);
}

ctx.addMiddlewares(ifDirectiveMiddleware);

function App() {
const [show, setShow] = useState(false);

return (
<div>
<button onClick={() => setShow(!show)}>Toggle</button>

<div $if={show}>Show 'em</div>
</div>
);
}

export default function IfDirectiveExample() {
return (
<BrowserWindow>
<App />
</BrowserWindow>
);
}

0 comments on commit 91e8051

Please sign in to comment.