Skip to content

Instantly share code, notes, and snippets.

@jordanrios94
Last active September 27, 2018 15:55
Show Gist options
  • Save jordanrios94/03c1791469ff4b13c1c1e4c6ea836df8 to your computer and use it in GitHub Desktop.
Save jordanrios94/03c1791469ff4b13c1c1e4c6ea836df8 to your computer and use it in GitHub Desktop.

Progressive Framework Side-by-Side Intro

Vue or React. Well lets see the most noticeable features.


Application State Management

Vue

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 />

React

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 Management (Controlled Components) - Basic state definition

Vue

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

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>
        );
    }

}

Local State Management (Controlled Components) - Basic state update

Vue

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>

React

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>
        );
    }

}

Conditional Rendering

Vue

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>

React

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>;
    }
}

Styling

Vue

Scoped styling

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>

SASS

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 on vue-style-loader

    {
        test: /\.scss$/,
        use: [
          'vue-style-loader',
          'css-loader',
          {
            loader: 'sass-loader',
            options: {
              data: '$color: red;'
            }
          }
        ]
    },
    

React

Scoped styling

React supports scoped css styling in the form of the style attribute.

<div style={{ textAlign: 'center' }}>
    <h1 style={{ color: 'blue' }}>Hello World</h1>
</div>

SASS

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"]
    },
    

Type checking

Vue

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

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'
    }
    ...
};

Production scenario

  • 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>
        );
    }
};

Storybook

Vue

Config

  • 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;
    

React

  • 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]'
                        }
                    }
                }
            ]
        }
    }
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment