-
Notifications
You must be signed in to change notification settings - Fork 1
3. Infra
WelcoME is built on ReactJS, with a Firebase backend and a custom implementation of Flux based on RxJS.
There are 3 parts to almost every component in the app:
- the actual component (unaware of the app state)
- a context (aware of the app state)
- the component stories
The component is a lightweight object that has no knowledge of the current state of the app. It simply defines the type of properties it receives and what to render. We're using react-md for basic components such as text fields.
For example, a simple SignIn
class would take an email an password as properties:
const SignIn = ({email, password}) => {
return (
<div>
<TextField
id='email'
onChange={value => onChangeKey('email', value)}
value={email.value} />
<TextField
id='password'
onChange={value => onChangeKey('password', value)}
value={password.value}
type='password'/>
</div>
);
}
SignIn.PropTypes = {
email: PropTypes.string.isRequired,
password: PropTypes.string.isRequired
}
The context has access to the state of the app and is written as a wrapper over the actual component. It pulls info from the state, calls actions to modify the state and passes data as properties to the component. For example, the SignInContext
would be defined like this:
const SignInContext = (props, context) => {
const state = context.store;
const handlers = context.handlers;
const forms = state.forms.signin;
return (
<SignIn
email={forms.email}
password={forms.password} />
);
}
SignInContext.contextTypes = {
store: PropTypes.object.isRequired,
handlers: PropTypes.object.isRequired
}
Note that SignInContext
has an inner Context of its own. This is a React construct that can be used to make data visible to components without explicitly passing it via the properties. We're using the underlying context to keep the global state
and handlers
. More on this in the sections below.
The component stories are an utility provided by StorybookJS in which you can write different states for the component and render it in isolation from the rest of the app.
Some definitions:
- the store holds the current state of the data in the app
- an action is an event you can trigger to change the state.
- a reducer takes in an
action
and some data and changes the state - a handler is what components call when they want to trigger/dispatch an action
- an observer for an action is triggered when the action is dispatched, has access to the data payload that is sent with the action (like username/password), can call async services like Firebase and other handlers to act upon the response
Say you want a new component for Sign-in.
- First you need to declare the actions that the component might trigger:
registerAction('SIGNIN_EMAIL_REQUESTED');
. After this, the actions will be visible as:Actions. SIGNIN_EMAIL_REQUESTED
- Add the action to the Reducer and define how to update the state:
Reducers.auth = (state = initialState, action) => {
switch (action.type) {
[...]
case Actions.SIGNIN_EMAIL_REQUESTED:
return {
...state,
loaded: false
};
[...]
}
}
- Then add a handler for the action:
Handlers.requestSignIn = fields => dispatch(Actions.SIGNIN_EMAIL_REQUESTED, fields)
- In the component itself, define the event for submitting the sign-in form, by referring to the new handler:
[...]
requestSignIn={() =>
handlers.requestSignIn({email: forms.email, password: forms.password})
}
[...]
- Then add an observer to the Firebase service for the new action, such that we can actually sign-in:
// login with email requested
payloads$(Actions.SIGNIN_EMAIL_REQUESTED)
.subscribe(fields => {
FirebaseAuth
.signInWithEmailAndPassword(fields.email, fields.password)
.then(user => Handlers.goToPath(user.type ? '/feed' : '/profile'))
.catch(err => Handlers.errorUser('auth', 'Sign In', err))
})
We explored a bunch of solutions and settled for RxJS as the basis for our Flux model. Specifically, we have a dispatcher, which is an observable stream, whose values can be multicasted to multiple Observers. // TODO