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

component.get_hooks: inject hook code into react component functions #810

Merged
merged 1 commit into from
Apr 16, 2023

Conversation

masenf
Copy link
Collaborator

@masenf masenf commented Apr 12, 2023

The new Component _get_hooks and get_hooks methods may be overridden by subclasses to return javascript code to be compiled into the react component function after all of the state/event handling logic and before returning the rendered JSX Element.

This allows components to use react hooks and extend or override pynecone state handling logic.

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Example

Here is a PersistentToken component, which defines a useEffect hook to get/set a more persistent identifier in localStorage:

from textwrap import dedent
import typing as t

import pynecone as pc
from pynecone.components.tags.tag import Tag
from pynecone.var import Var


class PersistentToken(pc.Component):
    library = "/utils/state"
    tag = "getToken"
    
    def render(self) -> str:
        """This component has no visual element, it only defines a hook"""
        return ""

    def _get_hooks(self) -> t.Optional[str]:
        trigger_on_change = Tag.format_prop(self.event_triggers["on_change"]).partition("=> ")[2].rstrip("}")
        return dedent("""
            useEffect(() => {
                const TOKEN_KEY = "persistent_token"
                if (typeof window !== "undefined") {
                    if (!window.localStorage.getItem(TOKEN_KEY)) {
                        window.localStorage.setItem(TOKEN_KEY, getToken());
                    }
                    %s
                }
            }, [])""" % trigger_on_change)

    @classmethod
    def get_controlled_triggers(cls) -> t.Dict[str, Var]:
        return {
            "on_change": Var.create("window.localStorage.getItem(TOKEN_KEY)"),
        }


class State(pc.State):
    persistent_token: str = ""

    @pc.var
    def token(self) -> str:
        return self.get_token()


def index() -> pc.Component:
    return pc.center(
        pc.vstack(
            PersistentToken.create(on_change=State.set_persistent_token),
            pc.text("Token: ", State.token),
            pc.text("Persistent Token: ", State.persistent_token),
        ),
    )

app = pc.App(state=State)
app.add_page(index)
app.compile()

Which ends up generating code like this

// ... snipped
useEffect(() => {
    const TOKEN_KEY = "persistent_token"
    if (typeof window !== "undefined") {
        if (!window.localStorage.getItem(TOKEN_KEY)) {
            window.localStorage.setItem(TOKEN_KEY, getToken());
        }
        Event([E("state.set_persistent_token", {value:window.localStorage.getItem(TOKEN_KEY)})])
    }
}, [])
return (
<Center><VStack><Text>{`Token: `}
{state.token}</Text>
<Text>{`Persistent Token: `}
{state.persistent_token}</Text></VStack>
<NextHead><title>{`Pynecone App`}</title>
<meta content="A Pynecone app."
name="description"/>
<meta content="favicon.ico"
property="og:image"/></NextHead></Center>
)
}

Thanks for your consideration.

The new Component `_get_hooks` and `get_hooks` methods may be overridden by
subclasses to return javascript code to be compiled into the react component
function after all of the state/event handling logic and before returning the
rendered JSX Element.

This allows components to use react hooks and extend or override pynecone state
handling logic.
@picklelo
Copy link
Contributor

Awesome thanks for adding this! It looks good, I'll play around with it a bit and leave a review

@Alek99 Alek99 self-requested a review April 16, 2023 04:09
Copy link
Member

@Alek99 Alek99 left a comment

Choose a reason for hiding this comment

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

lgtm

@Alek99 Alek99 merged commit c3148d3 into reflex-dev:main Apr 16, 2023
@masenf masenf deleted the hooks branch April 16, 2023 05:11
@adrianlzt
Copy link

I have updated the @masenf example to make it work with current pynecone version (0.1.34).

from textwrap import dedent
import typing as t

import pynecone as pc
from pynecone.components.tags.tag import Tag


class PersistentToken(pc.Component):
    library = "/utils/state"
    tag = "getToken"

    def render(self) -> str:
        """This component has no visual element, it only defines a hook"""
        return ""

    def _get_hooks(self) -> t.Optional[str]:
        trigger_on_change = Tag.format_prop(self.event_triggers["on_change"]).partition("=> ")[2].rstrip("}").replace(", _e", "")
        return dedent("""
            useEffect(() => {
                const TOKEN_KEY = "persistent_token"
                if (typeof window !== "undefined") {
                    if (!window.localStorage.getItem(TOKEN_KEY)) {
                        window.localStorage.setItem(TOKEN_KEY, getToken());
                    }
                    %s
                }
            }, [])""" % trigger_on_change)

    @classmethod
    def get_controlled_triggers(cls) -> t.Dict[str, pc.Var]:
        return {
            "on_change": pc.Var.create("window.localStorage.getItem(TOKEN_KEY)"),
        }


class State(pc.State):
    persistent_token: str = ""

    @pc.var
    def token(self) -> str:
        return self.get_token()


def index() -> pc.Component:
    return pc.center(
        pc.vstack(
            PersistentToken.create(on_change=State.set_persistent_token),
            pc.text("Token: ", State.token),
            pc.text("Persistent Token: ", State.persistent_token),
        ),
    )

app = pc.App(state=State)
app.add_page(index)
app.compile()

It was just moving from from pynecone.var import Var to pc.Var and adding .replace(", _e", "") to the trigger_on_change variable.

Without that replace, the JS code generated was:

Event([E("state.set_persistent_token", {value:window.localStorage.getItem(TOKEN_KEY)})], _e)

I just removed the , _e to make it look the same as @masenf example.

@masenf
Copy link
Collaborator Author

masenf commented Jun 29, 2023

docs coming soon for a less hacky way to accomplish this in reflex: reflex-dev/reflex-web#121

@adrianlzt
Copy link

What I'm truly interested in is to wrap hooks to get oauth data (#395)

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.

5 participants