Tutorial build Adding New Liner and State in React

in #utopian-io6 years ago

Repository

https://github.com/facebook/react

What Will I Learn?

I will learn :

  • Adding New Liner Entries Efficiently.
    • Implementing state management in React with Redux-saga.
    • Getting Started with State Management in Redux.

Requirements

Difficulty

  • Advanced

Adding New Liner Entries Efficiently.

Let's get straight to it. We'll be adding functionality to the form available at /add. We'll also be working on multiple form inputs. Let's create src/screens/AddLine/index.js (if you are yet to do so) and get to work. We'll be importing our dependencies for this module. We'll start off by importing React and it's Component subclass. We'll also be using the connect method from react-redux to add our store state and dispatch methods to the this.props property. We'll also use the Box, Flex, Label, Select, RedButton and Text Priceline components to construct our UI. We'll import the addLiner method from src/containers/Home/actions.js (which we'll soon create) as it will help us add a new "liner" (or quote, if you prefer) to the store. Finally, we also import the getAppState method from the reducer.js file at src/containers/App to help us retrieve the current application state and make it available to this module.

import { connect } from 'react-redux';
import { Box, Flex, Label, Select, RedButton, Text } from 'pcln-design-system';
import Textarea from '../../components/Form/Textarea';
import { addLiner } from '../Home/actions';
import { getAppState } from '../../containers/App/reducer';

Next, we'll be adding some code to our AddLiner class. We'll add a constructor to the class. Within our constructor, we'll first follow a common React practice and inherit from React's Component constructor by calling the super method with any provided props as the argument. We'll also be making the this.handleSubmit method accessible throughout our class. We then set a default state for our AddLine component. You may ask, "I thought we were supposed to use Redux for state? Why are we then setting state in this component?" Well, simply because we use Redux doesn't mean we can't use regular state management. We use regular state management for state that is supposed to remain private. For instance, we have two fields:

• A Select field with a list of authors we can use for any given quote.
• A Textbox where we can add the quote.

We'll save them into the component's state and later, we'll send them to the store.

    constructor (props) {
        super(props);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.state = {
            author: 'Immortal Technique',
            body: 'This is the point from which, I die or succeed; Living the struggle, I know I'm alive when I bleed.'
        }
    }
}

Next, in our class, we'll add a render method. We'd like to render a select box pre-filled with a list of authors within our form. To do this, we must first go through an array of authors and we'll create an <option></option> element for each author. We can do this by calling the map method on this.props.author (undefined at the moment).

    let authors = this.props.authors.map((author, index) => (
        <option value={author.name} key={index}>{ author.name }</option>
    ));
}

Next, we construct a little UI with the Priceline Design System Components. We create a form and we assign the this.handleSubmit method as its submit event handler. We also assign an inline handler method to the onChange method of the <Select/> component that simply sets the author state value to the selected author. We also make sure the select box selects an author by default. We do this by mapping the value attribute to the this.state.value option. We also set the state.body property to a new value anytime the value of the Text box changes. Finally, we show a pretty red button that triggers the submit action.

    ```return (
        <Flex mt={4} justify="center" alignItems="center">
            <Flex flexDirection="column" width={[ 0.8, 0.8, 0.5 ]}>
                <Text bold mb={3} fontSize={3}>Add. The Dopest Lines. Ever.</Text>
                <Box mb={3}>
                    <form onSubmit={this.handleSubmit}>
                        <Flex flexDirection="column" mb={3}>
                            <Label mb={2}>Author</Label>
                            <Select onChange={e => this.setState({
                                author: e.target.value
                            })} placeholder="Which cat dropped this line?" value={this.state.author}>
                            {authors}
                            </Select>
                        </Flex>

                        <Flex flexDirection="column" mb={3}>
                            <Label mb={2}>Lyrics</Label>
                            <Textarea rows={7} value={this.state.body} onChange={e => this.setState({
                                body: e.target.value
                            })} placeholder="Spit that line here, dawg..."></Textarea>
                        </Flex>

                        <RedButton type="submit">Save and go back</RedButton>
                    </form>
                </Box>
            </Flex>
        </Flex>
    )

With our render method done, we simply need to create the submit event handler. We use `e.preventDefault` to prevent our form from actually trying to send information to a server as that's not the behaviour we'd like. We also use the `Array.prototype.reduce` method to calculate the largest numeric id within our liners collection and we then simply add 1 to the id obtained to get our new id. We then call the addLiner method which is available on the this.props. We supply the new id, author and body to this method for processing. Finally, we go back to the index by calling `this.props.history.push('/')` which will take us to the root.

    ```handleSubmit (e) {
        e.preventDefault();
        let newID = this.props.liners.reduce((maxId, liner) => Math.max(maxId, liner.id), 0) + 1;
        this.props.addLiner({
            id: newID,
            author: this.state.author,
            body: this.state.body
        })
        this.props.history.push('/')

    }

We then need to create our mapStateToProps and mapDispatchToProps functions that we get to pass to Redux's connect method. We get to add the liners and authors props to the component.

    return {
        liners: getAppState(state).get('liners'),
        authors: getAppState(state).get('authors')
    }
}

const mapDispatchToProps = (dispatch) => {
    return {
        addLiner: data => dispatch(addLiner(data))
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(AddLine);

It's now time to create the addLiner method in src/containers/Home/actions.js. We'll create the actions.js file at src/containers/Home. We'll import our constants and we'll use them in the action object.


export const fetchLinersRequest = data => {
    return {
        type: ADD_LINERS_REQUEST,
        data
    }
}

export const addLiner = data => {
    return {
        type: ADD_LINER_REQUEST,
        data
    }
}

We then need to update our saga at the Home screen. We'll edit src/screens/Home/saga.js. We'll setup the dependencies. We get to import the all method that combines multiple saga functions. We also import the call method that helps us resolve any promises. We use the put method to send an action to the Redux store. We also use the takeLatest method to take the latest available saga action.

Next, we import the getLinersData method and a couple of constants.

import { getLinersData } from '../../services/DataService';
import { ADD_LINERS_REQUEST, ADD_LINER_REQUEST } from './constants';
import { SET_LINERS_DATA, ADD_LINER } from '../../containers/App/constants';

We get to our addLiner method now. We simply use this as a wrapper that calls our put method with a type and some data.

    return yield put({
        type: ADD_LINER,
        payload: data
    })
}

Then in the root method, we simply take the latest ADD_LINER_REQUEST and call the addLiner method above.

  yield all([
    takeLatest(ADD_LINERS_REQUEST, fetchLiners),
    takeLatest(ADD_LINER_REQUEST, addLiner)
  ]);
}

We then need to update our reducer at src/containers/App/reducer.js and add some code. We'll be using the fromJS method from ImmutableJS to turn Javascript objects or arrays to immutable ones. In our AppReducer function, we set the liners state property to an array that comprises of the initial liners data along with the new data.

import {ADD_LINER } from './constants';
const initialState = fromJS({
    liners: [],
})

We then add our reducer code.

    switch (action.type) {
        case ADD_LINER:
            return state.set("liners", fromJS([state.get('liners').push(action.payload.data)]));

        default:
            return state
    }
}

We've just completed functionality that enables us to add some new quotes easily. But, it's not enough, as we'll also need a way to keep our authors dynamic.

We'll go through the functionality of these methods:

• The slicer method: returns a portion of the state.

• The serialize method: can be used to perform an action before the data is saved or serialized to local storage.

• The deserialize method: is used to perform an action before data is retrieved or deserialized from local storage.

• The merge method: is used to merge the initial state and the persisted state. Since we're using immutable, we're merging the persisted data along with the initial state data through Immutable's mergeDeep method.

import persistState from 'redux-localstorage'

const store = createStoreWithMiddleware(
    rootReducer,
    initialState,
    compose(
        persistState(undefined, {
            slicer: (paths) => (state) => state,
            serialize: (subset) => JSON.stringify(subset),
            deserialize: (serializedData) => fromJS(JSON.parse(serializedData)),
            merge: (initialState, persistedState) => initialState.mergeDeep(persistedState)
        })
    )
)

Implementing state management in React with Redux-saga.

• What is State?

State simply means all data present in the application at a particular point in time. These datum could either be user generated or of third party origin. A good example of data within an application that can be considered as state is the simple boolean switch within a user's settings panel that allows the user choose to either receive notifications or go without.

• What is State Management?

State management simply refers to an architecture devised to ensure state is easily accessible and modifiable. You may write a custom state management mechanism or you may just piggyback on the excellent open source solutions accessible to all of us. Several excellent state management solutions such as Redux and MobX are freely available. For the remainder of our tutorial, we'll be narrowing our focus to the Redux state management solution.

• What is Redux?

Redux is a contraction of the terms Red__ucers and Fl__ux. Redux is a state management solution that marries the popular use of reducers in Javascript with the Flux ideology that was inspired by Facebook. This begs the questions posed below.

• What are Reducers?

Reducers are functions that try to collapse multiple values to a single value. For instance, using reducers, we may attempt to obtain the maximum value from a range of values. Take the following example in which we find the largest id in the items array. We do that by passing a method and an integer to the reduce method. This method will return the maximum value between the current item under consideration and the maximum previous value. The integer provided is the "initialization value" which basically begins maxId at -1. This reducer can be used when you need to add a new entry to the items array.

    { id: 1, title: 'Rush of Blood to the Head' },
    { id: 12, title: 'Viva la Vida or Death and all His Friends' },
    { id: 14, title: 'Parachutes' }
]

const largestId = items.reduce((maxId, item) => Math.max(item.id, maxId), -1) // returns 14

• What is Flux?

Flux is an application architecture philosophy from Facebook that tries to make applications easier to build and maintain by enforcing a singular direction for data flow. This is also called the unidirectional data flow model. Flux strips out unnecessary complexity so we can write our code with less headaches.

• What is State Immutability and Why Should I Care About It ?

State immutability is a concept that describes state that is prone to change, as conversely being prone to errors.

Basically, state immutability simply means, "Dude, never change or mutate the state in an application". I know you've got your eyebrows up and about to go into an uproar. "But how do we get anything done without changing the state?", you exclaim. Well, the solution to that is that we should never change state, we should only change a copy of the state object and enforce it as the current representation of the state. This is helpful in more ways than one. Implementing brain-wracking techniques like undo actions (Ctrl + Z) becomes a piece of cake and we can easily revert to a previous state image if we make an error. Also, when we hot reload our code, we don't have our state thrown out the window, but still readily available and useful.

We can implement state immutability in our application by leveraging the ImmutableJS package (also by Facebook).

• What Are Sagas and How Are They Related to Redux?

A Saga is like a separate part of your application that's solely responsible for side effects. When you're building apps, any actions that are carried out may come with their own side effects. For example, when making an asynchronous request, a common side effect encountered is the inability of the client to cancel the request in the case of inadvertent triggers. A Saga can help us handle such side effects.

According to its official Github page, Redux Saga is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, simple to test, and better at handling failures.

Sagas use an interesting new concept in ECMAScript 6 called generators. You can learn all about generators in ES6 here

A Gentle Introduction To Redux
Redux works with a set of separate but related entities. We'll be looking at these entities and their roles within a Redux application

Reducers: Reducers are 'pure' JavaScript functions that accept a state object along with an action object and returns a new state object. As an example, this is a typical Redux reducer. Here, it accepts an initialState object as an argument and it also accepts an action object. Notice how our reducer doesn't attempt to mutate the state, rather it returns a new state object based on the action object it was passed.

    title: "The Shawshank Redemption"
}

const AppReducer = (state = initialState, action) => {
    switch (action.type) {
        case SET_TITLE:
            return {
                ...state,
                title: action.data.title
            }

        default:
            return state
    }
}

Action Dispatchers: These are the little guys that tell the Redux store something has happened. They simply call the dispatch method with an action object as the argument.

Action Objects: These are simple objects with the required property of a type and an optional argument. If action dispatchers are messengers, then action objects are the messages they transport.

A Gentle Introduction To Sagas
Sagas are kinda like reducers and actions rolled in one. They are capable of dispatching action objects that can be acted upon by reducers and they are also capable of listening for action objects to act upon in they own right.

A typical saga looks like the one below. Here, we have the generator method setAppTitle that uses the yield statement to obtain the title value from getAppTitle. Upon obtaining the title, it then uses the put method from redux-saga to try and set the title of the app.

    const title = yield call(getAppTitle);
    return yield put({
        type: SET_APP_TITLE,
        payload: {
            title
        }
    })
}

export default function* root() {
  yield all([
    takeLatest(SET_APP_TITLE_REQUEST, setAppTitle),
  ]);
}

Getting Started with State Management in Redux.

We'll be writing code that allows our app add new quotes that can be displayed on the index page. We'll do that by sending our data to the Redux store upon submission and that'll allow us to access it from any part of our application.

We'll install the following packages:

react-router-redux: This helps our react-router stay in sync with our redux store.
redux-saga
redux-localstorage
immutable

We'll be using the Redux Local Storage middleware to persist our state to the browser's storage for ease of accessibility.

We'll be changing our application structure a little bit. Our new application structure will resemble the one below. The changes have been highlighted.


config/...
node_modules/...
public/...
scripts/...
src/
components/

Header/
index.js
logo.svg
containers/

App/
reducer.js
constants.js
actions.js
index.js
screens/...

redux/

store.js
reducers.js
registerServiceWorker.js

package.json

We'll be setting up our feed of liners to source data from the Redux store instead of from an array, but first of all, let's setup the Redux application structure. We'll modify src/index.js and have our app access the soon to be created Redux store. We'll first list out our dependencies. We'll need the Provider component that will allow our app access the Redux store. We'll also need the browserHistory method that allows us get the browsers history object. We also use the syncHistoryWithStore method to keep the history up to date with the store. Finally, we'll be using the store object which we're yet to create.

import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { browserHistory } from 'react-router';
import { BrowserRouter as Router } from 'react-router-dom';
import { syncHistoryWithStore } from 'react-router-redux';
import store from './redux/store'
import App from './containers/App';

Next, setup the application to use the above mentioned dependencies.


ReactDOM.render((
    <Provider store={store}>
        <Router history={history}>
            <App />
        </Router>
    </Provider>
),
document.getElementById('root'));

We'll create the store.js module at src/redux. We'll be using the createStore and applyMiddleware methods from redux to help us create a redux store and then apply some middleware that allows us extend the store's capabilities. We'll also use a so-called 'rootReducer' that actually just combines separate reducers into one giant object. In our configure method, we check if the browser window has the Redux DevTools extension installed. If it does, we create the store with Dev Tools support. We then apply middleware that helps us carry out various tasks. For now we'll be using none. We then create the store supplying it all the reducers along with any possible initial state data we may have available which at the moment is an empty object.

Last of all, we check if we're using hot module reloading. If we are using HMR, we keep the reducer updated with every hot reload by replacing the current reducer with itself.


import rootReducer from './reducers'

export default function configure(initialState = {}) {
  const create = window.devToolsExtension
    ? window.devToolsExtension()(createStore)
    : createStore

  const createStoreWithMiddleware = applyMiddleware()(create)

  const store = createStoreWithMiddleware(rootReducer, initialState)

  if (module.hot) {
    module.hot.accept('./reducers', () => {
      const nextReducer = require('./reducers')
      store.replaceReducer(nextReducer)
    })
  }

  return store
}

Let's create src/redux/reducers and get to work. We'll be importing a single reducer (the app reducer) from src/containers/App/reducer. We'll also be importing the combineReducers method from redux.

import AppReducer from '../containers/App/reducer';

export default combineReducers({
    app: AppReducer
})

We now have to create the AppReducer which we'll make available at src/containers/App/reducer.js. We'll be importing the fromJS method from the
ImmutableJS library. This fromJS method allows us to turn a regular JavaScript object to an Immutable object that can only be manipulated through an API that is exposed by the Immutable library. Also, we'll be defining a set of constants in the src/containers/App/constants.js as a good programming practice. We then define the
initial state for our app as an ImmutableJS object with the liners property set to an empty array.

Finally, we define the AppReducer function that accepts the state (we set it to the initial state by default) and the action object. We use the switch statement on the action.type to get the action.type value that corresponds to SET_LINERS_DATA. If we get a match, we set the liners property of the Immutable initialState to the value contained in the action.data.

import { SET_LINERS_DATA } from './constants';

const initialState = fromJS({
    liners: []
})

export default const AppReducer = (state = initialState, action) => {
    switch (action.type) {
        case SET_LINERS_DATA:
            return state.set('liners', action.data)
        default:
            return state
    }
}
So as an example, our action object may look like this:

{
    type: 'SET_LINERS_DATA',
    data: [
        {
            id: 3,
            body: "It's a new world",
            author: "Bryan Adams"
        },
        ....
    ]
}

We've successfully setup our initial Redux application structure. Let's do something with it. First of all, real web apps typically make requests to an external API for data. Let's mimic the real thing by moving our liners data from an array to a JSON file. Create assets/liners.json and add some JSON data.

    "id": 1,
    "author": "Immortal Technique",
    "government_name": "Felipe Andres Coronel",
    "body": "The purpose of life is a life with a purpose. Rather die for what I believe in than live a life that is worthless.",
    "photo": "immortal-technique.jpg"
},
{
    "id": 2,
    "author": "Gangnam",
    "government_name": "Gangnam Rocks",
    "body": "I don't rap for dead presidents. I'd rather see the president dead.",
    "photo": "gangnam.jpg"
},
{
    "id": 3,
    "author": "Grigs 3000",
    "body": "Hell just fell 3000 more degrees cooler but y'all can't measure my worth; and before you do, you'll need a ruler made by all the Greek gods.",
    "photo": "grigs-3k.jpg"
}]

Then we'll update the code at src/screens/home/index.js
to reflect the change.

Remove this line.

const liners =[...]
... And instead, run this import at the top.

import liners from ../data/liners.json

We'll update the code at src/screens/home/index.js to reflect the change.

Remove this line.

const liners =[...]
... And instead, run this import at the top.

import liners from ../data/liners.json

The switch in architecture here helps us move our data from non-flexible format to a more convenient one.

However, looking deeper, we've got some optimizations we could add to help our app more production-ready. We'll be leveraging the power of sagas to help make our application run with better separation of concerns. In a typical production app, we'd probably be making a HTTP request for the data instead of just importing it from a local source.

Resource

Proof of Work Done

Github

Sort:  

Thank you for your contribution.

  • The tutorial is poorly structured.
  • There is a tutorial quite similar to yours. It has the same code in certain sections.link

Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Hey @portugalcoin
Here's a tip for your valuable feedback! @Utopian-io loves and incentivises informative comments.

Contributing on Utopian
Learn how to contribute on our website.

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!

Congratulations @dolarsgrig! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

Award for the number of posts published

Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word STOP

To support your work, I also upvoted your post!

Do not miss the last post from @steemitboard!


Participate in the SteemitBoard World Cup Contest!
Collect World Cup badges and win free SBD
Support the Gold Sponsors of the contest: @good-karma and @lukestokes


Do you like SteemitBoard's project? Then Vote for its witness and get one more award!

Your account has been permanently banned for two reasons:


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Coin Marketplace

STEEM 0.18
TRX 0.15
JST 0.028
BTC 63768.57
ETH 2478.16
USDT 1.00
SBD 2.54