Grateful if you could share your experiences on this. Even if you prefer using Phoenix HTML without seperation between front-end and back-end.
I currently do a hybrid of both, mostly due to business constraints.
I share react code on mobile and my webapp interface, but I reuse the presentational react components in eex templates as well to have some interactivity on the pages that I want to be SSR’d and indexed.
If I was working with only Web, I would just stick to eex everywhere and sprinkling react widgets where I need them.
I sometimes have to use React, but my preference is:
- For most apps: “Sprinkles of JS” (UJS + Turbolinks)
- For very complex front-ends: Vue
When using Vue, I still often do routing from the backend, and make all of or most of the template files a Vue component (as is fairly common among Laravel devs).
For UI frameworks, I’ve worked most with Bootstrap but am getting more interested in Tailwind. Vue Element is also interesting.
Now that LiveView is out, I’ve been experimenting with a more hybrid approach with server-side “components” (i.e. stateless “partial”
~E functions or stateful child
~L Live Views), routing only server-side (regular and live), and sprinkling in presentational react components as needed.
I do think this will eventually work great and speed up development of some apps by removing much of the complexity of API/GQL/client side state but still haven’t figured out how to pass LV hooks (especially
pushEvent callback) into a react tree cleanly. Just waiting for more interop examples in the wild before testing this POC.
Seems a strange way of looking at things.
Phoenix may assemble the HTML/JS/CSS - but once it’s delivered to the browser it’s free to take on a life of its own. Not taking advantage of the separate browser resources is an implementation choice - not a lack of separation.
Lately I’ve really liked using https://materializecss.com.
I use a combination of Phoenix HTML and LiveView. I use Tailwind CSS.
I think this was an obvious point to make, but it seemed clear to me at least that the op was referring to using a more traditional client server full page refresh approach when they said Phoinex HTML. Of course the response could contain any number of JS frameworks
I meant Phoenix with built-in eex HTML templating vs JSON backend with JS driven front-end.
jQuery and Bootstrap 4.3, I gave up on front end rendering for now.
I’ve invested a summer in VueJS 2.5 iirc and it going to change in 3. Ugh. After a few years out of front end and I decided that front end was stable enough… noooope. I did Ember.JS and a little bit of Angular 1 before.
Highly interested in LiveView though, but I’m a one man shop, I’ll wait for it to be better.
For the past two years, I’ve been using Phoenix headless, with GraphQL as a go-between with Nuxt.js (Vue). We use it for Edgewise (https://edgewiserealty.com). I’ve used it on other projects as well. The Phoenix backend and Nuxt presentation layer are deployed separately via Kubernetes (different resource requirements), with assets (JS, CSS, etc.) hosted in a CDN. IMO, this is the “best” stack I’ve come across (all things considered).
I am very optimistic about LiveView and it has a ton of potential, but I think the dev experience of LiveView still has a long way to go before it can go up against something as mature as Vue.js. For instance, I was initially skeptical about Vue’s Single File Components, but I don’t think I can go back now. Side note, I’ve actually spent some time getting LiveView to populate Nuxt pages, which keeps the SFC capability with route splitting JS and CSS. I even called it “Phoenuxt”
It’s still early for LiveView, but I think these more advanced front-end concepts are going to be necessary for LiveView to go beyond the “look what I did without JS” demos. If I get some spare time, I’d like to actually create a “single file component” for LiveView, where the “template” portion is EEX. So there’d actually be 4 sections (EEX template, Elixir code, Vue JS script, style). But I digress…
I agree with the sentiment here and have to say that I think vue is a great technology even for smaller things is usually my default chioce now.
Im curious about your use of graphql here, do you find it more overhead to write than just exposing rest services? Also internally are you still writing rest services and then a further gql layer or does your gql resolvers/mutators directly expose/mutate directly?
Finally, are you using vuex to interact with gql and if so how are you finding it?
I don’t find GQL to be too much overhead. Plus, I think it’s more than worth it. I’ve lost count on how many “REST” APIs I’ve seen that couldn’t even follow basic
collection/resource noun conventions and odd ways of handling authentication (basic, query params,
Bearer, etc). Also, it’s essentially self-documenting via introspection, and you never have to worry about sending too little or bloating the response, or dealing with Swagger / OpenAPI / Postman collections. I usually pair it with Guardian, with a set of plugs that handle the initial header / JWT / resource / blacklist logic. I think the biggest concern is that you could accidentally allow someone to backdoor their way to sensitive information through a maze of querying. Something to watch out for, I suppose. Apollo has been doing some really interesting things with caching and tracing as well.
I initially offered both REST and GraphQL simultaneously, with the REST API sitting on top of the GraphQL layer. So a REST call for
/users/1 would internally call a GQL query, which would then tap into Ecto. GraphQL acts as the “view” for your REST API; which allows your REST logic to be quite thin. It’s not really too much trouble to offer both if your REST API is built on top of your GraphQL API.
Yes, I use vuex. I’m always looking for better ways to do it, but I almost always interact with any API (whether it’s Apollo, Axios, etc.) via the Vuex store actions. That piece is relatively constant. The piece that I go back and forth on, is whether or not to store the state in Vuex or within the components and let the data cascade.
In the former, the stores are loosely tied to a URL struture. So I might have a store called
product which has a
get action. When someone navigates to the
/products/1 page, the
get action is called and populates the state with that product’s data. The components within the page tend to make use of
mapState and computed properties quite a bit. When the user navigates away, the page component is responsible for “resetting” the store’s state (usually on
In the latter, I treat the store like an Elixir module, and essentially ignore the state functionality. The stores are not tied to URLs, and since there is no state, you don’t have to worry about resetting it. So I might have a
product store, with
update, actions. They don’t commit, they simply return the data. Whatever component made the API call is responsible for passing the data down to the child components; which I presume makes fans of dependency injection warm and fuzzy. In that model, you could make the argument that you don’t even need Vuex anymore, but I’d disagree because I still use the root store for session data. I also find that I have way more props on components than I’d like, and sometimes they’re there just as a pass-through to a child component! The elder components end up passing the kitchen sink down just incase a great-great-great grandchild component might need it.
In my opinion, they both have their merits, and I honestly go back and forth between which I prefer. I think Evan would say the latter was for smaller apps, and the former was for bigger apps. There’s a lot of gray area in there though…
Hope that helps.
Thanks for the interesting response I have many similar feelings to you on all this.
I quite like the idea of having a store per “page” with state that lasts as long as the components is mounted but I also understand the uneasiness of putting this in the vuex state in the first place.
Are you making several smaller gql queries for different parts of data in your “page”? Do you still have done kind of loading/loaded/failed indicators in your pages and how do you identify these from a gql queries?
I assume that you also have some date that you keep more long lived that ‘page’ components lifecycle like auth tokens, notifications counter and reference data etc? Are these always pulled from separate queries or somehow parts of your responses are designated to end up in these longer lived state buckets?
Regarding your prop data propogation, I am always unsure about this too. I feel like if the data is present in the store then am I just passing props down to dumb containers for the sake of it.
I completely agree with this in theory so to try and make components as decoupled as possible, but that chain of prop passing never gives me a good feeling if I’m going further than one level down.
Ok that’s interesting because I was more thinking along the lines of a gql resolvers querying a rest endpoint that would retrieve the data and not the reverse.
In some ways does this not remove the benefit of gql as you have a flexible query that is now hidden behind a ridgid resource?
Phoenix and Vue are the “magic stack” for me. Ever since I started using them together two years ago, I haven’t looked back.
For reference, I also dabbled in React for a while, but didn’t find it nearly as “natural” as Vue. IMO it abstracts away too much, and its source code was kind of a mess at the time I read it. I also don’t like that JSX is its go-to default and everything else is kind of a second thought.
Yes. The main page data is fetched server-side for SEO. On mount in the browser, additional below-the-fold content is fetched; which is sometimes handled by the parent, and sometimes delegated to the child components. You need to find the balance on page load performance and SEO. Additionally, you can use
IntersectionObserver to fetch content only as the user is about to scroll that section into view.
Yes. For cascading parent/child/grandchild components, it’s easy enough to have a
loading: true data prop by default in the parent, and then set
loading: false when the data is returned from the store action. It can get a little more tricky if a sibling component wants to know the loading status (eg. a dashboard where multiple panels are displaying the same data differently). Still doable, and I can share how I do it, if you like.
Yes, the session data (user info), and extracted cookie / localStorage data is kept in the root store, as well as actions for logging in, etc… I almost always wrap cookie and localStorage get/set in actions / mutations.
It’s case by case – but as a general rule – things like application settings and user profile data are all stored in the root store. I usually have a root getter called
isGranted that I can use for client-side permissions.
You’d run into trouble pretty quick. GQL is “declarative”, which means the user might query a field that your REST API wouldn’t have returned. So you can wrap your GraphQL in REST, but you really can’t wrap your REST in GraphQL.
Yes, and that’s one of the many reasons why GraphQL is superior to REST – in my opinion. REST is rigid by comparison. Sophisticated REST APIs like Stripe use
expand query params to tell the endpoint to include nested data, but that can only get you so far.
So to offer REST on top of GraphQL, you’re doing it not because it offers more functionality, but rather maintaining legacy API endpoints as you move from REST to GraphQL, or simply providing developer options. Not everyone is comfortable yet with GraphQL.
Just because I didn’t see it mentioned yet, I have to say I love Elm on the front-end, with phoenix on the back-end. I love the type safety and other guarantees Elm gives that a JS or even Typescript framework can’t, and once you’re familiar enough with it, it is a joy to work with.
It seems less mentioned around here than it used to be, but Elm still has a great committed community around it.
I have seen this statement posted a few times on this forum, which makes me ponder due to Elixir lack of type checking. Do you find the lack of type checking is less of an issue with Elixir compared to JS (maybe due to its functional nature or immutability ) ?