-
Install
node.js
-
Clone this repo
$> git clone git@github.com:cinema6/showcase.git
-
Connect to the Reelcontent VPN
-
Install Dependencies
npm install
npm test
npm run tdd
It is also possible to only watch and run a subset of unit tests with the --only
param. Unlike just using fdescribe
in a test file, --only
will cause browserify
/babel
to only transpile the relevant files, making the initial and subsequent builds much faster:
# Only build/run component tests
npm run tdd -- --only "*.component.ut.js"
npm start
-
Always use
redux-actions
to create actionsNO:
const DO_STUFF = 'DO_STUFF'; export function doStuff(payload) { return { type: DO_STUFF, payload: payload }; }
YES:
import { createAction } from 'redux-actions'; const DO_STUFF = 'DO_STUFF'; export const doStuff = createAction(DO_STUFF);
Redux middleware changes the way the store
's dispatch()
method functions, so it's important to understand all the middleware being used so that you are able to follow the flow of the application.
showcase
uses the following redux middleware:
1. fsa-thunk
This middleware allows you to pass a Function
(or thunk) to dispatch()
that it will call execute. Your Function
will be invoked with two arguments: dispatch
(Function
, so that you can dispatch other actions) and getState
(Function
so that you can use the store's state
in your logic.)
To create a thunk, you must wrap your action creator Function
in createThunk()
. Your action creator Function
should return a Function
(the thunk.)
Most importantly, the return
value of your thunk Function
will become the return
value of dispatch()
. This is very important as it allows the chaining of Promise
es.
Example:
import { createThunk } from 'src/middleware/fsa_thunk';
export const doStuff = createThunk(function() { // The action creator
return function thunk(dispatch, getState) { // The thunk
return dispatch(doAsyncStuff())
.then(dispatch(doMoreAsyncStuff());
};
});
store.dispatch(doStuff()).then(() => console.log('Stuff is done!'));
This middleware will dispatch()
actions that track the state of a Promise
when the Promise
is provided as the payload
of a Flux Standard Action (FSA).
Important: When you dispatch an FSA whose payload
is a Promise
, an action with the type
you provided will never actually be dispatch()
ed. Instead, three new actions will be dispatched for you:
- YOUR_TYPE_PENDING:
dispatch()
ed immediately - YOUR_TYPE_FULFILLED:
dispatch()
ed when thePromise
is fulfilled, with thePromise
's fulfillment value as itspayload
- YOUR_TYPE_REJECTED:
dispatch()
ed when thePromise
is rejected, with thePromise
's rejection reason as itspayload
For example:
function doSomethingAsync() {
return new Promise(resolve => {
setTimeout(() => resolve('Done after 1 sec.'), 1000);
});
}
export const DO_SOMETHING = 'DO_SOMETHING';
function doSomething() {
return {
type: DO_SOMETHING,
payload: doSomethingAsync() // this returns a Promise
};
}
store.dispatch(doSomething()); // A DO_SOMETHING_PENDING action is immediately dispatch()ed
// After one second, DO_SOMETHING_FULFILLED is dispatched
Where this can get interesting is when you remember that dispatch()
can return a Promise
:
import { getStuff } from '../actions/get_stuff'; // action creator that will cause dispatch() to return a Promise
export const DO_SOMETHING = 'DO_SOMETHING';
function doSomething() {
// Return a Function thanks to redux-thunk.
return function thunk(dispatch) {
// dispatch the DO_SOMETHING action
dispatch({
type: DO_SOMETHING,
payload: dispatch(getStuff()) // Dispatch getStuff() action: this returns a Promise
});
};
}
store.dispatch(doSomething()); // A DO_SOMETHING_PENDING action is immediately dispatch()ed
// DO_SOMETHING_FULFILLED is dispatch()ed as soon as the Promise returned by
// dispatch(getStuff()) is fulfilled.
This middleware ensures that dispatch()
always returns a Promise
so you can dispatch().then()
with confidence.
Additionally, it will make sure the Promise
it returns is rejected if it recieves a Flux Standard Action (FSA) representing an error
.
Example:
export const DO_SOMETHING = 'DO_SOMETHING';
function doSomething() {
return {
type: DO_SOMETHING,
payload: { foo: 'bar' }
};
}
// This action would not normally cause dispatch() to return a `Promise`, but now it does!
store.dispatch(doSomething()).then(() => console.log('YAY!'));
These action creators, along with the db reducer will automatically keep the app's cache of entities in-sync and maintain a single source of truth. The location of the cache is state.db
.
This function accepts three named parameters:
type
: The type of entity (e.g.campaign
,card
,experience
, etc.)endpoint
: The plural endpoint for the collection (e.g./api/content/cards
)key
(optional): The name of the unique identifying property for each object. Defaults toid
.
The function will return an Object
with 5 action creator Function
s:
list()
: Gets all entitiesget({ id })
: Gets a single entity by idcreate({ data })
: Creates an entity with the provideddata
update({ data })
: Updates an existing entity with the provideddata
remove({ id })
: Deletes the entity with the provided id
Each action creator Function
(list
, get
, create
, update
, remove
) also includes three properties:
START
: The name of the action dispatched when the operation startsSUCCESS
: The name of the action dispatched when the operation succeedsFAILURE
: The name of the action dispatched when the operation fails
Each SUCCESS
action's payload
will be an Array
containing the ids of the items operated on. This is true even for operations that operate on a single item; the single item's id
will be the sole member of the Array
.
Example:
const experience = createDbActions({
type: 'experience',
endpoint: '/api/content/experiences'
});
store.dispatch(experience.get({ id: 'e-jfruwe9ryf478' })); // Get a single item
Creates the reducer for state.db
. This app already has a db reducer.
The keys of the reducerMap
Object
should match up with an entity type
. The value should be a reducer Function
that can be used to add custom behavior to the entity cache.
Example:
export default createDbReducer({
experience: (state, action) => {
switch (action.type) {
case 'MY_CUSTOM_ACTION':
return { /* the new state of the cache */ };
default:
return state;
}
}
});
Creates the reducer that will manage the transient state of page components. Each key of the reducerMap
should be the path
(unique identifier) of a page component. Each value should be a reducer function for that page. This app already has a page reducer.
When a page component is rendered, its corresponding reducer will be called with a state
of undefined
to get the initial page state. Subsequently, the reducer will be called with every dispatched action until the page component is destroyed. A page's reducer is not called if the page is not on the screen.
Injects the page
state from redux as a prop into the component it wraps. The path
named property should correspond to the name of a configured page reducer.
Example:
Page reducer:
createPageReducer({
myPage: (state = { name: 'RC' }, action) => { // <--- Same name as path in pageify()
switch (state.action) {
// Handle other actions
default:
return state;
}
}
});
Component:
class MyPage extends Component {
render() {
const { page } = this.props;
return <div>My name is {page.name}!</div>;
}
}
export default pagify({
path: 'myPage' // <----- Same name as key in pageReducer
})(MyPage);