Skip to content

add-use-state: Create useState magic#4417

Closed
MaquinaTech wants to merge 2 commits intoalpinejs:mainfrom
MaquinaTech:add-use-state
Closed

add-use-state: Create useState magic#4417
MaquinaTech wants to merge 2 commits intoalpinejs:mainfrom
MaquinaTech:add-use-state

Conversation

@MaquinaTech
Copy link
Copy Markdown

Hello everyone.
I have created a new magic property to increase security in CSP-compliant developments.
Additionally, you can send variables by parameters to a function, change its value and have it extended.
I hope it helps you and I would love it if we could all improve this new implementation if necessary.

@MaquinaTech MaquinaTech changed the title add-use-state: Creare useState magic add-use-state: Create useState magic Oct 27, 2024
@SimoTod
Copy link
Copy Markdown
Collaborator

SimoTod commented Oct 27, 2024

Out of curiosity, what's the difference between a state and the already existing Alpine.stores?

I think the example we'll not run with the CSP build of Alpine, not sure if it makes more sense to show a fully functioning example if the aim is to help CSP complaint sites

@MaquinaTech
Copy link
Copy Markdown
Author

The difference between Alpine.store and $useState is that the store defines the data at a global level and $useState does it at a local level respecting the component hierarchy. The communication between Alpine components is quite poor and define global variables degrades it even more.

@MaquinaTech
Copy link
Copy Markdown
Author

MaquinaTech commented Oct 27, 2024

To respect CSP, direct insertions into variables in the HTML are not allowed, for example @click="count++"

@ekwoka
Copy link
Copy Markdown
Contributor

ekwoka commented Oct 28, 2024

I don't see why this should be a core plugin.

the naming is also confusing for this use case.

Does the current CSP build allow...function call expressions?

Checked. It does not.

So this still won't evaluate in expressions anyway.

Instead of this, I'd really just prefer a more advanced expression parser build into the CSP evaluator.

// 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
Copy Markdown
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.


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
Copy Markdown
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


test('useState updates state correctly',
html`
<div x-data="{ state: $useState('initialValue') }" x-init="$el.setAttribute('x-data', state)">
Copy link
Copy Markdown
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]...

@SimoTod
Copy link
Copy Markdown
Collaborator

SimoTod commented Oct 28, 2024

The difference between Alpine.store and $useState is that the store defines the data at a global level and $useState does it at a local level respecting the component hierarchy. The communication between Alpine components is quite poor and define global variables degrades it even more.

Right. In that case, I don't think it solves a big problem in Alpine that would suggest it's needed in the core or even as a first class plugin. x-data has already reactive data scoped locally, the state you define is just a wrapper around a variable.

I had a good look at your PR and I can see why you do it, you are trying to define pure functions that only modify the state and you were not happy because you can't do it for things such as integer literals because they are not references in javascript (like objects are).

<div x-data="{
  count: 0,
  state: {count: 0}
}">
    <button
        @click="increment(count)" // This does not work
        x-text="count"
    ></button>
    <button
        @click="increment2(state)" // This work
        x-text="state.count"
    ></button>
</div>

<script>
    function increment(state) {
        state++;
    }
    function increment2(state) {
        state.count++;
    }
</script>

but in doing so, you are forcing people to use .state and .setState() so it's adds more boilerplate (the majority of the devs will object that it should be transparent like proxy objects to remove DX frictions).

You may not agree with the approach but the Alpine way to do it (or at least one of them) would be to define the logic in your component and not in the global scope so it would look more like

<div x-data="{
  count: 0,
  increment() {this.count++}
}">
    <button
        @click="increment"
        x-text="count"
    ></button>
</div>

or, for those not liking js in the html attributes (CSP build as well), something like

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('counter', () => ({
            count: 0,
            increment() {this.count++}
        })
    })
</script>
<div x-data="counter">
    <button
        @click="increment"
        x-text="count"
    ></button>
</div>

That being said, if you like the approach, you can use it in your own project. You can even publish a third party plugin for you to use in all your projects and for other likeminded people who likes the approach but I personally don't feel like it belongs to the core.

@MaquinaTech
Copy link
Copy Markdown
Author

I find your answer very interesting @SimoTod . You are right in what you say and I will add this tool as an external plugin. I think the most important use of this is to be able to modify the input parameters just like you do in increment2 in a consistent and simple way.

@calebporzio
Copy link
Copy Markdown
Collaborator

PR Review: #4417 — add-use-state: Create useState magic

Type: Feature
Verdict: Close

What's happening (plain English)

This PR adds a new $useState magic property to Alpine core. It works like React's useState:

  1. You call $useState('initial') in x-data and get back an object with .state (getter) and .setState() (setter).
  2. Under the hood it wraps the value in Alpine.reactive({ value: initialState }) and exposes a getter/setter pair.
  3. The stated motivation is CSP compliance and "pass-by-reference" for primitives — wrapping a primitive in an object so you can pass it to external functions and have mutations propagate back.

Other approaches considered

  1. Alpine.data() (existing) — Already handles the CSP use case perfectly. You define named components with Alpine.data('counter', () => ({ count: 0, increment() { this.count++ } })) and reference them with x-data="counter". No inline expressions needed.
  2. Alpine.store() (existing) — For shared state across components, stores already exist.
  3. Just use objects in x-data — If you need pass-by-reference semantics, x-data="{ state: { count: 0 } }" already works because objects are references. No new API needed.

All three existing approaches are simpler and already cover the stated use cases.

Changes Made

No changes made. This PR should be closed, not fixed.

Test Results

No CI checks have been run on this branch. The tests in the PR are also broken — they call state('updatedValue') as if $useState returns a function, but the implementation returns { state, setState }. The tests would not pass against the actual implementation. There's also a hardcoded cy.wait(1000) with a Spanish comment, and tests 2 and 3 are identical except for the wait.

Code Review

Several issues beyond the fundamental "should this exist?" question:

  • useState.js:2: Uses Alpine.reactive() as a global but Alpine is never imported. This would throw a ReferenceError at runtime.
  • useState.js:4: Uses const — Alpine uses let.
  • useState.js:4-5: Uses semicolons — Alpine doesn't.
  • magics/index.js:18: Registers a "missing plugin" warning for useState — but this isn't a plugin, it's defined in core. This is contradictory: the magic is registered on line 4 and a "you didn't install the plugin" fallback is registered on line 18. The fallback would overwrite the real registration.
  • Tests are broken: The test HTML uses state('updatedValue') but the implementation returns { state, setState }, so you'd need state.setState('updatedValue'). The tests don't match the implementation.
  • Naming confusion: $useState borrows React's naming but doesn't behave like React's useState (no component-scoped lifecycle, no array destructuring, different API shape). This would confuse both React devs and Alpine devs.
  • Docs claim CSP compliance: But the examples in the docs use inline expressions like @click="title.setState('Hello World!')" which still violate CSP.

Security

No security concerns beyond the false claim of CSP compliance — the implementation doesn't actually solve CSP issues as described.

Verdict

Close. The community already reached this conclusion in the comments. A collaborator (@SimoTod) and a contributor (@ekwoka) both independently identified that this doesn't solve a problem Alpine doesn't already handle with Alpine.data(), Alpine.store(), and plain objects in x-data.

The specific issues:

  1. It doesn't need to exist. Every use case described is already covered by existing Alpine APIs. Alpine.data() handles CSP-compliant component definitions. Alpine.store() handles shared state. Objects in x-data already give you pass-by-reference semantics.
  2. The implementation is broken. Alpine is used as a global without being imported. The tests don't match the API. The "missing plugin" warning overwrites the actual magic registration.
  3. The naming is misleading. Borrowing useState from React creates false expectations about behavior.
  4. The contributor agreed. @MaquinaTech acknowledged in the comments that this belongs as a third-party plugin, not in core.

This adds permanent public API surface to Alpine for something that's already solvable with existing tools. Per Alpine's philosophy of simplicity, this should stay a third-party plugin if the author wants to maintain it.


Reviewed by Claude

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants