#justanotherintro
Explore tagged Tumblr posts
Text
Intro to React and Redux
TF is this?
I used the boilerplate project built by Cory House (Twitter: @housecor), SlingShot to build my company's transition into React. This was chosen because of his excellent PluralSight videos, and I felt that the patterns used were generally community accepted and thought out. Any reference to "this project" could be considered standard defined from Cory House, or just my whim.
Background (skip this if you just want to learn some react/redux)
I was working in a silo for two years, building out the front-end for a site that runs on .NET and MVC architecture. There is nothing wrong with server-side rendered pages. But our front-end was thrown together with little thought to re-usable peices. For example, there are at least 5 layout templates for a website that you log into and view a few pages (think accessing your bank account).
Our end-users have really bad internet connections. With a slow server-side rendered website, most used the native apps. It was most apparent when we had an extremely high-usage day, which brought the native mvc site to a crawl, but native apps were fine calling our new APIs. Had the site been fully converted to the React framework built for new features (as opposed to redoing the entire site in high priority), it would have likely persevered through the high traffic.
That's all well and good, 20-20 past vision stuff. But it still points to why I wanted to push for a SPA driven by React and Redux. The whole intention of the site is to act like the native apps, so let's bring it up to speed!
All that said, I decided to leave for several reasons, but I wanted to make sure any of the devs, including all .NET devs, native android, and native iOS devs, would at least have something to refer to besides just the general "here's how he built this so let's use that pattern" learning. I hope it's helpful for them, and I hope this reaches anyone else interested in React and Redux. There are a shit-ton of tutorials out there, I'm hoping I can make a TL;DR version of Cory's awesome 6+ hours of lessons.
Component Structure
Components can be written as a function or by extending the React.Component class. This projects uses the Container/Dumb Component pattern where "smart" components are considered containers, and dumb components (under /components) take props and know how to display them.
Props
Props are the data passed into a component. They can be any javascript value type, which allows for display data or functions that need to be executed within the component. props are the first param passed to a functional component. Access props from a class component by using this.props.
PropTypes
Every component should define its prop's types. This enforces type checking for the linter and testing, as well as will be generated in the npm run gendocs script. You will also see console warnings if an unexpected type is used in development mode. The standard pattern is to declare the component as a const or class, and before exporting, assign the propTypes object:
const MyDumbComponent = (props) => { return (<h1>{props.header}</h1>); }; MyDumbComponent.propTypes = { classes: PropTypes.string, header: PropTypes.string.isRequired, }; export default MyDumbComponent
When extending the React.Component class, you may see propTypes as a static property in tutorials. This is fine, however the pattern in this project still maintains them outside of the class declaration, mostly for consistency.
If your component can work without requiring access to React's lifecycle methods and manage state, it is better to create it as a function so as to not add the ES6 class overhead. However, do not make that prevent you from using the class structure if needed.
The most typical needs for React's lifecycle access are setting up/managing component state and retrieving data before a render. Below is an example of a component that handles state management. Note that this is referring to the "component state." Meaning this is just pure React at this point, without introducing Redux app state management.
Here's a simple incrementer/decrementer component:
import React from 'react'; import PropTypes from 'prop-types' class Counter extends React.Component { // Initialization: constructor(props) { super(props); // Define initial state: this.state = { amount: 0, }; // Bind class functions to `this` to be accessed within class methods: this.increment = this.increment.bind(this); this.decrement = this.decrement.bind(this); } /** Increase the amount */ increment() { this.setState({ amount: this.state.amount + 1 }); } /** Decrease the amount */ decrement() { this.setState({ amount: this.state.amount - 1 }); } /** Render the component */ render() { const {amount} = this.state; return ( <div> <label>Amount: {amount}</label> <br><button>Increase</button> <button>Decrease</button> </div> ) } }
So what is happening here? The constructor is run at the initial load of the component. super(props) is simply passing props to the React.Component abstract class. We want to define our intentional state for this component. In this case, we just want to keep track of what the amount to display is. The binding is a side-effect of ES6 classes. There are several ways to handle dealing with this in a class, specifically with render(), but this is the most resource efficient way, it just adds a few extra lines.
increment() and decrement() are class methods that are simply updating the component state. You cannot mutate this.state directly, which is where this.setState() comes in. It is a React function that accepts an object of what the new state will be, and when called, will trigger a component re-render. So anytime the state is updated, render() is called and the state values will be updated.
render() is the override for React.Component that must return jsx. In this case the component is providing the amount display and the buttons to modify that amount. Note that jsx requires a single opening and closing tag surrounding all others.
return ( <div>section 1</div> <div>section 2</div> )
This is invalid. If you do not want to introduce a wrapping html element, you can use the `` tag to prevent additional html written to the DOM.
return ( <div>Section 1</div> <div>Section 2</div> )
Redux Preview
This section explains how a component can trigger data retrieval and access the Redux store. It may be worth checking out the vocab section of Redux first.
Retrieving data for a component can be done by overriding componentWillMount() and firing off your required actions. Most commonly something like:
async componentWillMount() { this.props.DisplayLoading(); await this.props.GetTransactions(); this.props.FinishLoading(); }
This will cause the spinner to display, dispatch the action to call the api to retrieve transactions, then hide the spinner. await is used here because we know GetTransactions is asynchronously calling the API.
Accessing The Redux Store
Redux is built in a way that it can be used without React, so in order for a component to access the store, we use the react-redux package, which provides an HoC (Higher-order Component) function called "connect".
This essentially wraps your component and returns a new component that has store access. It takes up to 4 params, but typically you will only use the first two: mapStateToProps and mapDispatchToProps.
mapStateToProps is a function that is supplied with the state (store) object. You must return and object from this function, providing any props that require data from the state. For example:
class TransactionList extends React.Component { async componentWillMount() { this.props.DisplayLoading(); await this.props.GetTransactions(); this.props.FinishLoading(); } render() { const {isLoading, transactions} = this.props; if (isLoading) { return (); } const transactionItems = transactions.map(t => { return ({t.name}); }; return ( <ul></ul> ); } } function mapStateToProps(state) { return { transactions: state.transactions, isLoading: state.spinners > 0, } } export default connect(mapStateToProps, {})(TransactionList);
A couple things to note in this example. - We are using destructuring to declare isLoading and transactions. If you are unfamiliar, it was introduced in ES6 (https://hacks.mozilla.org/2015/05/es6-in-depth-destructuring/) - In render(), a list of `` tags are built by mapping an array of objects and returning jsx. This is a useful feature as you can build jsx sections piecemeal. - mapStateToProps is declared outside of the component class. This is because it isn't tied to the component itself. - mapStateToProps is creating its own prop, isLoading by computing off of the redux state, where transactions is just plucked directly. This is fine, the point is your component does not have to necessarily follow the redux state structure. - Another note about this, is the concept of selectors. This concept houses how the data is returned from state, but a more advanced topic worth diving into when you're more comfortable. - The second param for connect is an empty object, this is addressed in the next section - TransactionList is wrapped in parenthesis after connect is executed. This is shorthand for:
const connectedStateAndProps = connect(mapStateToProps, {}); export default connectedStateAndProps(TransactionList);
mapDispatchToProps
This is used to provide dispatch. Dispatch is simply a function that takes an object with an action type and any additional data (must define a type as a string). Reducers are listening for dispatch actions. They are simply a giant switch statement that intercepts the dispatch type and determines if/how to update the redux store. More information on dispatch Usage of this within components can vary, and this project abstracts a lot of it out into defining actions (/actions). The more manual way to set it up in your component is as follows:
class TransactionList extends React.Component { async componentWillMount() { this.props.DisplayLoading(); await this.props.GetTransactions(); this.props.FinishLoading(); } render() { const {isLoading, transactions} = this.props; if (isLoading) { return (); } const transactionItems = transactions.map(t => { // Updated from previous example return ( this.props.GetTransactionById(t.id)}> {t.name} ); }; return ( <ul></ul> ); } } // Added from previous example: function mapDispatchToProps(dispatch) { return { GetTransactions: () => dispatch({ type: 'TRANSACTIONS.GET_TRANSACTIONS' }), GetTransactionById: (id) => { dispatch({ type: 'TRANSACTIONS.GET_BY_ID', id: id, }); }, DisplayLoading: () => dispatch({ type: 'COMMON.DISPLAY_LOADING' }), FinishLoading: () => dispatch({ type: 'COMMON.HIDE_LOADING' }), } } function mapStateToProps(state) { return { transactions: state.transactions, isLoading: state.spinners > 0, } } export default connect(mapStateToProps, {})(TransactionList);
As you can see, setting all your action dispatchers up within the component can add extra bloat. To alleviate that, the second param for connect can accept an object of actions. This is the pattern used in the project, so you simply import your action and define it (Note: will explain actions in detail later):
// file: ./actions/transactions.js export function GetTransactions() { return async dispatch => { try { let list = await TransactionsApi.GetTransactions(); dispatch({ type: TRANSACTIONS.GET_TRANSACTIONS_SUCCESS, transactions: list }); } catch (error) { dispatch({ type: TRANSACTIONS.GET_TRANSACTIONS_FAILURE, message: error.message || 'Unfortunately there was a problem retrieving your transactions. Please try again' }); } } } // file: ./containers/transactions/TransactionList.js import {GetTransactions} from '../../actions/transactions'; //... same class example from above export default connect(mapStateToProps, {GetTransactions})(TransactionList);
The benefits from doing it this way are better readability, abstraction of function, and easier to unit test your actions.
Redux
Redux is a single source for your entire application state. It's independent of React, can only be modified by dispatching actions (immutable), and allows for time travel because of this. React interacts with the Redux store with react-redux, which provides the ability to utilize the application state in components, as well as take actions to change state.
Redux Terms
Store: The actual data store. This is just a javascript object, so it can hold any values necessary. Redux itself is meant to maintain all app state in this single store, but allows for it to be composable using combineReducers.`
Action: An object to be received by reducers. It must contain a type property with an assigned string. These types are managed in /consants/actionTypes.js. Any additional data can be provided in the action.
{ type: "USER.LOGGED_IN", name: 'derp' }
Action Creator: A function that returns an action
function userLoggedIn() { return { type: 'USER.LOGGED_IN', username: 'derp' }; }
Reducer: A function that takes state as the first param and action as the second. State is the current state of the redux store. action is the action object containing type and data.
function userReducer(state, action) { switch (action.type) { case 'USER.LOGGED_IN': //don't mutate the state return {...state, username: action.username}; default: return state; } }
Dispatch: This function is the glue between actions and reducers. You execute dispatch() when you want to bubble up an action to be intercepted by reducers. It is provided by the Redux store, but accessed via connect (see mapDispatchToProps). This is the only way to trigger a state change.
function userLoggedIn(dispatch) { dispatch({ type: 'USER.LOGGED_IN', username: 'derp' }); }
Thunk: A thunk is a function that is returned by another function. redux-thunk is middleware between actions and reducers. If the action is a function, it executes it, thus allowing for asynchronous calls before the reducer.
function userLogin() { return async (dispatch) => { let user = await UserApi.Login(); dispatch({ type: 'USER.LOGGED_IN', username: user.username, }); } }
combineReducer(): This function allows for composing with multiple reducers. This project does this in /reducers/index.js. It simply imports all other reducers and composes them in the structure of the desired Redux store. This can be nested, as you can see in /reducers/Support/index.js, which simply exports the support related reducers:
import { combineReducers } from 'redux'; import categories from './categories'; import conversations from './conversations'; import errors from './errors'; import phoneNumbers from './phoneNumbers'; export default combineReducers({ categories, conversations, phoneNumbers, errors, });
This results in the Redux store shape as:
{ support: { categories: [], conversations: [], phoneNumbers: [], errors: null, } }
The idea is that you can write small and specific reducers that only care about their slice of the state. As this is the case, the returned new state from a reducer function, for instance in /reducers/Support/phoneNumbers:
import {SUPPORT} from '../../constants/actionTypes'; import initialState from '../initialState'; export default function phoneNumbersReducer(state = initialState.support.phoneNumbers, action) { switch(action.type) { case SUPPORT.GET_INDEX_SUCCESS: { if (action.index.phoneNumbers) { return action.index.phoneNumbers.slice(); } return state; } default: return state; } }
The state param for this function is only the state.support.phoneNumbers slice of the Redux store.
It's best to define what your idea of the initial app state is, so reducers can define their slice of the default state. This project defines that in /reducers/initialState.
So when adding newly nested pieces to the store, be sure to import them through to the main /reducers/index.js module. Also note that when using combineReducers, you are defining the name of that slice of state, so by changing phoneNumbers like so below:
import phoneNumbers from './phoneNumbers'; export default combineReducers({ phones: phoneNumbers, });
Accessing the final store will result in state.support.phones instead of state.support.phoneNumbers
0 notes