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

feat: new simple framework agnostic api #158

Closed
wants to merge 1 commit into from

Conversation

michalkvasnicak
Copy link
Collaborator

Change Summary

This PR is just another attempt to simplify the frames app building and also provide framework agnostic API that can be used outside of Next.js

Merge Checklist

  • PR has a Changeset
  • PR includes documentation if necessary
  • PR updates the boilerplates if necessary

Copy link

vercel bot commented Mar 9, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
frames-js ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 9, 2024 6:26pm
framesjs-debugger ❌ Failed (Inspect) Mar 9, 2024 6:26pm

@@ -0,0 +1,72 @@
import { AnyJson } from "../new-api";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@davidfurlong @stephancill the issue here is that next.js fails to compile the route because it somehow doesn't allow to use react in route handlers. Maybe there is a way to work around this but I couldn't find one quickly.

export const framesApp = (
<FramesApp>
<Frame index>
{({ frameState }) => (
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@davidfurlong @stephancill if you need to access the frame state (state passed from button click!, not the global frames state parsed from meta tag) you can use a function to render the frame.

Copy link
Contributor

Choose a reason for hiding this comment

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

not a big fan of render props - think they're awkward and many developers don't use them/don't know how to. It's hard to reason about whats injected and where it's coming from

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@davidfurlong who is our target audience? I'm confused because render props is common pattern as well. But I would dive that much into React as it is unnecessary to build frames app and makes the app and whole build bigger.

path="/next"
label="Next"
state={{
clicked: isFrameState(frameState) ? frameState.clicked + 1 : 1,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@davidfurlong @stephancill this is a next frame state (the frame state of the destination frame will be set to this value)

* GET renders always only initial frames
*/
export async function GET(req: Request) {
return renderFramesToResponse(framesApp, req);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@davidfurlong @stephancill this returns Response from Web API so is compatible with Remix and Next.js, by using some wrapper it could be also compatible with express, etc...

@@ -0,0 +1,286 @@
import assert from "assert";
Copy link
Collaborator Author

@michalkvasnicak michalkvasnicak Mar 9, 2024

Choose a reason for hiding this comment

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

This file contains framework agnostic frames building, you can use this one insteead of React. React is just a wrapper around it. So perhaps the nextjs route could be fixed by removing react API and use this one directly.

This API is not complete, I wanted to open the discussion.

@davidfurlong @stephancill

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 the reason we are sticking to the react-like syntax? Feels like it is forcing us to keep all the logic contained within a single component

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

React just because whole frames were mainly promoted as nextjs thingy right? So I guess it is nice to also provide React syntax although in my opinion it unnecessarily limits us in order to provide the easiest API possible.

Also I think you meant to comment on react api right? Because this file doesn't contain React API, this is agnostic API using simple classes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Also another question, how would you like to built frames apps? I think it's easier to think about some non abstract examples.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure about @davidfurlong, but I'm not attached to React for this use case as it introduces a lot of magic abstractions.

As for how I would like to build frames apps, I like the <FrameContainer> pattern that frames.js uses today. Getting rid of the reducer would be a big improvement and button targets make it possible to have separate file-based routes (ideally without magic redirects). If a frame route can be simplified to something like

export async function POST(req: NextRequest) {
  const {frameMessage, state} = await getFrameMessage(req)
  
  // side effects

  return <FrameContainer>
    ...
  </FrameContainer>
  
}

then we would already be in a great spot. I know @davidfurlong is a lot more opinionated about this so maybe he has a different idea.

Copy link
Contributor

Choose a reason for hiding this comment

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

agree with stephan on this

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok so essentially just keep the API as is but change how post button works to use paths so you can have multiple handlers.

I still think that having some sort of router is closer to react developers than having multiple route files. But maybe that could be abstracted afterwards.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I would rename the getFrameMessage as it returns message and state.

@michalkvasnicak
Copy link
Collaborator Author

@davidfurlong @stephancill this one is a draft to open a discussion with you. This API is framework agnostic so it should be really easy to integrate frames anywhere and also provide more templates to #143 so we can support remix, express, etc.


export const framesApp = (
<FramesApp>
<Frame index>
Copy link
Contributor

Choose a reason for hiding this comment

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

I think path="/" here is okay. It's not immediately clear what the index prop is doing

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense but also index is used to detect initial frame. So perhaps initial prop would be better?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think path="/" is clearer. there could be multiple frame starting points

Copy link
Collaborator Author

@michalkvasnicak michalkvasnicak Mar 12, 2024

Choose a reason for hiding this comment

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

How would you set up multiple starting points @davidfurlong ? When you build frames app you have just one GET handler and only one Initial frame right?

Comment on lines +102 to +111
class FrameExternalButton implements IFrameButton {
private href: URL | undefined;
private index: AllowedButtonIndexes | undefined;
private label: string | undefined;

constructor(href: URL, index: AllowedButtonIndexes, label: string) {
this.href = href;
this.index = index;
this.label = label;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Should try to stick more closely to the spec and avoid introducing new abstractions like href - buttons have an action and target

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep that makes sense.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok when I'm thinking about it, it doesn't matter whether it is href or action, the point is, that it must be an URL: I don't like having disjoint unions to define stuff that much, it is more clear to have specific classes or functions to create specific buttons (e.g. createMintButton, createPostButton or something like this than createButton({ action: 'post', ... })).


export const framesApp = (
<FramesApp>
<Frame index>
Copy link
Contributor

Choose a reason for hiding this comment

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

Where do you execute side effects like updating a db or executing a transaction?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That one is tricky and currently it is not implemented and I think that's the drawback of using React API as it doesn't allow us to use async render handlers.

Either we would have some sort of "hacky" solution I mean you could have on Frame a prop handler: () => Promise<void> or something like that.

Otherwise on agnostic API it would be just a handler property where you could define your own async function to do something when Frame is rendering.

@@ -0,0 +1,286 @@
import assert from "assert";
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 the reason we are sticking to the react-like syntax? Feels like it is forcing us to keep all the logic contained within a single component

@stephancill stephancill mentioned this pull request Mar 11, 2024
4 tasks
Copy link
Contributor

@davidfurlong davidfurlong left a comment

Choose a reason for hiding this comment

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

hm I feel like this PR is rewriting a largely declarative (JSX) definition into some sort of nested imperative library for rendering it. Not a fan of the overall direction.

Feel it would be much cleaner if dev returned json (maybe via jsx syntax, maybe not?), then lib used a zod validator on that json to catch errors. Declarative > Imperative, Functional > Object oriented is my north star here. Feels like we're going the wrong direction on those fronts somewhat.

This PR is intended to solve

  1. Decoupling the library from next.js
  2. Replacing useReducer state handling with an alternative that is easier to reason about

I think we should discuss the APIs for that before diving into code. For example the frame as an object rather than functions per frame is a discussion we should have via pseudocode before writing this whole PR out - I don't think an object with all frames as render functions is the way to go

Lots of my PR comments are on granular details that may not matter at all if we don't go in this direction - we need to agree on the high level before we dive into writing/reviewing the low levels

clicked: number;
};

function isFrameState(
Copy link
Contributor

Choose a reason for hiding this comment

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

this is a bit of a strange function to have the user implement in the starter.

Copy link
Contributor

Choose a reason for hiding this comment

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

if user needs type safety they should define the type or zod validator and the lib should assert the correct types rather than have the user implement a type validation function like this

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep those are possible solutions. Zod validator is the best I think as it actually validates.


export const framesApp = (
<FramesApp>
<Frame index>
Copy link
Contributor

Choose a reason for hiding this comment

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

I think path="/" is clearer. there could be multiple frame starting points

</>
)}
</Frame>
<Frame path="/next">
Copy link
Contributor

Choose a reason for hiding this comment

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

not a fan of putting many frames in one return, would rather have these be different routes in some way

export const framesApp = (
<FramesApp>
<Frame index>
{({ frameState }) => (
Copy link
Contributor

Choose a reason for hiding this comment

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

not a big fan of render props - think they're awkward and many developers don't use them/don't know how to. It's hard to reason about whats injected and where it's coming from

@@ -0,0 +1,286 @@
import assert from "assert";
Copy link
Contributor

Choose a reason for hiding this comment

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

agree with stephan on this

>
<div
style={{
display: "flex",
Copy link
Contributor

Choose a reason for hiding this comment

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

where is this styling coming from

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

from FrameImage component

},
});
} catch (e) {
return new Response("Internal Server Error", { status: 500 });
Copy link
Contributor

Choose a reason for hiding this comment

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

need meaningful error messages

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yep this is just draft

assert(this.index, "Button index is not set");
assert(this.label, "Button label is not set");

return `
Copy link
Contributor

Choose a reason for hiding this comment

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

these new lines are awkward - same with tabs

@michalkvasnicak
Copy link
Collaborator Author

hm I feel like this PR is rewriting a largely declarative (JSX) definition into some sort of nested imperative library for rendering it. Not a fan of the overall direction.

Feel it would be much cleaner if dev returned json (maybe via jsx syntax, maybe not?), then lib used a zod validator on that json to catch errors. Declarative > Imperative, Functional > Object oriented is my north star here. Feels like we're going the wrong direction on those fronts somewhat.

This PR is intended to solve

  1. Decoupling the library from next.js
  2. Replacing useReducer state handling with an alternative that is easier to reason about

I think we should discuss the APIs for that before diving into code. For example the frame as an object rather than functions per frame is a discussion we should have via pseudocode before writing this whole PR out - I don't think an object with all frames as render functions is the way to go

Lots of my PR comments are on granular details that may not matter at all if we don't go in this direction - we need to agree on the high level before we dive into writing/reviewing the low levels

@davidfurlong I agree with you on this. But each change that is happening is somehow always tied to "how it will be used in nextjs?".

In my opinion it would be great to move away from React if possible as that will allow you to have smaller builds (vercel, lambda, cloudflare). Only code that needs React in some way is @vercel/og although this can be replaced by object definition.

I agree that specifying frame as an object and validating it using zod is another way to go and probably nice as it is also possible to infer types from the schema. So perhaps that would be nice way to build frames app.

// route.ts -- initial frame, handled only by GET request
export async function GET(req) {
  // not a good name for function, something different would be better
   const app = frames(req, initialState);

   return app.frame({
      image: {}, // either React definition or JSX object notation e.g. { type: Component, props: {...}}
      buttons: [
         { action: 'post', label: 'Next', target: '/next-route', state: { clicks: 1 } }
      ]
   }); 
}

export async function POST(req) {
  const app = frames(req, initialState);

  return app.frame({
      image: {}, // either React definition or JSX object notation e.g. { type: Component, props: {...}}
      buttons: [
         { action: 'post', label: 'Next', target: '/next-route', state: { clicks: app.state.clicks + 1 } }
      ]
   }); // req necessary to correctly build the target for `post` button
}

// /next-route/route.ts -- next frame after button click, only POST request
export async function POST(req) {
   const app = frames(req, initialState);

  app.setState() // this could be used for app state `meta fc:frame:state` manipulation? As addition to button provided state

   return app.frame({
      image: {}, // either React definition or JSX object notation e.g. { type: Component, props: {...}}
      buttons: [
         // use app.state here as next state when going back, so we have button specific state
         { action: 'post', label: 'Back', target: '/', state: app.state }
      ]
   });
}

something like this? app.state is weird I know, perhaps app.buttonState would be clearer.

@stephancill
Copy link
Contributor

@michalkvasnicak I think your example is on the right track and we should proceed in this direction

@michalkvasnicak
Copy link
Collaborator Author

@stephancill also frames() function should handle message parsing, state parsing and validation of the message right? We want to make the public API surface minimal so it is easy and fast to build apps.

@stephancill
Copy link
Contributor

@michalkvasnicak that's right. could be configurable via an optional second argument similar to getFrameMessage

@davidfurlong
Copy link
Contributor

Hold up I think theres a bunch of things that are off about this

Base automatically changed from dev to main March 18, 2024 15:38
@stephancill
Copy link
Contributor

closing this in favour of #175

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.

3 participants