Vue or React. Well lets see the most noticeable features.
Vuex is the preferred library for managing state.
It consists of states (where all the data is kept), mutations (single purpose functions to update state), actions (a fancy function with the purpose for triggering multiple mutations), and lastly modules (the state definition or shape).
store/modules/books.js
module.exports = {
state: {
books: []
},
mutations: {
addBook(state, payload) {
state.books.push(payload);
},
update(state, list) {
state.books = list;
}
},
actions: {
reorderBooks({ commit, state }) {
const clone = [...state.books];
clone.sort(function(a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
commit('update', clone);
}
}
};
store/index.js
import Vuex from 'vuex';
import books from './modules/books';
module.exports = new Vuex.Store({
modules: { books }
});
Top most component to initialize state e.g App.js
import Vue from 'vue';
import store from './store';
const app = new Vue({
el: '#app',
store,
components: {
'books': require('./components/Books')
}
});
components/Books.vue
<template>
<ul class="book-list">
<li class="source-item" v-for="book in books">
Book Name: {{ book.name }}
</li>
</ul>
</template>
<script>
export default {
props: {...}
data() {...}
computed: {
books() {
return this.$store.state.books;
}
},
methods: {
addBook() {
this.$store.commit('addBook', {
name: 'BFG'
});
},
reorder() {
this.$store.dispatch('reorderBooks');
}
}
}
</script>
<style />
Redux is the preferred library for managing state.
It consists of reducers (where all the data is kept), action creators (where data is dispatched to update reducers), and finally an all important connect function to give the component access to the state.
reducers/booksReducer.js
import {
ADD_BOOK
} from '../actions';
const INITIAL_STATE = {
list: [],
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case ADD_BOOK:
return {...state, list: [...state.list, action.payload]};
default:
return state;
}
};
reducers/index.js
import booksReducer from './booksReducer';
export default combineReducers({
books: booksReducer
});
actions/index.js
export const ADD_BOOK = 'add_book';
export const addBook = book => ({
type: ADD_BOOK,
payload: book
});
Top most component to initialize state e.g App.js
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import reducers from './src/reducers';
import Books from './components/Books';
class App extends Component {
render() {
const store = createStore(
reducers,
{ books: [{ name: 'james and the giant peach' }] }
);
return (
<Provider store={store}>
<Books />
</Provider>
);
}
}
export default App;
components/Books.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addBook } from '../actions';
class ContactList extends Component {
addBook = () {
this.props.addBook({ name: 'BFG' });
}
render() {
return (
<ul>
{this.book_list.map(({ name }, index) => (
<li key={index}>Book Name: {name}</li>
))}
</ul>
);
}
}
const mapStateToProps = ({ books }) => {
const { list } = books;
return { book_list: list };
};
export default connect(mapStateToProps, { addBook })(ContactList);
Local state can be defined as such in the data
method, applies only when used in component definition as shown below, else data
is an object.
Using the values in the template is fairly straight forward, can be accessed either by using handlebars syntax syntax for text, or by using vue directives of v-bind:value
or :value
to access title
from data
.
<template>
<div>
<h1>{{ title }}</h1>
<input type="text" v-bind:value="title" />
<input type="text" :value="title" />
</div>
</template>
<script>
export default {
name: 'MediaObject',
data() {
return {
title: 'Hello World'
};
},
...
};
</script>
React state
property can be anything, however typically it is an object.
We can access the state values anywhere in the class by using this.state
.
class MediaObject extends React.Component {
state = {
title: 'Hello World'
}
render () {
return (
<div>
<h1>{this.state.title}</h1>
<input type="text" value={this.state.title} />
</div>
);
}
}
With Vue, updating the local state can easily be done by replacing v-bind
with v-model
, as v-model
does a two way data binding.
Updating the state as well in the rest of the application can be easily done by changing the data value by doing the following this.title = 'new value'
. Note: This logic mainly is found in methods.
<template>
<div>
<input type="text" v-model:value="title" />
</div>
</template>
<script>
export default {
name: 'MediaObject',
data() {
return {
title: 'Hello World'
};
},
methods: {
updateTitle() {
this.title = 'Hello World Updated'
}
}
...
};
</script>
In React, in order to update the state, this.setState
needs to be called with new values the state is going to be updated with.
class MediaObject extends React.Component {
state = {
title: 'Hello World'
}
onInputChange = e => {
this.setState(state => ({
title: e.target.value
}));
}
render () {
return (
<div>
<h1>{this.state.title}</h1>
<input type="text" value={this.state.title} onChange={this.onInputChange} />
</div>
);
}
}
Using v-if
or v-show
we can place on any element with the template
to render based on a prop
or data
value.
else-if
and else
can be added to siblings to give you more control for what to render based on the values being referenced.
<template>
<h1 v-if="title === 'Hello'">Hello</h1>
<h1 v-else-if="title === 'World'">World</h1>
<h1 v-else="title === 'World'">It must be Hello World</h1>
</template>
<script>
export default {
name: 'MediaObject',
data() {
return {
title: 'Hello World'
};
}
...
};
</script>
Any method with React can contain JSX. In this case we can use the render
function to add if else
statements in order to return the template markup we want.
class MediaObject extends React.Component {
state = {
title: 'Hello World'
}
render () {
if (this.state.title === 'Hello') {
return <h1>Hello</h1>;
}
if (this.state.title === 'World') {
return <h1>World</h1>;
}
return <h1>It must be Hello World</h1>;
}
}
Vue supports scoped sass styling for components. By default in the webpack configuration from vue cli the scoped styling is applied to all .vue
files.
<template>
<div class="text-box">
<h1 class="header">Hello World</h1>
</div>
</template>
<script />
<style lang="sass">
.text-box {
text-align: center
.header {
color: blue;
}
}
</style>
To install sass, update the following.
-
Install loaders
npm install --save-dev node-sass sass-loader;
-
Update webpack.config* files to include
sass-loader
, depends onvue-style-loader
{ test: /\.scss$/, use: [ 'vue-style-loader', 'css-loader', { loader: 'sass-loader', options: { data: '$color: red;' } } ] },
React supports scoped css styling in the form of the style attribute.
<div style={{ textAlign: 'center' }}>
<h1 style={{ color: 'blue' }}>Hello World</h1>
</div>
To install sass on for a create react app, the following is required.
-
Eject and install loaders
npm run eject; npm install --save-dev node-sass sass-loader;
-
Update config/webpack.config* files to include sass loader
rules: [ /* more loaders here */ { test: /\.scss$/, include: paths.appSrc, loaders: ["style-loader", "css-loader", "sass-loader"] },
Built-In as part of definition of the component, we can specify requirements for its props
.
export default {
name: 'MediaObject',
props: {
image: {
type: String,
required: true
},
header: {
type: String,
required: true
},
paragraph: {
type: String
}
}
}
Typescript can provide an even better development for components as it provides type checking for functions, and statements.
It can be installed as part of vue instance or read the guide recommend by (ugh) Micrsoft to set up
Example below shows how easy it is to use.
<template />
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
name: 'MediaObject',
props: {
image: {
type: String,
required: true
},
header: {
type: String,
required: true
},
paragraph: {
type: String
}
},
methods: {
headerMessage (): string {
return this.header + ' world'
}
}
...
});
</script>
<style />
React has also Built-In props declaration.
MediaObject.propTypes = {
header: PropTypes.string.isRequired,
paragraph: PropTypes.string.isRequired,
image: PropTypes.string
}
In addition Flow.js
provides an even more control of the functions and variable data types to provide more insight on how to use the component.
type MediaObjectType = {
header: string,
paragraph: string,
image?: string
};
export default class MediaItem extends React.Component<MediaObjectType> {
headerMessage (): string {
return this.header + ' world'
}
...
};
- Picture.js
import * as React from 'react';
type PictureType = {
viewport: Array<ViewportType>,
alt: string
};
type ViewportType = {
width: number,
srcset: string
};
export default class Picture extends React.Component<PictureType> {
// We can't loop inside render()
createMediaSource = (viewport: Array<ViewportType>, alt: string): React.Node => {
// Sort viewport for the smallest to be the first one
return viewport.sort((first: ViewportType, second: ViewportType): number => second.width - first.width)
.map((obj: ViewportType, index: number): React.Node => {
if (index === 0) {
return (
<React.Fragment key={index}>
<source media={'(min-width:' + obj.width + 'px)'} srcSet={obj.srcset} />
</React.Fragment>
);
} else if (index === (viewport.length - 1)) {
return (
<React.Fragment key={index}>
<source media={'(min-width:' + obj.width + 'px)'} srcSet={obj.srcset} />
<img src={obj.srcset} alt={alt} />
</React.Fragment>
);
} else {
return (
<React.Fragment key={index}>
<source media={'(min-width:' + obj.width + 'px)'} srcSet={obj.srcset} />
</React.Fragment>
);
}
}, alt);
};
// With the correct linting rules, one does not need to explicitly check React.Node
render (): React.Node {
return (
<picture className="media-asset__image">
{this.createMediaSource(this.props.viewport, this.props.alt)}
</picture>
);
}
}
- MediaItem.js
import React from 'react';
import Picture from '../../atoms/Picture/Picture';
let dataExample = {
viewport: [
{
width : 992,
srcset : 'http://www.volkswagen.co.uk/files/live/sites/vwuk/files/About%20Us/Category%20Hero/Volkswagen_Red_Polo_Outside_Desktop.jpg'
},
{
width : 768,
srcset : 'http://www.volkswagen.co.uk/files/live/sites/vwuk/files/About%20Us/Category%20Hero/Volkswagen_Red_Polo_Outside_Tablet.jpg'
},
{
width : 320,
srcset : 'http://www.volkswagen.co.uk/files/live/sites/vwuk/files/Homepage/Hero-carousel/image/polo-mobile-carousel.jpg'
},
],
alt: 'Polo'
};
export default class MediaItem extends React.PureComponent<{}> {
render () {
return (
<div className="media-asset__background-image">
<Picture
alt={dataExample.alt}
viewport={dataExample.viewport} />
</div>
);
}
};
-
Install dependencies
npm i -g @storybook/cli; getstorybook; npm install --save-dev @storybook/vue; npm install --save-dev storybook-addon-vue-info; npm install --save-dev @storybook/addon-knobs
-
.storybook/addons.js
import '@storybook/addon-actions/register'; import '@storybook/addon-links/register'; import '@storybook/addon-notes/register'; import '@storybook/addon-knobs/register';
-
.storybook/config.js
import { configure } from "@storybook/vue"; function loadStories() { require("../src/stories"); } configure(loadStories, module);
-
Make sure webpack config can compile
.vue
files. -
src/stories/index.js
import { storiesOf } from "@storybook/vue"; import { withKnobs, text } from '@storybook/addon-knobs'; import { withInfo } from 'storybook-addon-vue-info'; import MediaObject from '../components/MediaObject.vue'; storiesOf('Media Object', module) .addDecorator(withKnobs) .add('with header', withInfo({ summary: 'Summary for MyComponent' })(() => ({ components: { MediaObject }, template: '<media-object header="BUILDING STUFF" paragraph="" />' }))) .add('with paragraph', withInfo({ summary: 'Summary for MyComponent' }) (() => ({ components: { MediaObject }, template: '<media-object header="" paragraph="Lorem ipsum" />' }))) .add('with image', withInfo({ summary: 'Summary for MyComponent' }) (() => ({ components: { MediaObject }, template: '<media-object header="" paragraph="" image="https://scontent-lht6-1.cdninstagram.com/vp/f80de90220f4e1aaedff871e790d4647/5BD8C054/t51.2885-15/sh0.08/e35/s750x750/15306073_345142072522696_5823500891986067456_n.jpg" />' }))) .add('with configurable props', withInfo({ summary: 'Summary for MyComponent' }) (() => { const header = text('header', 'HEADER'); const paragraph = text('paragraph', 'Lorem ipsum'); const image = text('image', 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2000px-React-icon.svg.png'); return { components: { MediaObject }, template: `<media-object header="${header}" paragraph="${paragraph}" image="${image}" />` }; }));
-
Run storybook. When
getstorybook
is executed, it updates the package.json with a new node script command. We can execute it to start storybook.npm run storybook;
-
Install dependencies
npm i -g @storybook/cli; getstorybook; npm install --save-dev @storybook/addon-info @storybook/addon-knobs; @storybook/addons babel-core
-
.storybook/addons.js
import '@storybook/addon-actions/register'; import '@storybook/addon-links/register'; import '@storybook/addon-knobs/register';
-
.storybook/config.js
import { configure, setAddon } from '@storybook/react'; import infoAddon from '@storybook/addon-info'; if (process.env.NODE_ENV === 'test') { // for storyshots function loadStories() { require('../src/stories'); } setAddon({ addWithInfo: function addWithInfo(storyName, info, storyFn) { return this.add(storyName, (context) => { const renderStory = typeof info === 'function' ? info : storyFn; return renderStory(context); }); } }); configure(loadStories, module); } else { function loadStories() { require('../src/stories'); } setAddon(infoAddon); configure(loadStories, module); }
-
.storybook/webpack.config.js (this is not the same config file used for the application)
const path = require('path') module.exports = { module: { rules: [ { test: /\.scss$/, loaders: ['style-loader', 'css-loader', 'sass-loader'], include: path.resolve(__dirname, '../') }, { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /\.(woff|woff2)$/, use: { loader: 'url-loader', options: { name: 'fonts/[hash].[ext]', limit: 5000, mimetype: 'application/font-woff' } } }, { test: /\.(ttf|eot|svg|png)$/, use: { loader: 'file-loader', options: { name: 'fonts/[hash].[ext]' } } } ] } }