Skip to content

Instantly share code, notes, and snippets.

@holyjak
Last active December 2, 2021 10:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save holyjak/9934049f2706d640a3e3eb3053cc7f4a to your computer and use it in GitHub Desktop.
Save holyjak/9934049f2706d640a3e3eb3053cc7f4a to your computer and use it in GitHub Desktop.
Want more from your frontend framework!

Source code for my Telia Full Stack Feast talk "Want more from your frontend framework!" (slides) (6/2020), comparing Redux with REST and a Fulcro with Pathom (Graph API).

Use case we are implementing: Show “hot deals” in your webshop, loaded on-demand.

PS: For the sake of simplicity I am cheating a little and presenting the Fulcro HotDeals component as a root component. If it was used as a child, we would need to either change the a Link Query or make sure that the :deals are presented as a property on the parent component. Also, I use unqualified keys for brevity. This is not recommended in practice.

/** Traditional Redux + REST implementation */
import React from 'react';
import Redux from 'react-redux';
// ################################################# FRONTEND
// FRONTEND 1 - UI: HotDeals.jsx
export default HotDeals = Redux.connect(
(state) => _.pick(state, ["deals", "dealsError", "dealsLoading"]),
{ loadHotDeals } // defined below
)(function HotDeals({deals, dealsError, dealsLoading, loadHotDeals}){
React.useEffect(() => loadHotDeals(), []) // on mount
if (!deals || dealsLoading) return <p>Loading....</p>
if (dealsError) return <p>Something went wrong</p>
return <ul>{deals.map(deal => <Deal {...deal}/>)}</ul>
})
// FRONTEND 2: actions.js
export function loadHotDeals() {
return {
type: "LOAD_HOT_DEALS",
promise: fetchHotDeals()
}
}
// FRONTEND 3: backend-client.js
export function fetchHotDeals() {
return fetch('https://backend/hot-deals')
.then(res => res.json())
}
// FRONTEND 4: reducer.js
//import { handle } from 'redux-pack'; // 1 Promise action -> 4 events
export function reducer(state = myInitialState, action) {
const { type, payload } = action;
switch (type) {
case "LOAD_HOT_DEALS":
// The UI expects deals, dealsLoading, dealsError:
return handle(state, action, {
start: prevState => ({
...prevState,
dealsLoading: true, dealsError: null
}),
finish: prevState =>
({ ...prevState, dealsLoading: false }),
failure: prevState =>
({ ...prevState, dealsError: payload }),
success: prevState =>
({ ...prevState, deals: payload })
});
// ... repeat ∀ data sources ...
}
}
// ################################################## BACKEND
// BACKEND - BUSINESS: webshop.js
async function hotDeals(env) { return ...; }
// BACKEND - PLUMBING: controller.js
router.get('/hot-deals', async(req, res) =>
res.json(await webshop.hotDeals(req.env)));
;; Implementation based on Fulcro and Graph API (via Pathom)
;; ################################################# FRONTEND
;; FRONTEND - all of it: deals.cljs
;; Fulcro calls start with 'f', as in 'ffetch/..'
(def deal-factory (fcomp/factory Deal)) ;; Deal component not shown
;; Syntax: [1, 2, ...] = "array", {:key "value", ...} = map, (something ...) = invoke something (a function, ...)
(defsc HotDeals [this props]
{:query [{:deals (fcomp/get-query Deal)} [ffetch/marker-table :deals-marker]] ; (1) (**)
:use-hooks? true}
(fhooks/use-effect (fn [] (ffetch/load! ; (**)
this :deals Deal ; (2)
{:marker :deals-marker})) ; (**)
[])
;; NOTE: We say we need :deals in the query (1), we get them from props (3),
;; we load! them from the server (2) - all 3 must match (and Fulcro checks it)
(let [marker (get props [ffetch/marker-table :deals-marker])]
(cond
(ffetch/loading? marker) (p "Loading...")
(ffetch/failed? marker) (p "Something went wrong...")
:else (ul (map deal-factory (get props :deals)))))) ; (3)
;; (**) - highlights: 1) declarative data needs; 2) built-in load!; 3) built-in loading/failed tracking
;; ################################################# BACKEND
;; BACKEND - BUSINESS: webshop.clj
(defn hot-deals [env] ...)
;; BACKEND - PLUMBING: graph-api.clj
(pc/defresolver hot-deals [env _]
{::pc/input #{}
::pc/output [{:deals [:deal/id :deal/title ...]}]} ; (4)
{:deals (hot-deals env)})
;; NOTE: The output key :deals, (4) matches the key FE queries for
;; In config:
... (pc/connect-plugin {::pc/register [hot-deals ...]}) ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment