I recently decided to break up my color-contrast-chart
NPM package to be less coupled to a specific framework. I split off the core functionality into one package and the React rendering bits into another.
I did this because I knew I would be building a Svelte version of the package to use on my new site whenever I get around to refactoring it with Sapper. Since I was already building two versions I figured I might as well make a third and try out Vue for the first time. (Sorry Angular friends, maybe next time.)
This post is far from an exhaustive tutorial about how to use each framework, but more of a look into how each handles common frontend concerns at a high level. I've been working with React for almost four years, looking into Svelte on and off for about a year, and this will be my first time digging into Vue. I feel pretty confident in the React version, having refactored it several times already, but there's probably room for improvement in my Svelte and Vue implementations. If you have any constructive criticism, I'd absolutely love to hear it either on GitHub or Twitter.
Specifications
In order to make sure I was building the same features into each package, I came up with a short list of specifications. Each component would:
accept a color array to be transformed by the
color-contrast-table
core packagebe able to toggle a set of default styles on but expose a set of usable class names for custom styles
have a pair of props that would be able to toggle the name and value being edited in real time and stored in local state
be able to pass in custom functions for when
names
andvalues
change as well as a function for when inputsblur
I made a table of props and proptypes that each version of the component would need to conform to.
name | type | notes |
---|---|---|
colors | array | see color-contrast-table |
useStyles | boolean | turn on default styles for chart |
editNames | boolean | activate input for color names, does not need to be passed if onNamesChange is provided |
editValues | boolean | activate input for color values, does not need to be passed if onValuesChange is provided |
onNamesChange | function | optional callback to be fired when color name inputs change, will be passed (colorIndex, event.target.value) |
onValueChange | function | optional callback to be fired when color value inputs change, will be passed (colorIndex, event.target.value) |
onInputBlur | function | optional callback to be fired on blur of any input, will be passed (event) |
Scaffolding a New Project
Creating a new project in a modern framework often require setting up complex environments and build setups. Luckily, all three frameworks offer a pretty straightforward ways to get a new project started.
React offers a first party CLI application tool,
create-react-app
. It's definitely a fast and easy way to get a new project going, and comes with Jest tests and Webpack already configured.Once you have the CLI installed, you can run
create-react-app new-project
There's actually a really helpful section in the React documentation suggesting some alternate tools for different scenarios if a full-blown Create React App would be too much overhead.
If TypeScript is your thing, I've heard good things about
TSDX
but have never used it myself.Starting a new Svelte project could not be easier. There's a code snippet to copy and paste from The Svelte homepage that runs a
degit
command to create a Svelte app. It even usesnpx
so it doesn't required that you havedegit
installed.npx degit sveltejs/template new-project
While
degit
is yet another tool built by Svelte's creator Rich Harris, it isn't really a first party Svelte CLI tool. I think, though, that this speaks to how straightforward Svelte is compared to other frameworks. It can install from a simple repo with arollup.config.js
(another Rich Harris tool) and not have to deal with a complex Webpack configuration. This is because all of Svelte's work is done at compile time, so there's no need to transform or polyfill anything for run time in the browser.Vue offers an absolutely amazing first party CLI. To start a new project, run
vue create new-project
Honestly, this tool was magical, especially the
vue serve
command. It was crazy cool to be able to create any.vue
file, then runvue serve
and be able to view just that file being built and served in the browser.
Dev Tools
There isn't a ton to say about this, other than that all three frameworks have browser extension dev tools. I would highly recommend downloading them if you're doing any work with a particular framework. They help peer into component structure and data in more specific ways that a browser's normal inspect tools. This can save a lot of time when trying to diagnose the cause of an issue, plus it gives you some extra power to poke around in other people's websites and see how they're using a framework.
Components & Template Engines
Components are small pieces of reusable code that combine to build up complex applications, which is called "component composition." Often they accept data from their parent components and display or transform it in some way. Most modern JavaScript frameworks refer to passed data as "props," short for properties.
The most simple component in my color-chart is the <Color>
component. This component needs to look at a color
object which will contain a name
, value
, score
, and contrast ratio
.
There are two ways to create components in React: class components and function components. For the sake of simplicity in this post I'll be referring to function components. The React team is adamant that class components are not being deprecated and are still a totally valid way to approach React.
A React component is literally just a function that is exportd from a
.js
or.jsx
file. Attributes that are passed to a component can be accessed with theprops
argument of a function component.The
React
package itself will need to be imported first thing in any React component file.// color.jsx import React from 'react' const Color = (props) => { return { <div> <span> {props.color.score} </span> </div> } } export default Color
It's not required to name this function, but doing so will make debugging a lot easier than
export default () => {...}
will.While technically not a requirement, the large majority of React projects use JSX as the template tool. JSX is not only for React, but I've never seen one used without the other.
JSX works mostly like HTML, but with ability to interpolate Javascript by wrapping it in curly brackets (
{ }
). This syntax can run expressions or print values, such as those from theprops
object . String attributes can be passed using quotation marks, but anything else will need to use theprop={value}
bracket syntax.This particular component is self-contained, but if it needed to wrap and render another component it could use React's special
props.children
value. This value will be automatically added to any component that contains children and doesn't need to be explicity passed anywhere.// label.jsx import React from 'react' const Label = (props) => { return { <span> {props.children} </span> } } export default Label
A component can be imported into another component's file with an import statement and then used like any other element.
// color.jsx import React from 'react' import Label from './label.jsx' const Color = (props) => { return { <div> <Label> {props.color.score} </Label> </div> } } export default Color
A Svelte component is a special type of file with the
.svelte
extension that lets the Svelte compiler know it needs to be treated in a certain way. Behind the scenes, Svelte components are actually JavaScript classes, but writing them is a lot like writing regular HTML. JavaScript is contained within<script>
tags, CSS within<style>
tags, and everything else on the page will be rendered as HTML. A Svelte component can consist of one or more of these three sections.// color.svelte <script> export let color </script> <div> <span> {color.score} </span> </div>
A few things are immediately different from React. The component is automatically named and exported based on the name of the file. Rather than be passed an a
props
object, values must be created with theexport let
syntax. Sort of a running theme for Svelte, the syntax seems weird at first but makes sense the more you think about it. Think of it as declaring a reassignable variable, and exporting it so that it can be overwritten by other components.Similarly to JSX, Svelte uses curly brackets to escape HTML and interpolate JavaScript values declared in the
<script>
tag. Unlike JSX, anything that is correct in HTML is also valid in a Svelte template. Svelte describes itself as as "superset" of HTML, so "any valid HTML is valid Svelte."Component composition works slightly different as well. Rather than passing children via props, Svelte uses a special tag called a
<slot/>
.<slot>
elements are functionally very different than JSX'sprops.children
, but at a basic level they should work mostly the same.// label.svelte <span> <slot /> </span>
Components can be imported within the
<script>
section of other components and used as custom HTML tags.// color.svelte <script> export let color import Label from './label.svelte' </script> <div> <Label> {color.score} </Label> </div>
Vue, like React, has multiple ways to structure a component file. I don't have enough experience to speak to the pros and cons of each, but I chose to write all my components using the single file component approach.
Apparently you can use JSX with Vue, but none of the Vue developers I know have ever mentioned doing this. By default, Vue comes with its own template syntax that is similar to Svelte's.
// color.vue <script> export default { name: 'Color', props: { color } } </script> <template> <div> <span> {{ this.color.score }} </span> </div> </template>
Just like Svelte, JavaScript in a Vue component lives inside a
<script>
tag at the top of the file. Unlike Svelte, this code is required for a Vue component, and must contain a structureddefault export
object. Vue components instantiate JavaScript classes, and Vue forces you to be declarative when defining a component's dependcies. This makes sense if you think about writing theconstructor()
function of a regular class.The
name
key corresponds to how the component will be referenced in other files. All the props need to be explicitly added to theprops
object. Once properties are assigned to the Vue component class, they can be used in the HTML by using thethis.
syntax.this
in JavaScript can be kind of a hard thing to understand, but for this scenario think about it as "this component."Vue also needs to return a
<template>
tag in each component to register what HTML will be rendered. Interpolatin JavaScript in HTML uses "Mustache" syntax, or double curly brackets ({{ }}
).For composing components, Vue also uses the
<slot>
tag.// label.vue <script> export default { name: 'Label' } </script> <template> <span> <slot /> </span> </template>
Like
props
, any imported components will need to be registered inside the<script>
tag. After that, they can be used inside the<template>
as custom elements.// color.vue <script> import Label from './label.vue' export default { name: 'Color', props: { color }, components: { Label } } </script> <template> <div> <Label> {{ this.color.score }} </Label> </div> </template>
Passing Props and Checking Types
props
, like I briefly mentioned before, are how modern JavaScript frameworks pass data around. For the most part props "flow down," meaning that they are passed from parent to child and not in the other direction.
In the color chart, each <Color>
component needs to get a certain set of color data from its parent <Row>
component. That data looks like this:
color: {
name: 'purple', // the color's name
value: '#800080' // the color's hex value,
score: 'AAA', // whether or not the color passes contrast with the row
ratio: 1.5 // the actual contrast with the current row
}
There are three main ways data can be passed: as individual attributes; bundled up as an object containing multiple attributes that can be unpacked later; or using some variation of the spread funtion to unpack the data before it makes it into the component.
The good news is that all three frameworks also have tools to add types to help manage a component's expectations around props data.
In React,
props
are immutable data passed into child components. This means that unless you go out of your way to allow changes, what you pass into a component will be exactly what the component will always render. Props are passed using regular HTML attribute syntax for strings, and the curly brackets syntax for any other type of value.// row.jsx import React from 'react' import Color from './color.jsx' const Row = (props) => { return ( <tr> <Color name={props.color.name} value={props.color.value} score={props.color.score} ratio={props.color.ratio} /> </tr> ) } export default Row
To ensure that the
<Color>
component knows what type to expect for each prop, React requires the PropTypes addon. This used to be part of the core React package, but was split into its own dependency since using it is optional. It now needs to be separately imported into any file it is used in.A component's
.propTypes
are assigned in the same file, after the main body of the component.// color.jsx import React from 'react' import PropTypes from 'prop-types' const Color = (props) => { return { <div title={`Ratio ${props.ratio}`}> <span> {props.score} </span> </div> } } Color.propTypes = { name: PropTypes.string, value: PropTypes.string, score: PropTypes.string, ratio: PropTypes.number } export default Color
To make this more concise, the entire
color
object can be passed from<Row>
to<Color>
, then its attributes can be accessed inside the<Color>
component.// row.jsx import React from 'react' import Color from './color.jsx' const Row = (props) => { return ( <tr> <Color color={props.color} /> </tr> ) } export default Row
This means an update in the Color's PropTypes. It's possible to deep check the shape of an object, but for the sake of simplicity let's just check that it is an object.
// color.jsx import React from 'react' import PropTypes from 'prop-types' const Color = (props) => { return { <div title={`Ratio ${props.color.ratio}`}> <span> {props.color.score} </span> </div> } } Color.propTypes = { color: PropTypes.object } export default Color
The last way to pass props is to take advantage of the fact that they themselves are
object
type and use the ...spread operator. Instead of passing the bundled object as one value, the spread operator will take each attribute and pass it individually. This is a particularly helpful tool when a parent is passed props that in turn need to all be passed through to a child. Rather than be set to the value of an attribute, this syntax is applied directly to the component.// row.jsx import React from 'react' import Color from './color.jsx' const Row = (props) => { return ( <tr> <Color {...props.color} /> </tr> ) } export default Row
Because each attribute was passed unbundled from the single
color
object, the type of each individual prop can be checked.// color.jsx import React from 'react' import PropTypes from 'prop-types' const Color = (props) => { return { <div title={`Ratio ${props.ratio}`}> <span> {props.score} </span> </div> } } Color.propTypes = { name: PropTypes.string, value: PropTypes.string, score: PropTypes.string, ratio: PropTypes.number } export default Color
Svelte
props
work similar to Reactprops
. They have nearly the same syntax, with quotation marks for strings and curly brackets for other values, but can also accept any keywords that work in regular HTML.// row.svelte <script> export let color import Color from './color.svelte' </script> <tr> <Color name={color.name} value={color.value} score={color.score} ratio={color.ratio} /> </tr>
Unlike React, Svelte comes built-in with an easy-to-opt-into typing system. When defining a
prop
in a child component using theexport let
syntax, a default value can be set. Svelte will then throw a warning when a new value is assigned that does not match that original type. To opt out of this either don't set a default or explicitly assign itundefined
.// color.svelte <script> export let name = '' export let value = '' export let score = '' export let ratio = 0 </script> <div title={`Ratio ${ratio}`}> <span> {score} </span> </div>
Svelte can also pass an entire object as one attribute. One cool thing Svelte offers is the ability to use a shorthand to pass a prop if its name and value are the same. Instead of setting an attribute equal to a value, the value can be wrapped with curly bracket and added directly on an element.
// row.svelte <script> export let color = {} import Color from './color.svelte' </script> <tr> // long form <Color color={color} /> // shorthand <Color {color} /> </tr>
These values can then be accessed off of the object in the child component.
// color.svelte <script> export let color = {} </script> <div title={`Ratio ${color.ratio}`}> <span> {color.score} </span> </div>
It may seem like all individually defined and exported props are not automatically grouped into a single
props
object, but they actually are. This comes with the caveat that this syntax is best used sparingly.$$props references all props that are passed to a component – including ones that are not declared with export. It is useful in rare cases, but not generally recommended, as it is difficult for Svelte to optimise.
The same spread operator syntax can be used to pass all props into a child.
// row.svelte <script> export let color = {} import Color from './color.svelte' </script> <tr> <Color {...$$props} /> </tr>
In the child component the exported
props
will need to correspond to whatever was also passed to the parent. In this example, it would be acolor
object.// color.svelte <script> export let color = {} </script> <div title={`Ratio ${color.ratio}`}> <span> {color.score} </span> </div>
Passing props in Vue works mostly like React and Svelte, but with a few syntactical differences. Firstly, prop attributes have to be "kebab-case". Vue props will also always use the double quotation (
" "
) syntax to declare all types of props, not just strings. Vue also makes a distinction between static props and dynamic props, the latter of which are prepended withv-bind:
. Since the color chart will sometimes update when a user interacts with it, this is the syntax to use in this situation.// row.vue <script> import Color from './color.vue' export default { name: 'Row', components: { Color }, props: { color }, } </script> <template> <tr> <Color v-bind:name="this.color.name" v-bind:value="this.color.value" v-bind:score="this.color.score" v-bind:ratio="this.color.ratio" /> </tr> </template>
Vue, like Svelte, comes with a typing system that only requires a small amount of additional syntax. When registering
props
to a component, each prop can be assigned a value to define a type associated with each key.// color.vue <script> export default { name: 'Color', props: { name: String, value: String, score: String, ratio: Number } } </script> <template> <td> <div title="`Ratio ${this.ratio}`"> <span> {{ this.score }} </span> </div> </td> </template>
Just like the other frameworks, objects can be passed down as props as well. Like Svelte, Vue offers a shorthand for writing out
v-bind:
.v-bind
can be left off and dynamic props can be prepended with just:
.// row.vue <script> import Color from './color.vue' export default { name: 'Row', components: { Color }, props: { color: Object }, } </script> <template> <tr> // long form <Color v-bind:color="this.color" /> // shorthand <Color :color="this.color" /> </tr> </template>
As you would probably expect, those values can be accessed via the
color
object prop.// color.vue <script> export default { name: 'Color', props: { color: Object } } </script> <template> <td> <div title="`Ratio ${this.color.ratio}`"> <span> {{ this.color.score }} </span> </div> </td> </template>
Similar to Svelte, Vue also secretly bundles all of a components registered prop values into an object called
$props
, which can be referenced elsewhere in the component asthis.$props
. Unlike Svelte and React, however, there isn't really a way to spread this value into a child component and it is made more for referencing passed in values elsewhere in the<script>
tag.
Inline Javascript and Styles
Sometimes, in order to make components more dynamic, values need to be set locally to each instance of a component. This is often useful to do within the render body of the component if the output will end up directly in the DOM. These DOM side effects often involve manipulating classes for CSS, or even adding inline CSS directly onto elements.
For the color chart, the easiest way to style each block was to add an inline background-color
to each <Color>
component. Also, each block runs a small JavaScript function to determine whether to use black or white text.
Using a conditional CSS class is how I was able to scope the global style sheet for the entire chart. A user can turn styles off by not passing the useStyles
top level prop.
I'll be using Block Element Modifier style classes for this section, so if these look odd read up on what all the dashes and underscores mean.
React does its best to be unopinionated when it comes to styles. JSX uses the
className
attribute as an analog to the regular HTMLclass
. Any string, or expression that evaluates to a string, can be used and will end up in the DOM and can be hooked up to an external style sheet.// color.jsx import React from 'react' const Color = (props) => { return ( <td className='color-contrast-table__color'> <div className='color-contrast-table__color-block'> <span className='color-contrast-table__color-score'> {props.color.score} </span> </div> </td> ) } export default Color
React also allows for inline styles, but the syntax is different than regular HTML. The
style
attribute accepts an object, and as such the keys need to be camelCase strings rathan than normal CSS properties that contain hyphens. JavaScript values can be used directly in this object.// color.jsx import React from 'react' const Color = (props) => { return ( <td> <div style={{ backgroundColor: props.color.value }}> <span> {props.color.score} </span> </div> </td> ) } export default Color
The last built-in way to work with styles in React is similar to inline styles, but is useful for styles that don't need to be attached to a specific DOM element. React's (hilariously named)
dangerouslySetInnerHTML
attribute can be combined with a<style>
tag to create an on-page style sheet.// styles.jsx import React from 'react' const Styles = () => { return ( <style dangerouslySetInnerHTML={{__html: ` .color-contrast-table ... } `}} /> ) } export default Styles
Since JSX is "just javascript," running inline method doesn't require any special work. A function can be imported from another file, and then used anywhere in the body of the component or its returned JSX.
// color.jsx import React from 'react' import getLabelColor from './get-label-color.js' const Color = (props) => { return ( <td> <div style={{ backgroundColor: props.color.value, color: getLabelColor(props.color.value) }} > <span> {props.color.score} </span> </div> </td> ) } export default Color
More complex functions can even be defined as nested functions within a component before the return of the component's JSX. To hook up the
useStyle
prop I used this strategy to conditionally add a top levelclassName
and then scoped my<style>
tag accordingly.// table.jsx const Table = (props) => { function getClassName() { if (props.useStyles) { return 'use-styles color-contrast-table' } else { return 'color-contrast-table' } } return ( <table className={getClassName()}> <tbody> ... </tbody> </table> ) } export default Table
Svelte, striving to be as close to HTML as possible, lets you use regular HTML class attribute syntax.
// color.svelte <script> export let color </script> <td class='color-contrast-table__color'> <div class='color-contrast-table__color-block'> <span class='color-contrast-table__color-score'> {color.score} </span> </div> </td>
Since "valid HTML is valid Svelte," a regular
style
attribute can be used to create inline styles. This isn't very well documented, but Svelte can also interpolate JavaScript values within this attribute with the curly bracket syntax.// color.svelte <script> export let color </script> <td> <div style='background-color: {color.value};'> <span> {color.score} </span> </div> </td>
Svelte also comes with some pretty powerful built-in style tools. In addition to being able to define a
<script>
tag at the top of a.svelte
file, you can define a<style>
tag as well. The Svelte compiler will generate unique classes that will encapsulate styles to only effect elements within this component file. This means that styles won't naturally cascade down to child components, unless the style rule is wrapped with the:global()
modifier.// styles.svelte <style> :global(.color-contrast-table) { ... } </style>
Because the
<script>
and HTML sections of a component are optional, this creates a global<Style />
component I can include near the top level of my color chart.Since the
<script>
section of a component will run any valid JavaScript, component functions can be imported and run here. Variables can also be created and used in the HTML body with the same bracket syntax asprops
values.// color.svelte <script> export let color import getLabelColor from './get-label-color.js' const textColor = getLabelColor(color.value) </script> <td> <div style=' background-color: {color.value}; color: {textColor}; '> <span> {color.score} </span> </div> </td>
Just like React, methods can be called inline using curly brackets. I used the same scoping method combined with a
:global()
modifier to toggle the styles for the chart.// table.svelte <script> export let colors export let useStyles function getClassName() { if (useStyles) { return 'use-styles color-contrast-table' } else { return 'color-contrast-table' } } </script> <table class='{useStyles()}'> <tbody> ... </tbody> </table>
One extra thing to note here when applying this logic specifically to classes is that Svelte also comes with a
class:
directive that makes conditionally rendering classes easier. Whatever directly follows the:
will be added to the element's class list if the prop is truthy.// table.svelte <script> export let colors export let useStyles </script> <table class='color-contrast-table' class:use-styles={useStyles} > <tbody> ... </tbody> </table>
Vue, just like Svelte, uses the same class attribute syntax as regular HTML.
// color.vue <script> export default { name: 'Color', props: { color } } </script> <template> <td class='color-contrast-table__color'> <div class='color-contrast-table__color-block'> <span class='color-contrast-table__color-score'> {{ color.score }} </span> </div> </td> </template>
Vue's inline style attribute, however, is a little closer to React's. Vue uses the
v-bind:style
syntax and needs to be passed an object. It can use regular, hyphenated CSS properties as keys as long as they're surrounded with single quotation marks (' '
).Any attributes that need access to interpolated values needs to use a Vue directives. A directive is a special kdinf of attribute, usually prefixed with "v-" that lets Vue know it needs to interpolate that attribute in a special way.
// color.vue <script> export default { name: 'Color', props: { color } } </script> <template> <td> <div v-bind:style="{ 'background-color': color.value }"> <span> {{ color.score }} </span> </div> </td> </template>
One awesome benefit of using a Vue single file component is the ability to add a
<style>
tag to a component in addition to a<script>
tag. This works a lot like the Svelte<style>
tag, but is more configurable. By default the styles will work like a global CSS stylesheet, but the tag itself can accept ascoped
attribute. This will add generated classes and encapsulate the styles similar to Svelte.// styles.vue <script> export default { name: 'Styles', } </script> <style> .color-contrast-table { ... } </style> <template> <div></div> </template>
One thing worth mentioning here is that since a Vue component must contain a
<template>
tag and that tag must contain an element, you will have to put some random DOM element here. If that bothers you (it bothered me), there is an NPM package calledvue-fragment
that will allow you to return a ghost element that won't actually render anything.When it comes to using methods inside of components, Vue requires a bit more structure than the other two frameworks.
To define a value in the
<script>
section and use that value in the component body, it must be registered like other imports. In a single file component,data
needs to be a function that returns an object of data values. If thatdata
value is going to be defined as the result of an expression, that function needs to also be registered to themethods
key of the Vue object.// color.vue <script> import getLabelColor from './get-label-color.js' export default { name: 'Color', props: { color }, methods: { getLabelColor }, data: function() { return { textColor: this.getLabelColor(this.color.value) } } } </script> <template> <td> <div v-bind:style="{ 'background-color': color.value, 'color': this.textColor }" > <span> {{ color.score}} </span> </div> </td> </template>
Methods can also be defined as values inside the
methods
object, and can be run directly within the<template>
section.// table.vue <script> export default { name: 'Table', props: { colors, useStyles }, methods: { getClassName: function() { if (this.useStyles) { return 'use-styles color-contrast-table' } else { return 'color-contrast-table' } } } } </script> <template> <table v-bind:class="getClassName()"> <tbody> ... </tbody> </table> </template>
For methods that determine which classes are added to an element, Vue allows for binding a directive to the
class
attribute. You can pass an object to a bound class attribute, and if the value returns truthy the key will be added to the element's class list.// table.vue <script> export default { name: 'Table', props: { colors, useStyles } } </script> <template> <table v-bind:class="{ 'use-styles': useStyles }"> <tbody> ... </tbody> </table> </template>
Loops and Conditional Rendering
Moving up one level from the <Color>
component in the color charts is the Row component. Each row contains two things: a collection of generated <Color>
components, and a <Header>
element that needs to know whether to show regular text or editable inputs.
Each Row will get props
data that looks like this:
{
color: {
name: 'purple', // the name of this color
value: '#800080', // the value of this color
combinationScores: [...], // a list of data about the other colors
},
editNames: false, // whether the name should show text or an input
editValues: false // whether the value should show text or an input
}
The biggest difference between JSX and any other templating system I've used is highlighted in the first paragraph of the JSX documentation.
JSX may remind you of a template language, but it comes with the full power of JavaScript.
This means that rather than learning JSX specific ways to programmatically create markup, React relies on directly using JavaScript methods. To render a list of items, an array of data can be looped through with a
.map
or a.forEach
Array
method.// row.jsx import React from 'react' import Color from './color.jsx' import Header from './header.jsx' const Row = (props) => { return ( <tr> <Header editNames={props.editNames} editValues={props.editValues} /> {props.color.combinationScores.map((color, index) => { return ( <Color key={index} color={color} /> ) })} </tr> ) } export default Row
One particular quirk to note here is that when rendering elements any list of elements React requires each to have a unqiue
key
attribute. This can be anything — like the elementsname
value — but its often easiest to just use the array index. This helps React keep track of items during re-renders.Another gotcha of JSX is that despite being "just JavaScript,"
if ()
statements don't work inside the return of a render method. Because of this JSX code is more likely to use either the "short circuit" or ternary operators.The "short circuit" is more of a design pattern and less of an actual operator, but it can be read as "if the data exists, show the HTML; if not, stop and show nothing."
If the row component needed to only show
<Color>
components for passing color combinations, it might look like this:{color.score !== 'fail' && <Color color={color} /> }
The ternary operator is used when one element is needed in one scenario and another in a different scenario, such as in the Header of each row.
// header.jsx import React from 'react' const Header = (props) => { return ( <th> {props.editNames ? <input type='text' value={props.color.name} /> : <span>{props.color.name}</span> } {props.editValues ? <input type='color' value={props.color.value} /> : <span>{props.color.value}</span> } </th> ) } export default Header
A ternary, especially when intermixed with markup, can look confusing at first, but it functions essentially the same as an
if...else
statement. If the expression before the?
is truthy, the first condition before the:
will execute. If not, default to the second condition after the:
.Svelte comes with its own built in template syntax. To run logic you'll need to hook into their specific syntax, using
#
to start an expression,:
to continue the previous expression, and/
to end.To render a list of elements from an
array
of data, Svelte has several way to run an each loop.// row.svelte <script> export let color export let editNames export let editValues import Color from './color.svelte' import Header from './header.svelte' </script> <tr> <Header editNames={editNames} editValues={editValues} /> {#each color.combinationScores as color} <Color color={color} /> {/each} </tr>
A nice thing about Svelte templates over JSX is that they are a little smarter, so there's no need to manually add a key to elements (although you can if you ever need to).
Svelte also provides blocks for
if
type statements so there's no need to use something like the short circuit or the ternary.An
if
block wraps some HTML that will only be returned if the condition returns true. To only show blocks with a passing score:{#if color.score !== 'fail'} <Color color={color} /> {/if}
An
if
statement can also have an:else
clause, providing a default that will be rendered if the conditional is evaluated as false.// header.svelte <script> export let editNames export let editValues </script> <th> {#if editNames} <input type='text' value={color.name} /> {:else} <span>{color.name}</span> {/if} {#if editValues} <input type='color' value={color.value} /> {:else} <span>{color.value}</span> {/if} </th>
Vue templates also come with a built in logic system, but rather than using custom tags it uses directives.
Vue's template directive attributes are interesting because they can be applied directly onto a tag, or can be applied to a wrapping
<template>
element that will function as a "rootless" element during render.// row.vue <script> import Header from './header.vue' import Color from './color.vue' export default { name: 'Row', components: { Header, Color }, props: { color }, } </script> <template> <tr> <Header :editNames="editNames" :editValues="editValues" /> // directive directly on element <Color v-for="(color, index) in color.combinationScores" :key="index" :color="color" /> // with wrapping element <template v-for="(color, index) in color.combinationScores"> <Color :key="index" :color="color" /> </template> </tr> </template>
Vue also has both
v-if
andv-else
directives that work how you would expect. Just like withv-for
these can be applied on a wrapping<template>
or directly to an element.// header.vue <script> export default { name: 'Header', props: { editNames, editValues }, } </script> <template> // directive directly on element <th> <input v-if="editNames" type='text' v-bind:value="color.name" /> <span v-else>{{ color.name }}</span> <input v-if="editValues" type='color' v-bind:value="color.value" /> <span v-else>{{ color.value }}</span> </th> // with wrapping element <th> <template v-if="editNames"> <input type='text' v-bind:value="color.name" /> </template> <template v-else> <span >{{ color.name }}</span> </template> <template v-if="editValues"> <input type='color' v-bind:value="color.value" /> </template> <template v-else> <span >{{ color.value }}</span> </template> </th> </template>
Vue also comes with one extra operator that the other two frameworks don't include —
v-show
.v-show
works visually just likev-if
, but instead of not rendering an element it still renders the markup, but uses CSS to hide it from the DOM.// directive directly on element <Color v-show="color.score !== 'fail'" color={color} /> // with wrapping element <template color.score !== 'fail'> <Color color={color} /> </template>
This could be accomplished in React or Svelte as well, but it's nice of Vue to provide such an easy syntax for it.
State and Events
If props are one half of modern frameworks, state is the other. Props are data passed into components, state represents values internal to the component at render time. State is often updated in response to actions that a user takes. State can even be derived from an initial set of props
passed into a component as a starting state.
Each component can hold it's own state, and pass it down into child components via props, but often the top level component for a system will maintain all the state and disperse it down the tree. All three frameworks contain mechanisms to re-render a component if its props
or state
change. If a user takes an action, the top level component will be notified to update its state, and then let the children know they needs to re-render with new data.
Custom events tied to different inputs and user actions are used to update state.
React has a very complicated state system for class components that involves binding values and updater functions to a component's
this
. Since I've only been talking about function components it makes more sense to look at theuseState
hook and not thesetState
function. Hooks can be a confusing concept at first, especially if you're not already familiar with all of the class lifecycle methods, but try to think of them as importable functions that add ability for components to do more complex actions than just render JSX.Any of the built-in React hooks can be imported as a named import from the
'react'
package, or they can be accessed directly off theReact
default import, likeReact.useState()
.The
useState
hook is a function that can accept an argument to use as its initial state. It will return two values: the state value and a function to update that state value. These two values are usually written with the array destructure assignment syntax, and will mostly follow thevalueName
,setValueName
naming convention.// chart.js import React, { useState } from 'react' import generateChart from 'color-contrast-table' const Chart = (props) => { const [colors, setColors] = useState(props.colors) const colorChart = generateChart(colors) return ( <Table colors={colorChart} /> ) } export default Chart
Calling
setColor(newColors)
would cause the value ofcolors
to change and cause a re-render of the<Chart>
component as well as any children that depend on thecolors
prop. ThesetColors
function can even be passed down as a prop itself and called within a child component. State setting functions in class components would need to have theirthis
value bound in the class constructor, but because arrow function components don't reassignthis
a state updater can be passed and called wherever without worrying about binding.Because this function needs to be called on the change event of an input, which will only return one value, the function needs to do a little more work to get only that new value into the existing
colors
object. Once that is worked out, the newcolors
object can be set using thesetColor
updater.// chart.js import React, { useState } from 'react' import generateChart from 'color-contrast-table' const Chart = (props) => { const [colors, setColors] = useState(props.colors) const onNamesChange = (index, value) => { const newColors = [...colors] newColors[index].name = value setColors(newColors) } const onValuesChange = (index, value) => { const newColors = [...colors] newColors[index].value = value setColors(newColors) } const colorChart = generateChart(colors) return ( <Table colors={colorChart} onNamesChange={onNamesChange} onValuesChange={onValuesChange} /> ) } export default Chart // header.jsx import React from 'react' const Header = (props) => { return ( <th> <input type='text' value={props.color.name} onChange={event => props.onNamesChange(props.index, event.target.value)} /> <input type='color' value={props.color.value} onChange={event => props.onValuesChange(props.index, event.target.value)} /> </th> ) } export default Header
The most important concept when dealing with state in React is that state objects are immutable and should always be set using one of React's updaters and never reassigned directly.
// don't do this! const [colors] = useState(props.colors) const onNamesChange = (index, value) => { colors[index].name = value }
Svelte is very interesting when it comes to stateful logic. One of the main tenets that differentiates Svelte from other frontend systems is that all assignments are reactive. Svelte "hijacks" the assignment operator (=) and uses it to trigger state changes and re-renders within Svelte apps.
Svelte also comes with a built-in method to make anything reactive by prefacing it with
$:
. This is another one of those "looks weird but is actually valid JavaScript" things from Svelte, this time taking advantage of a JavaScriptlabel
.All of that to say, simply by declaring a prop, a Svelte app is already stateful. State, whether just a reactive prop or a labeled reactive statement, can be passed down to child components like any other type of data.
// table.svelte <script> export let colors import generateChart from 'color-contrast-table' import Table from './table.svelte' $: colors = generateChart(colors) </script> <Table {colors} />
Updating stateful values in Svlete can be handled in nearly the same way as in React, where an updater function is passed down into a child, and the return will set a new value in the parent. However, Svelte also offers something very different called a
store
.A
store
is a place to put data outside of the normal component hierarchy. They provide methods tosubscribe
,update
andset
their data. Svelte stores come in several varieties, but the one that makes the most sense to use here is awritable
, meaning that we can both read and write data to it.// stores.js import { writable } from 'svelte/store' export const colorArray = writable()
Then, in the
<Chart>
component, thecolorArray
store can be imported,set
withprops
data, subscribed to, and passed down.// chart.svelte <script> export let colors import generateChart from 'color-contrast-table' import Table from './table.svelte' import { colorArray } from './stores.js' colorArray.set(colors) colorArray.subscribe(colorArray => colors = colorArray) $: colors = generateChart(colors) </script> <Table {colors} />
This doesn't change much in the
<Chart>
component itself, but what it does allow for is direct access to update thecolorArray
store without having to pass functions. Instead, the<Header>
component can access the store directly, call itsupdate
method, and the rest of the components will be made aware of this change via thesubscribe
method in<Chart>
. Store methods can be composed in other functions that will sort out updating a single object property, then can be bound to input events using Svelte's element directives.// header.svelte <script> export let color export let index import { colorArray } from './stores.js' const onNamesChange = (index, value) => { colorArray.update(colorArray => { const newColors = [...colorArray] newColors[index].name = value return newColors } }) const onValuesChange = (index, value) => { colorArray.update(colorArray => { const newColors = [...colorArray] newColors[index].value = value return newColors } }) </script> <th> <input type='text' value={color.name} on:input={event => onNamesChange(index, event.target.value)} /> <input type='color' value={color.value} on:input={event => onValuesChange(index, event.target.value)} /> </th>
Vue has two state-like concepts: computed and watched properties. A
computed
property is one that is calculated based on some other data in the app and will remain cached after updating. Awatch
property is like acomputed
property that requires a more custom implementation for how the data changing should be handled. The Vue docs recommend usingcomputed
overwatch
for most scenarios that don't involve asynchronous data, so that seemed like the right option for me.// chart.vue <script> import generateChart from 'color-contrast-table' import Table from './table.vue' export default { name: 'Chart', components: { Table }, props: { colors }, methods: { generateChart, }, data() { return { colorArray: this.$props.colors } }, computed: { colorChart: function () { return this.generateChart(this.colorArray) } } } </script> <template> <Fragment> <Styles/> <Table v-bind:colors="colorChart" /> </Fragment> </template>
It feels slightly extraneous to have to assign a
data.colorArray
value and pass it through tocomputed.colorChart
, but since Vueprops
are also immutable this is necessary.props.colors
can't be changed, butdata.colorArray
can, and whenever it doescomputed.colorChart
will also change and cause a re-render.Vue uses an event-driven system to update state. Rather than having a function be passed down and bound to the change event of an input, an event is emitted by the input and then "caught" by a parent component. Custom events are defined by using the
v-on:
syntax, and are then passed an$emit()
function. The first argument of$emit()
is the name of the event to be emitted and the rest will be passed into the function when the event is caught.// header.vue <script> import getLabelColor from './get-label-color.js' import EditName from './edit-name.vue' import EditValue from './edit-value.vue' export default { name: 'Header', props: { color, index, }, methods: { getLabelColor, namechange(index, value) { this.$emit('namechange', index, value) }, valuechange(index, value) { this.$emit('valuechange', index, value) } } } </script> <template> <th> <input type='text' :value="value" v-on:input="$emit('namechange', index, $event.target.value)" /> <input type='color' :value="value" v-on:input="$emit('valuechange', index, $event.target.value)" /> </th> </template>
These events can be caught and passed upwards so that a deeply nested child can update a value higher than its direct parent. For less typing, the
@
symbol can be used as a shorthand forv-on
.// header.vue methods: { namechange(index, value) { this.$emit('namechange', index, value) }, valuechange(index, value) { this.$emit('valuechange', index, value) }, } ... <input type='text' :value="color.name" @input="$emit('namechange', index, $event.target.value)" /> <input type='color' :value="color.value" @input="$emit('valuechange', index, $event.target.value)" /> // other components in between methods: { namechange(index, value) { this.$emit('namechange', index, value) }, valuechange(index, value) { this.$emit('valuechange', index, value) }, } ... <Header @namechange="namechange" @valuechange="valuechange" /> //chart .vue methods: { namechange: function (index, value) { ... } valuechange: function (index, value) { ... } ... <Table @namechange="namechange" @valuechange="valuechange" > }
This might look like props being passed down, but this data flow is actually starting in the nested component and working its way upward. Once this function has been passed upwards to the same component in which the original computed values live, a method with the same name as the emitted event can be run to assign new data.
// chart.vue <script> import generateChart from 'color-contrast-table' import Table from './table.vue' export default { name: 'Chart', components: { Table, }, props: { colors: Array, }, methods: { generateChart, namechangefunction (index, value) { this.colorArray[index].name = value }, valuechangefunction (index, value) { this.colorArray[index].value = value } }, data() { return { colorArray: this.$props.colors } }, computed: { colorChart: function () { return this.generateChart(this.colorArray) } }, } </script> <template> <Fragment> <Styles/> <Table :colors="colorChart" @namechange="namechange" @valuechange="valuechange" /> </Fragment> </template>
Like I mentioned before, we can't mutate
props
values, so instead these functions needs to changecomputed.colorArray
. One nice side effect of this requirement is that unlike in React and Svelte where we had to spread an existing value into a new object to update one value, Vue can directly change one piece of thecomputed.colorArray
object.
Front end state management is a LOT more complicated than what I briefly touched on here. Often since state deals with user input and data transformation there are a million different ways to handle this depending on how to optimize for the specific situation.
React can use the context API to circumvent passing props a lot like how Svelte uses stores. Svelte can use custom event directives to emit and listen for custom events similar to how Vue works. All three frameworks can pass down a function in one way or another that can be used to update a parent's state.
Performance
Just for fun, here's the final bundle size of each package running within three small example projects I put together.
Quirks and Final Thoughts
Of the these three frameworks, I've been working with React the longest and have built the most complex apps with it. My last few day jobs have involved primarily using React. That said, while I have the most knowledge about its intricacies, I probably also have the most experience with running up against its rough edges. There's a lot I like about React now, but I remember JSX being a lot to get used to. It can sometimes be hard to search out help as a beginner, especially now that there's probably as much information on the web about function components and Hooks as there are about class components and Lifecycle Methods. As a beginner its not always apparent when to use which.
Also, for whatever reason, at some point React's dev tools became two views — Components and Profiler. To be honest, I still don't know what the Profiler view does and much preferred the old consolidated view.
I've only built a handful of small projects with Svelte, but its been very enjoyable to use. It definitely has some syntax oddities, but I've found that they're often justifiable when explained in the right way. A lot of things make sense eventually but are used in ways that I've never quite seen JavaScript written before. This is out of the scope of what I wrote about in this post, but Svelte not relying on a virtual DOM is something that greatly interests me. This makes me want to explore Svlete more in the future.
As far as the Svelte community, I'm not a fan of Discord being the official source of help. I've come across many GitHub issues through web searches, been redirected to the Discord group, then been unable to use Discord's search to find any relevant information. Asking for help in Discord is hit or miss. It really depends who else is online at the time and if they have the answer to the question you're asking. Plus, I don't always have time to wait around for an answer, and since Discord lacks threads I've definitely missed responses that have come in after I've closed the Discord app.
Vue is super interesting, and as a direct alternative to React there's a lot about it that appealed to me. At first it felt like registering props, components, methods, data, etc... in each file felt like a lot of work, but the more I worked with Vue the more I appreciated its strong guidance system. For this being my first Vue project, the compiler threw a LOT of helpful console warnings and errors that really led to an overall smooth developer experience.
That said, I found Vue's versions can be kind of confusing. I can imagine this is how a lot of people feel not really being familiar with class vs function components in React, but I got pretty mixed up several times trying to use version 3 solutions to version 2 problems, or trying to do something in a single file component that was meant for a regular Vue instance.
This is just a personal opinion, but I think its super weird to use double quotes for interpolation.
// React <div attribute={value}>text</div> // Svelte <div attribute={value}>text</div> // Vue <div v-attribute="value">text</div>
I have to say that most of my compile errors were around trying to use curly brackets when Vue wanted double quotes. Quotation marks pretty heavily signify a
string
type in my mental model at this point.
I hope this brief look into React, Svelte, and Vue helped someone make up their mind about which of the frameworks they'd like to get started with. All three have strengths and weaknesses, things I like and things I don't. It's hard to pick one I would definitely say is "the best," and that's probably a some-what subjective answer anyways. The best way to find out for yourself is to just start building.
If any of the examples from the post need a little bit more context to make sense, check out the full mono-repo for all three versions on GitHub. And if I got something wrong, either in the explanation or the packages themselves, I'm definitely accepting feedback and pull requests!
Good luck building!