Skip to content

Instantly share code, notes, and snippets.

@redbar0n
Last active January 4, 2021 00:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save redbar0n/796c0fdaa07e19ffb3bc2d4e720193e2 to your computer and use it in GitHub Desktop.
Save redbar0n/796c0fdaa07e19ffb3bc2d4e720193e2 to your computer and use it in GitHub Desktop.
Draft 2 - Alternative SolidJS syntax suggestion.
import { useStore } from "../store";
import ArticlePreview from "./ArticlePreview";
export default ArticleList = props => {
const [{ token }, { unmakeFavorite, makeFavorite }] = useStore(),
handleClickFavorite = (article, e) => {
e.preventDefault();
article.favorited ? unmakeFavorite(slug) : makeFavorite(slug);
},
handlePage = (v, e) => {
e.preventDefault();
props.onSetPage(v);
setTimeout(() => window.scrollTo(0, 0), 200);
};
// Alternative 0 - Current implementation in SolidJS:
// From: https://github.com/ryansolid/solid-realworld/blob/master/src/components/ArticleList.js
//
// Pro: Powerful directives.
// Con: Idiosyncratic, unfamiliar to newcomers.
// Con: Imperative code posing as declarative. for-loops and conditionals are hallmarks of imperative code, they describe HOW. Declarative code purports to describe only WHAT.
// Con: Loses familiarity with React JSX. More importantly: Loses familiarity with HTML.
// Con: Fallback in first line (like <Suspense fallback={...}>) doesn't coincide with when you think about it, since you typically think of the happy path first, and then fallbacks.
// Con: Components do not correspond to what is rendered: Loses correlation with what's seen in the browser → Less intuitive, and less fast to visually "scan".
// Con: Has to support eventual updates/changes to JS (ECMAscript standard). New/other ways of iterating?
// Con: SolidJS becomes more of a framework than a library. Tries to "own more of the world".
// Con: Would need dedicated tooling / syntax highlighting if one wanted to clearly separate SolidJS' imperative looping-constructs from markup.
return (
<Suspense fallback={<div class="article-preview">Loading articles...</div>}>
<For
each={props.articles}
fallback={<div class="article-preview">No articles are here... yet.</div>}
>
{article => (
<ArticlePreview article={article} token={token} onClickFavorite={handleClickFavorite} />
)}
</For>
<Show when={props.totalPagesCount > 1}>
<nav>
<ul class="pagination">
<For each={[...Array(props.totalPagesCount).keys()]}>
{v => (
<li
class="page-item"
classList={{ active: props.currentPage === v }}
onClick={[handlePage, v]}
>
<a class="page-link" href="" textContent={v + 1} />
</li>
)}
</For>
</ul>
</nav>
</Show>
</Suspense>
);
// Alternative 8.1. Refactored to correspond most closely with alternative 0 above, for easier 1:1 comparison.
// Pro's and Con's listed later, since it became a long list. Necessary documentation:
// Solid.createRenderObj() will implicitly create a <></> tag where subsequent render.add() content will go inside.
// Solid.createRenderObj({suspended: true}) will create the equivalent of a wrapper <Suspense></Suspense> tag. It should always have a default fallback of "Loading...".
// ArticlePreview could handle it's own fallback explicitly (the logic should be contained inside itself), in case it is suspended, by rendering instead: <div class="article-preview">No articles are here... yet.</div>
const render = Solid.createRenderObj({suspended: true})
props.articles.forEach(article => {
render.add(<ArticlePreview article={article} token={token} onClickFavorite={handleClickFavorite} />)
})
if (props.totalPagesCount > 1) {
render.add(
<nav>
<ul class="pagination">
{ [...Array(props.totalPagesCount).keys()].forEach(
v => render.add(
<li
class="page-item"
classList={{ active: props.currentPage === v }}
onClick={[handlePage, v]}
>
<a class="page-link" href="" textContent={v + 1} />
</li>
)
)}
</ul>
</nav>
)
}
return render
// Alternative 8.1 (above). Explanation and Pros/Cons:
//
// You can use regular JS iterators, like `.forEach` or `.map` here, because it will only run the first time, and not on every subsequent render.
// Since `Solid.createRenderObj()` with its `.add()` function will have full control over the execution of the rendering.
// The render object can be created once, and then the component disappears so that all that remains is regular DOM nodes (instrumented with reactive bindings, of course).
// NB: Not entirely sure if it would be possible to nest render.add() functions like this. But if function calls like `f(g(x))` where g(x) executes first are possible, it should be possible to execute the innermost render.add() first, and similarly work its way outwards.
//
// Pro: Full power of regular JS available at developer's hands. Knowledge carry-over. Fewer new SolidJS specific concepts to learn → Faster on-ramp. Simply returning an object will feel familiar to working with regular JS functions.
// Pro: Future improvements/changes to JS (ECMAscript standard) won't need to be supported by SolidJS.
// The surface of SolidJS is minimised as far as possible (being more of a library than a framework is good!).
// It doesn't try to "own the whole world", like other compiled approaches do. Echoing Rich Harris' argument against Imba, Marko etc., ref: The truth about Svelte, https://gist.github.com/Rich-Harris/0f910048478c2a6505d1c32185b61934
// Pro: Less escaping {} syntax in the render context, since the default context is the JS context, not the JSX context.
// markup-in-JS > JS-in-markup, since JS is the more powerful context/language, ref Pete Hunt on React here: https://youtu.be/x7cQ3mrcKaY?t=399
// Pro: Makes rendering more fine-grained: not all batched up into one render function. Which is also more in line with how SolidJS actually works.
// Pro: Makes intermittent variable declarations/namings more feasible, leading to clearer code.
// Pro: Syntax highlighting and tooling support already baked in: Will be easier to separate behavior (always just JS) from markup (always just JSX).
// Pro: Creates a stronger impression of the create-once nature of SolidJS components. Rather than the continuously-execute-and-diff nature of React components.
// Con: The flipside of the aforementioned Pro is that the conceptual model of how React components work isn't carried over.
// Although that carry-over would have been an illusion that might break at an unexpected (and typically untimely) time in the future, anyway.
// So making it slightly more apparent up-front might not be a bad thing.
// Con: Might not look as appealing as having everything in a single JSX structure, at first glance.
// Con: Might not be as intuitive to scan, at first glance.
// Con: Having to cognitively context-switch between imperative logic and markup. As opposed to if everything were just "components" like it is now (alternative 0).
// Pro: Imperative logic actually _is_ conceptually different from markup, so a context-switch is a good thing, as it makes the separation clearer.
// Pro: Components are reserved exclusively for the conceptual unit of markup-containing-styling-and-logic-to-render-itself.
// Con: Slightly more dissimilar JSX to React.
// Con: In this example we still mix imperative statements with declarative markup. But unavoidable, to some degree, since JS and HTML needs to interface somehow. Will refactor below, to see if this Con can be alleviated.
// Pro: But at least the imperative code is more apparent, and not posing as components which are declarative in nature.
};
// Alternative 8.2 (below) (alternative 8.1 refactored into independent components, to not intermix imperative statements with declarative markup so much).
// Pro: All the Pro's of 8.1.
// Pro: Eliminated Con's of 8.1:
// - Might not look as appealing as having everything in a single JSX structure, at first glance. → After refactoring the difference in appeal is the same, or better, since logic can now be extracted from control flow components like `<For>` to normal JS context control structures like `for` or `.forEach`.
// - Might not be as intuitive to scan, at first glance. → A single `render.add` containing merely markup affords the same ability to intuitively scan.
// - Having to cognitively context-switch between imperative logic and markup. → No context switching since after refactoring a single component is just one context.
// - Slightly more dissimilar JSX to React. → JSX inside a single `render.add` looks identical to React, plus we avoid the need to use JSX escaping like {} to insert control flow inside React's render-function/return-object.
// - In this example we still mix imperative statements with declarative markup. → Here, in 8.2, we don't mix imperative statements inside any `render.add` where all the declarative markup lives. This fact/benefit could serve as a pointer to refactoring to components like this: getting all the imperative control flow out of the {} brackets will lead to cleaner design (extracted components, less control flow escaping, more straightforward markup inside the components, more flexible component internals as they have full freedom of JS, and more versatile components for potential reuse).
// Pro: Flexibility of being in the JS context by default, not having to escape using {}, and merely using render.add() wherever needed.
// Con: Have to define `const render = Solid.createRenderObj();` in every component. Maybe `render` could be imported from Solid, and whenever used in a function automatically instantiated as a new renderObject?
export default ArticleList = props => {
const render = Solid.createRenderObj({suspended: true})
render.add(
<>
<ArticlePreviews articles={props.articles} />,
<Pages {...props} />
</>
)
return render
})
const ArticlePreviews = props => {
const [{ token }, { unmakeFavorite, makeFavorite }] = useStore();
const handleClickFavorite = (article, e) => {
e.preventDefault();
article.favorited ? unmakeFavorite(slug) : makeFavorite(slug);
};
const render = Solid.createRenderObj();
// What if you could add many fine-grained render primitives to the Solid render object, like this?
// You could keep the powerful logic in plain JS. Instead of emulating powerful logical primitives (iteration, conditionals etc.) in SolidJS.
props.articles.forEach(article =>
render.add(<ArticlePreview article={article} token={token} onClickFavorite={handleClickFavorite} />)
)
return render
}
const Pages = props => {
const render = Solid.createRenderObj()
if (props.totalPagesCount > 1) {
render.add(
<nav>
<ul class="pagination">
<Page {...props} />
</ul>
</nav>
)
}
return render
}
const Page = props => {
const handlePage = (v, e) => {
e.preventDefault();
props.onSetPage(v);
setTimeout(() => window.scrollTo(0, 0), 200);
}
const render = Solid.createRenderObj()
[...Array(props.totalPagesCount).keys()].forEach(
v => render.add(
<li
class="page-item"
classList={{ active: props.currentPage === v }}
onClick={[handlePage, v]}
>
<a class="page-link" href="" textContent={v + 1} />
</li>
)
)
return render
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment