React - Putting it all together
Presented by
Guyllaume Cardinal
and
Guyllaume Cardinal
Partner at Majisti, brand creator and Frontend expert
Majisti 3 years, Web dev 7 years
React for more than a year
Elaborated all of Majisti's frontend stack
- Quality, scalability, service, maintanability
- Custom Web application and platforms development
- Consulting and team coaching
We also blog (sometimes)!
http://booborguru.com/
contact@majisti.com
514-316-9092
Today's schedule
- Disclaimers (nothing too bad, don't worry!)
- Libraries used
- An explanation of how we wire it all together
- Conclusion
Before We Begin
A few disclaimers
- My examples use TypeScript
- This talk assumes you already know React
- This is just the setup I'm using right now, nothing more
- I don't have the absolute truth (though I wish I did!)
Before We Begin
Missing stuff from this talk
- Testing won't be covered
- I won't explain Webpack (there's already a ton of really good resources)
- Continuous Integration/Continuous Delivery
- Choice of backend
- Docker setup
One more thing
The usual React warnings
It's kind of a React tradition at this point.
- It's a view layer, nothing more
- It does not care where data comes from, how it's sent or transformed within the app
- It could be swapped out and replaced with any other view layer out there
-
You will need to figure out your "framework"
Everything in this talk is the result of:
- Our needs as a business
- What our clients expects out of our products
- Bias towards our own opinions and the way we work
One more More thing
It is but Majisti's Framework
It should still give you a good idea of where to start when working with React!
Oh, last thing, I promise!
Sample "project"
Now for the fun part
What do we use?
- React (obviously)
- Redux
- React Router (and react-router-redux)
- Reselect
- InversifyJS
- react-css-modules
Real World App
Rocket LEague Custom Training
Filter training codes for the Rocket League game
by Guyllaume Cardinal and Steven Rosato
Real World App
b2bup
Connecting businesses together at events.
by Majisti
A view layer. It helps you build user interfaces in a reusable way
I highly recommend Rami Sayar's talk: https://vimeo.com/189677855
REact
Why we love it
- Pretty small size
- Thriving community
- Backed by Facebook
- Tiny API footprint: Coding for React is simple
- Tooling is amazing (hot reloading, so sexy <3)
- Never having to code a date picker again? Count me in!
REact
Alternatives and competition
- Meteor's Blaze templating
- Angular's directives
- vue.js
- Polymer
- Mithril
- Preact
handle application state and how it changes with user interaction
Redux
The store
Central point of your app.
- Any and all state change goes through the store via store.dispatch()
- The current state can be fetched with store.getState()
import {createStore} from 'redux'
import RootReducer from 'RootReducer'
const initialState = {}
const store = createStore(RootReducer, initialState)
Redux
The State
POJO (Plain old JavaScript object). Describes your application's current state.
{
user: {
firstName: "Guyllaume",
lastName: "Cardinal",
},
noteItems: {
"92ns812m04": {
content: "Do HTML5mtl talk",
done: false,
category: "91n236bws6",
}
}
noteCategories: {
"91n236bws6": {
name: "Reminders",
color: "f7f2d5",
}
}
}
Redux
The State
You should aim to keep it as flat as possible
- Updating a deep nested object will make it difficult to keep the state's immutability
- Updating a low level value will mean a deeply nested property will be considered as changed, causing useless renders
- Accessing data will get ugly after a while
- Refactoring will get even uglier
Redux
Connected React Components
Using redux's connect function, we can access the state and inject props into our React components
export class UserSummary extends React.Component {
public render() {
return (
<div>
<p>{this.props.firstName}</p>
<p>{this.props.lastName}</p>
</div>
)
}
}
export function mapStateToProps(state) {
return {
firstName: state.user.firstName,
lastName: state.user.lastName,
}
}
export default connect(mapStateToProps)(UserSummary)
Redux
Actions
- POJO
- Describes what happened, not how
- They have no required format, the only thing that Redux requires is that they have a type property.
{
type: "USER_LOGGED_OUT",
}
Redux
Actions
There is a standard that emerged: flux standard actions
{
type: "ADD_TODO",
payload: {
content: 'Do something',
completed: false,
title: null,
},
error: false,
meta: {
text: "Intended for any extra information that is not part of the payload"
}
}
Redux
Action creators
They create and return the action, maybe doing some work beforehand
export default class UserActionCreator {
public requestNewUserProfileSuccess(user: UserResponse) {
return {
payload: {
firstName: user.profile.firstName,
id: user.id,
lastName: user.profile.lastName,
},
type: "REQUEST_NEW_USER_PROFILE_SUCCESS",
}
}
}
Redux
Reducers
Pure functions. They wait for actions and return a new state, with the action's payload merged
export default function UserReducer(state = null, action) {
switch (action.type) {
case "REQUEST_NEW_USER_PROFILE_SUCCESS":
state[action.payload.id] = action.payload
return Object.assign({}, state)
default:
return state
}
}
- Reducers must always return something, never undefined
- They must be pure function
Redux
Reducers
import {combineReducers} from 'redux'
export function idReducer(state = null, action) {
switch (action.type) {
/** Decide if we reducer or not **/
}
}
export function profileReducer(state = null, action) {
switch (action.type) {
/** Decide if we reducer or not **/
}
}
export default combineReducers({
id: userIdReducer,
profile: profileReducer,
})
{
id: '123',
profile: {
firstName: 'Guyllaume',
lastName: 'Cardinal',
},
}
Redux
Reducers
// RootReducer.ts
import UserReducer from '...'
import NoteItemsReducer from '...'
import NoteCategoriesReducer from '...'
export default combineReducers({
user: UserReducer,
noteItems: NoteItemsReducer,
noteCategories: NoteCategoriesReducer,
})
// UserReducer.ts
export function idReducer(state, action) {/* Some code */}
export function profileReducer(state, action) {/* Some code */}
export default combineReducers({
id: idReducer,
profile: profileReducer,
})
// Resulting state
{
user: {
id: '123',
profile: {
firstName: 'foo',
lastName: 'bar',
}
},
noteItems: {},
noteCategories: {},
}
Composed to create a root reducer
Redux
Binding it all together
Now we have:
- A store, built with a root reducer and an initial state
- Action creators that can return new actions
- Connected React components that fetch the current application's state using connect()
- Reducers that will react to certain action types and return a new state accordingly
Redux
Binding it all together
Introducing dispatch(). That's how you send your actions to the store so reducers can act on them.
const initialState = {}
const store = createStore(RootReducer, initialState)
store.dispatch({type: 'ADD_TODO'})
store.dispatch(UserActionCreator.requestNewUserProfile())
Redux
Binding it all together
export class UserSummary extends React.Component {
public render() {
return (
<div>
<p>Hello HTML5mtl</p>
<a onClick={this.props.onLogoutClick}>Logout</a>
</div>
)
}
}
export function mapDispatchToProps(dispatch) {
return {
onLogoutClick: dispatch(UserActionCreator.userLogout()),
}
}
export default connect(null, mapDispatchToProps)(UserSummary)
Redux
What have we achieved?
- Components are decoupled from the state
- Interactions with Redux is also decoupled
- State is a POJO that is never mutated, allowing for awesome tools
Redux
Tooling - Redux dev tools
- Inspect state and actions being dispatched
- Go back in time by "canceling" actions
- Hot reloaded code gets re-evaluated with the current actions
- Much, much, more
Redux
Tooling - Redux logger
Redux
Tooling - redux-thunk
- It's a middleware
- Allows an action creator to return a function
- Will be passed the store's dispatch
function fetchUserProfile(userId: string): Function {
return dispatch => {
fetch(`${api}/userProfile/${userId}`)
.then(profile => {
dispatch({type: 'FETCH_SUCCESS', payload: profile})
})
}
}
function saveAndClose(): Function {
return dispatch => {
dispatch(this.save())
dispatch(this.close())
dispatch(NavigationActionCreator.returnHome())
}
}
Redux
A bit of sidetracking - Middlewares
- Similar to Express or Koa middlewares (and a lot of other libraries)
- Extension to the store, acts between the dispatching of an action and that action reaching the reducers
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import RootReducerfrom 'RootReducer'
const initialState = {}
const store = createStore(
RootReducer,
initialState,
applyMiddleware(thunk),
)
Redux
Taking it all in
- Redux becomes the core of the application. A lot goes through its workflow
- Thinking in Redux might take a while
- I suggest doing (not just reading) the examples on Redux's documentation. Really helped it click for me
Redux
Alternatives
- MobX
- Jumpsuit (kind of a Redux superset)
- Meteor (using Meteor, but preferring React rather than Blaze)
- Angular (again, using React as the view)
Take a deep breath
CSS Modules, but with some added sugar for ease of use with React
CSS Modules?
- It's the idea of bundling CSS with your code and scoping the resulting CSS to the file that imports it
- Allows inheritance of styles and importing of partial styles
- Requires the use of a module bundler like Webpack (and a few loaders)
CSS Modules?
import React from 'react'
import styles from './table.css'
export default class Table extends React.Component {
render () {
return <div className={styles.table}>
<div className={styles.row}>
<div className={styles.cell}>A0</div>
<div className={styles.cell}>B0</div>
</div>
</div>
}
}
// Resulting HTML
<div class="table__table___32osj">
<div class="table__row___2w27N">
<div class="table__cell___1oVw5">A0</div>
<div class="table__cell___1oVw5">B0</div>
</div>
</div>
react-css-modules
react-css-modules
You're using CSS?
- We already know CSS
- As portable as a component built with Radium or the likes
- No extra markup; in your code and the resulting HTML
- Results in a single stylesheet, helps when loading the page
SASS actually, but yes. Here's why:
REact-css-modules
- You have to use camelCase CSS class names
- You have to use styles.className to define the className prop
- Reference to an undefined CSS Module resolves to undefined without a warning
CSS Modules aren't good enough
import React from 'react'
import CSSModules from 'react-css-modules'
import styles from './table.scss'
@CSSModules(styles)
export default class Table extends React.Component {
render () {
return <div styleName='table'>
<div styleName='row'>
<div styleName='cell'>A0</div>
<div styleName='cell'>B0</div>
</div>
</div>
}
}
REact-css-modules
/** table.scss **/
.table {
background: blue;
}
.row {
border: purple dotted 6px;
}
.cell {
background: pink;
}
import Table from 'Table'
import styles from './extendedTable.scss'
<Table styles={styles} />
REact-css-modules
Extending Styles
What if we want to override that previous table's style?
import * as React from 'react'
import * as classNames from 'classnames'
import * as CSSModules from 'react-css-modules'
import * as styles from './styles.scss'
@CSSModules(styles)
export default class SubmitButton extends React.Component {
public render() {
const buttonStyles = classNames({
button: true,
error: this.props.hasError,
})
return <button styleName={buttonStyles}>Submit</button>
}
}
REact-css-modules
Conditional Styles
Enter classNames. Small utility to determine styles based on any condition you may have
@import "palette";
.button {
color: $primaryColor;
& .error {
color: $errorColor;
}
}
REact-css-modules
Theming
Basically the same as theming with SASS
REact-css-modules
- Regular inline styles (but, not really)
- Radium
- styled-components
- JSS
Alternatives
complete routing library for React
React Router
<Router history={browserHistory}>
<Route path="/" component={App}>
<Route path="about" component={About}/>
<Route path="users" component={Users}>
<Route path="/user/:userId" component={User}/>
</Route>
<Route path="*" component={NoMatch}/>
</Route>
</Router>
What does it look like?
React Router
Integration with Redux
- react-router can be used as is
- react-router-redux allows us to keep URL and state in sync
- react-router-redux is very "setup and forget"
import {Router, Route, browserHistory} from 'react-router'
import {syncHistoryWithStore, routerReducer} from 'react-router-redux'
const store = createStore(
combineReducers({
...reducers,
routing: routerReducer
})
)
const history = syncHistoryWithStore(browserHistory, store)
ReactDOM.render(
<Router history={history}>{/* Routes */}</Router>,
document.getElementById('mount')
)
React Router
Navigation
import {Link, browserHistory} from 'react-router'
export default class Menu extends React.Component {
render() {
return (
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/news">News</Link></li>
<li><a onClick={this.props.logoutClickHandler}>Logout</a></li>
</ul>
)
}
}
export function mapDispatchToProps(dispatch) {
return {
logoutClickHandler: () => {
dispatch(UserActionCreator.logout())
browserHistory.push('/')
}
}
}
export default connect(null, mapDispatchToProps)(Menu)
React Router
Complex Views
<Router history={browserHistory}>
<Route path="/" component={BaseLayout}>
<Route path="about" components={{
header: MainHeader,
content: AboutContent,
footer: Footer,
}}/>
</Route>
</Router>
export default BaseLayout extends React.Component {
render() {
return (
<div className="header">{this.props.header}</div>
<div className="content">{this.props.content}</div>
<div className="footer">{this.props.footer}</div>
)
}
}
React Router
Something of note
Use Redux's mapStateToProps() to map react-router props to your components.
// Say we have a route "/search?num=20"
export SearchView extends React.Component {
render() {
return <SearchResults numToDisplay={this.props.numberOfResults} />
}
}
function mapStateToProps(state, ownProps) {
return {
numberOfResults: ownProps.location.query.num
}
}
export default connect(mapStateToProps, null)(SearchView)
React Router
That's about it!
For the basics anyway. react-router can handle:
- Styling the active route
- Confirming navigation
- Server-side rendering
- Code splitting and lazy loading (only load the JS required for the current view)
Reselect
Selector Library to help with relational data and derived data
( No logo)
- Allows the creator of selectors and caching so that no value is recalculated without need
- Makes it easy to flatten the Redux state without sacrificing relational data
- Bonus, it also further decouples components from your state
Reselect
Reselect
Reselect
Simple example
import { createSelector } from 'reselect'
export const profilesSelector = state => state.profiles
export const usersSelector = state => state.users
export const currentUserIdSelector = state => state.currentUserId
export const currentUserSelector = createSelector(
[usersSelector, profilesSelector, currentUserIdSelector],
(users, profiles, currentUserId) => {
let user = users[currentUserId]
user.profile = profiles[user.profileId]
return user
}
)
createSelector(...inputSelectors | [inputSelectors], resultFunc)
Reselect
With Redux
import {currentUserSelector} from 'selectors/UserSelectors'
// The view component here somewhere
function mapStateToProps(state) {
return {
user: currentUserSelector(state),
}
}
Notice that mapStateToProps() is no longer bound to the state shape
Reselect
PAssing props to a selector
import {createSelector} from 'reselect'
profilesSelector = state => state.profiles
profileByIdSelectorCreator = (id) => {
return createSelector(
[profilesSelector],
(profiles) => {
return profiles[id]
}
)
}
// Use it like this
function mapStateToProps(state) {
const profileByIdSelector = profileByIdSelectorCreator('72jbkaj292ka0')
return {
userProfile: profileByIdSelector(state),
}
}
Reselect
In summary
We use it because it simplifies and optimizes computed values
We also like that it decouples our views from the state
Dependency injection
(aka Inversion of control)
Inversifyjs
Instead of importing concrete helper methods and services, you have a container building your services allowing you to abstract them to your application by working with interfaces instead
// BEFORE
import * as rest from 'rest'
export default class UserActionCreator {
public getProfile() {
return dispatch => {
rest('/profile')
.then(response => {
dispatch(getProfileSuccess(response))
})
}
}
}
// AFTER
import {RestClient} from 'domain/interfaces/RestClient'
export default class UserActionCreator {
private restClient: RestClient
public constructor(restClient: RestClient) {
this.restClient = restClient
}
public getProfile() {
return dispatch => {
this.restClient.get('/profile')
.then(response => {
dispatch(getProfileSuccess(response))
})
}
}
}
Inversifyjs
Benefits of abstraction
- Reusability
- Readability
- Focus on what the object does, not how it does it
- Remove code duplication via abstract classes
Inversifyjs
How it works
Inversifyjs
How it works
// Types.ts
// You can also use strings, but be careful about namespacing them
export default {
RestClient: Symbol('RestClient')
ActionCreator: Symbol('ActionCreator')
}
// Somewhere in your domain
export interface RestClient {
get(resource: string, parameters: UrlParameters)
}
Define Interfaces and Types
Inversifyjs
How it works
// CujoClient.ts
import {injectable} from 'inversify'
@injectable()
class CujoClient implements RestClient {
// Whatever your client does
}
// ActionCreator.ts
import {injectable, inject} from 'inversify'
import Types from 'types'
import {RestClient} from 'RestClient'
@injectable()
export default class ActionCreator {
private restClient: RestClient
public constructor(@inject(Types.RestClient) restClient: RestClient) {
this.restClient = restClient
}
}
Configure injectable objects
Inversifyjs
How it works
// Container.ts
let container = new Container()
container.bind<RestClient>(Types.RestClient).to(CujoClient)
container.bind<ActionCreator>(Types.ActionCreator).to(ActionCreator)
export default container
// Now, anywhere you can
import Container from 'Container'
let actionCreator = Container.get<ActionCreator>(Types.ActionCreator)
actionCreator.doSomeAction()
Wire it all together!
Inversifyjs
Simple (?) explanations
Types: Used to identify the service we want
@injectable(): Decorator that allows InversifyJS to have access to the method it needs to create and wire the object
@inject(): Tells InversifyJS which service to bind to the parameter
Inversifyjs
Abstraction: Concretely
Let's take the earlier ActionCreator example
import {injectable, inject} from 'inversify'
import Types from 'types'
import {RestClient} from 'RestClient'
@injectable()
export default class ProfileActionCreator {
private restClient: RestClient
public constructor(@inject(Types.RestClient) restClient: RestClient) {
this.restClient = restClient
}
public getProfile(): Function {
return dispatch => {
this.restClient.get('/profile')
.then(response => this.getProfileSuccess(response))
.catch(error => this.getProfileFailure(error))
}
}
}
Inversifyjs
// Before
let container = new Container()
container.bind<RestClient>(Types.RestClient).to(CujoClient) // CujoClient
container.bind<ProfileActionCreator>(Types.ProfileActionCreator).to(ProfileActionCreator)
export default container
Abstraction: Concretely
// After
let container = new Container()
container.bind<RestClient>(Types.RestClient).to(RestfulClient) // Notice the change
container.bind<ProfileActionCreator>(Types.ProfileActionCreator).to(ProfileActionCreator)
export default container
// Usage has not changed at all in your code
import Container from 'Container'
let profileActionCreator = Container.get<ProfileActionCreator>(Types.ProfileActionCreator)
profileActionCreator.getProfile()
Inversifyjs
Testing is easier with constructor injection
No need to use SinonJS's stubs (though it's still useful!)
describe(`ActionCreator`, () => {
let actionCreator: ActionCreator
beforeEach(() => {
const restClient = {
get: () => {user: {name: 'Guyllaume'}}
}
actionCreator = new ActionCreator(restClient)
})
it(`Should successfully fetch the user's name`, () => {
expect(actionCreator.getUserName('id').payload).to.equal('Guyllaume')
})
})
Abstraction: Concretely
Inversifyjs
Other benefits of InversifyJS
- Easy creation of factories
- Allows the elimination of static methods
- Container can be built based on environment
Overall, it really helps in following OOP's best practices (SOLID, GRASP)
The biggest part is done!
WIRING all that in a project
WIRING all that in a project
(Not like that)
WIRING all that in a project
Basics
- All the logic should be in Services and ActionCreators
- Never access props from the router or any other provider. Instead ask for specific values in your component's props
- Reducers should only determine when information is returned to the state, never how
- Never use strings, prefer string constants
Components
We have 4 types:
- The ApplicationRoot: Only one per application, it's used to wire all the application together. App.tsx in the demo project.
- Layouts: These are the various layouts the project will use. They get their children injected via React Router and they can be connected
- Sections: The components that get injected into Layouts. They can also be connected
- Components: Regular React components. They absolutely cannot be connected. We sometimes have wrapper components, see StatefulLink in the sample project.
Connecting components
You can in theory connect any component in any way, but we enforce some simple rules:
- You can never directly access the state to send props. Use selectors
- Sections and Layouts are allowed access to the Container
- There is very minimal logic in the mapStateToProps() and mapDispatchToProps() methods
- Always export the mapStateToProps() and mapDispatchToPros() methods
- When connecting a component, always export the class and export default the connected version
Reusability
There are a few places where we accept project specificity
- ActionCreators and Reducers: Most of them will not be reusable. You might be able to salvage some (like authentication), but it's rare.
- View Sections: Layouts and components will be able to be reused from project to project, but view Sections will always be project specific.
- The Container: It's possible to extend a base container with some general and basic services, but the container itself will not be reusable.
INversifyJS
InversifyJS is responsible for a few things:
- ActionCreators (including a factory for ease of use)
- Every services and utility (RestClient, AuthenticationService, Validators, Hydrators, etc)
- StoreCreator
Reducers
Two simple rules:
- Do not put any logic in reducers
- Never mutate the state. Use Object.assign({}, newState) or Array.slice(0) or the spread operator instead
Extra rule if you are using TypeScript:
- Type your reducer's action parameter
function profile(state: any = null, action: Action<User>): User {
...
}
In conclusion
- It's really just what we use, based on our needs and our client's
- We aim at maintainability and testability, our decisions reflect that
- It's probably not perfect, I'm sure our stack will change in the future (redux-thunk being a good example)
Good resources
- Redux "tutorial" - Really helped Redux click for me
- awesome-react - A constantly updated list of awesome React resources. Don't like a library we use? Check for an alternative in there!
- Reactiflux - A Discord community, tons of channels, tons of people
And finally, just... Code something. Play around!
- Quality, scalability, service, maintanability
- Custom Web application and platforms development
- Consulting and team coaching
We also blog (sometimes)!
http://booborguru.com/
contact@majisti.com
514-316-9092
Thank you! Questions?
Email: guyllaume.cardinal@majisti.com
LinkedIn: Guyllaume Cardinal
Github: @gCardinal
React - Putting it all together
By Guyllaume Cardinal
React - Putting it all together
React n'est pas un framework. On ne cesse de l'entendre, mais rarement les gens montre comment ils l’utilisent leur projets. Quel outils, librairie ou patron de conception sont les bons à utiliser? Dans cette présentation, Guyllaume Cardinal de Solutions Majisti nous explique comment ils développent leur applications et quels outils ils utilisent. Une fois la présentation terminé, vous aurez un meilleur point de départ pour vos propres projet. Et pour ceux qui sont déjà experts, vous aurez vu une façon différente de faire les choses! --- React is not a framework. We keep hearing it, but rare are those who show how they use it in their own projects. Which tool, library or design pattern is the right one? In this talk, Guyllaume Cardinal from Solutions Majisti explains how they develop their applications and what tools they use. After this talk, you will have a better starting point for your own projects. And for the already experts among you, you will have seen a different way of doing things!
- 1,638