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

add-use-state: Create useState magic #4417

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/alpinejs/src/alpine.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { debounce } from './utils/debounce'
import { throttle } from './utils/throttle'
import { setStyles } from './utils/styles'
import { entangle } from './entangle'
import { useState } from './useState'
import { nextTick } from './nextTick'
import { walk } from './utils/walk'
import { plugin } from './plugin'
Expand Down Expand Up @@ -63,6 +64,7 @@ let Alpine = {
evaluate,
initTree,
nextTick,
useState,
prefixed,
prefix,
plugin,
Expand Down
4 changes: 4 additions & 0 deletions packages/alpinejs/src/magics/$useState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { useState } from '../useState'
import { magic } from '../magics'

magic('useState', (initialValue) => useState(initialValue))
2 changes: 2 additions & 0 deletions packages/alpinejs/src/magics/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { warn } from '../utils/warn'
import { magic } from '../magics'

import './$useState'
import './$nextTick'
import './$dispatch'
import './$watch'
Expand All @@ -14,6 +15,7 @@ import './$el'
// Register warnings for people using plugin syntaxes and not loading the plugin itself:
warnMissingPluginMagic('Focus', 'focus', 'focus')
warnMissingPluginMagic('Persist', 'persist', 'persist')
warnMissingPluginMagic('useState()', 'useState', 'use-state')
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like you're adding it to the core...

not as a plugin...

so which is it?

Definitely should not be a built in.


function warnMissingPluginMagic(name, magicName, slug) {
magic(magicName, (el) => warn(`You can't use [$${magicName}] without first installing the "${name}" plugin here: https://alpinejs.dev/plugins/${slug}`, el))
Expand Down
14 changes: 14 additions & 0 deletions packages/alpinejs/src/useState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function useState(initialState = '') {
let state = Alpine.reactive({ value: initialState });

const setState = (newValue) => {
state.value = typeof newValue === 'function' ? newValue(state.value) : newValue;
};

return {
get state() {
return state.value;
},
setState
};
}
82 changes: 82 additions & 0 deletions packages/docs/src/en/magics/useState.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
order: 10
prefix: $
title: useState
---

# $useState

`$useState` is a magic function that allows you to read and set data in variables.

```alpine
<div x-data="{ title: $useState('Hello') }">
<button
@click="title.setState('Hello World!')"
x-text="title.state"
></button>
</div>
```

In the example above, the default value of `title` is set using `$useState('Hello')`. The variable is updated with `title.setState('Hello World!')`, and its value is accessed with `title.state`.

## Initial State

You can initialize the state with any value, including objects and arrays:

```alpine
<div x-data="{ user: $useState({ name: 'John', age: 30 }) }">
<button
@click="user.setState({ name: 'Jane', age: 25 })"
x-text="user.state.name"
></button>
</div>
```

## Reactive Updates

The state is reactive, meaning any changes to the state will automatically update the DOM elements that depend on it. This reactivity extends deeply, so if you pass the state variable as a parameter and modify it within a function, the changes will propagate and update the DOM as if it were an input/output variable.

```alpine
<div x-data="{ count: $useState(0) }">
<button
@click="increment(count)"
x-text="count.state"
></button>
</div>

<script>
function increment(state) {
state.setState(state.state + 1);
}
</script>
```

In this example, the `increment` function takes the state variable `count` as a parameter and updates its value. The DOM automatically reflects the updated state.

## Accessing State

You can access the current state using the `.state` property and update it using the `.setState` method.

## Example with Array

```alpine
<div x-data="{ items: $useState(['Item 1', 'Item 2']) }">
<button
@click="items.setState([...items.state, 'Item 3'])"
x-text="items.state.join(', ')"
></button>
</div>
```

In this example, a new item is added to the array, and the DOM updates to reflect the change.

## Benefits

### Input/Output Variables

One of the key benefits of using `$useState` is the ability to treat state variables as input/output variables. This means you can pass them around in functions and have their changes automatically propagate through the DOM, enhancing the reactivity of your application.

### Enhanced Security

Another significant advantage is that `$useState` helps in complying with Content Security Policy (CSP) guidelines. By avoiding inline scripts and using safer methods to manage state, your application becomes more secure and less vulnerable to certain types of attacks.

38 changes: 38 additions & 0 deletions tests/cypress/integration/magics/$useState.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { haveAttr, html, test } from '../../utils'

test('useState initializes state with the given initial value',
html`
<div x-data="{ state: $useState('testValue') }" x-init="$el.setAttribute('x-data', state)">
Copy link
Contributor

Choose a reason for hiding this comment

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

All your tests fail

</div>
`,
({ get }) => {
get('[x-data]').should(haveAttr('x-data', 'testValue'))
}
)

test('useState updates state correctly',
html`
<div x-data="{ state: $useState('initialValue') }" x-init="$el.setAttribute('x-data', state)">
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this init even doing?

It will just set x-data = [object Object]...

<button @click="state('updatedValue')">Update</button>
</div>
`,
({ get }) => {
get('[x-data]').should(haveAttr('x-data', 'initialValue'))
get('button').click()
get('[x-data]').should(haveAttr('x-data', 'updatedValue'))
}
)

test('useState reacts to state changes',
html`
<div x-data="{ state: $useState('initialValue') }" x-init="$el.setAttribute('x-data', state)">
<button @click="state('updatedValue')">Update</button>
</div>
`,
({ get }) => {
cy.wait(1000) // Espera 1 segundo para asegurarte de que Alpine.js se haya inicializado
get('[x-data]').should(haveAttr('x-data', 'initialValue'))
get('button').click()
get('[x-data]').should(haveAttr('x-data', 'updatedValue'))
}
)
Loading