Presented by
Guyllaume Cardinal
and
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
We also blog (sometimes)!
http://booborguru.com/
contact@majisti.com
514-316-9092
It's kind of a React tradition at this point.
You will need to figure out your "framework"
Everything in this talk is the result of:
It should still give you a good idea of where to start when working with React!
Filter training codes for the Rocket League game
by Guyllaume Cardinal and Steven Rosato
Connecting businesses together at events.
by Majisti
I highly recommend Rami Sayar's talk: https://vimeo.com/189677855
Central point of your app.
import {createStore} from 'redux'
import RootReducer from 'RootReducer'
const initialState = {}
const store = createStore(RootReducer, initialState)
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",
}
}
}
You should aim to keep it as flat as possible
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)
{
type: "USER_LOGGED_OUT",
}
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"
}
}
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",
}
}
}
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
}
}
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',
},
}
// 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
Now we have:
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())
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)
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())
}
}
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import RootReducerfrom 'RootReducer'
const initialState = {}
const store = createStore(
RootReducer,
initialState,
applyMiddleware(thunk),
)
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>
SASS actually, but yes. Here's why:
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>
}
}
/** 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} />
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>
}
}
Enter classNames. Small utility to determine styles based on any condition you may have
@import "palette";
.button {
color: $primaryColor;
& .error {
color: $errorColor;
}
}
Basically the same as theming with SASS
<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>
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')
)
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)
<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>
)
}
}
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)
For the basics anyway. react-router can handle:
( No logo)
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)
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
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),
}
}
We use it because it simplifies and optimizes computed values
We also like that it decouples our views from the state
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))
})
}
}
}
// 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
// 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
// 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!
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
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))
}
}
}
// Before
let container = new Container()
container.bind<RestClient>(Types.RestClient).to(CujoClient) // CujoClient
container.bind<ProfileActionCreator>(Types.ProfileActionCreator).to(ProfileActionCreator)
export default container
// 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()
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')
})
})
Overall, it really helps in following OOP's best practices (SOLID, GRASP)
(Not like that)
We have 4 types:
You can in theory connect any component in any way, but we enforce some simple rules:
There are a few places where we accept project specificity
InversifyJS is responsible for a few things:
Two simple rules:
Extra rule if you are using TypeScript:
function profile(state: any = null, action: Action<User>): User {
...
}
And finally, just... Code something. Play around!
We also blog (sometimes)!
http://booborguru.com/
contact@majisti.com
514-316-9092
Email: guyllaume.cardinal@majisti.com
LinkedIn: Guyllaume Cardinal
Github: @gCardinal