Start by reading this Flux overview.
$ npm install
# Start server
$ npm start
# Build frontend (with watching)
$ npm run watch
Start by running the setup above, and open http://localhost:9990 and the DevTools console.
The app uses both ImmutableJS and ES6, so check out these resources:
We'll start by saving messages. Go to js/components/MessageInput.jsx
.
This component wraps the input field at the bottom of the screen. When we type
and press Enter, we should see a log in the console.
Use js/actions/MessagesActionCreator.jsx
to create an action for saving the
message. All Ajax responses are immutable objects (created with ImmutableJS),
therefore it's often helpful to add .toJS()
when logging them, e.g.
console.log(newFields.toJS());
When the save succeeds, dispatch an action using the Dispatcher
. The input to
Dispatcher.dispatch
is an object. We should always include a type
, as this
is used in the store to identify an action. Example dispatch:
Dispatcher.dispatch({
type: 'some-type',
key: 'value',
anotherKey: 42
});
You can find all the relevant types for these tasks in js/stores/MessagesStore
.
Other than type
you are free to add as many key/value pairs as you want.
Every dispatched message is logged in the MessagesStore
, so you can use this
to debug your actions.
Tips: The dispatcher docs: http://facebook.github.io/flux/docs/dispatcher.html
Tips 2: Both the channel and the message is needed in the store, so both should be sent.
There are helper methods for adding and updating messages at the bottom
of MessagesStore
.
After changing the state of the store, you must always remember to emit. Usually
this means calling MessagesStore.emitChange()
. In the next tasks we will react
to these events.
For now you can check that everything works by logging the new messages
state
in the store.
Now we want js/components/Messages.jsx
to display the saved message. We start
with listening for changes on the MessagesStore
. We do this by registering a
listener in componentDidMount
, e.g.
componentDidMount() {
SomeStore.addChangeListener(this._onChange);
}
This means that we add the listener when a component is added to the DOM. We
should always remove the listener in componentWillUnmount
, i.e. when the
component is removed from the DOM.
Tips: Learn more about React's lifecycle methods in https://facebook.github.io/react/docs/component-specs.html#lifecycle-methods
There is already an _onChange
method in the Messages
component.
This callback uses the function getStateFromStores
, which is a normal pattern
in Flux. As you can see the function is also used in getInitialState
.
In getStateFromStores
, use MessagesStore#all
to fetch all the messages on
the current channel (which is available on the props
).
The component should already handle rendering of the messages, so if you refresh and try to create some messages, they should be rendered.
Now it's time to fetch messages from the backend. Remember that ajax calls should be done in action creators, not in stores.
Usually the fetch process is as follows:
- Component asks store for data
- If no data, create an action that fetches data
- Wait while data is fetched and change event is triggered
- Update the component with the new state
After the last step the component is re-rendered automatically when the state is changed.
We start in the Messages
component. In componentDidMount
we can check if
messages are set. As this method is called after getInitialState
, we can check
if the messages received from the MessagesStore
is undefined
. If it is, we
know that we need to fetch messages from the backend.
To fetch messages we can implement the fetchAll
method in the
MessagesActionCreator
. To get the messages on the correct form, we should
call createMessage
on each message, i.e. map
over the messages.
On success we dispatch the messages and update the state in the MessagesStore
.
(Remember the helpers at the bottom. And remember to emit after change.)
Now, if you refresh you should see an initial message from the server. If you write messages then refresh, they should be received from the server.
Now we need to handle multiple channels. If we switch between the channels on
the frontend, nothing happens and it keeps all the messages from #general
.
To fix this we need to make changes to Messages.jsx
, so it fetches the new
channel's messages when switching between the channels.
The reason this doesn't work already is that React does not remove the
component from the DOM when changing the route (i.e. changing the channel).
Therefore componentDidMount
is not called when we change routes. Instead
componentWillReceiveProps
is called with the new props, which in our case is
the new channel.
We therefore need to "reset" our component state based on the new props:
componentWillReceiveProps(nextProps) {
// nextProps will contain the new channel
this.setState(...);
}
Again we can use getStateFromStores
to get the state (i.e. messages), but now
for the new channel. Remember to send in the new props to get the correct
state.
When changing state we must remember that the component is responsible for
fetching data. One way of doing this is in the callback to setState
:
this.setState(newState, function() {
if (this.state.someState == null) {
// fetch data
}
});
I.e. what happens in the callback is basically the same messages check
and action creation done in componentDidMount
.
Now if we refresh and change tabs, data for the new channel should be fetched and displayed.
It's time to handle errors when fetching messages. In MessagesActionCreator
we already handle the success case in the first callback to then
. In the
second callback we receive an error, e.g.
ajax.get('/url').then(
res => {
// handle success
},
err => {
// handle error
}
);
The task is to handle errors when fetchAll
fails. One possibility is dispatching
an action with the type receive_messages_failed
that includes the error. Then,
in the store, we can save this error the same way we set messages
, i.e. its
own object, e.g. errors
, that contain the last error for each channel.
There is already a function at the bottom of the store you can start with. Btw, remember that we use ImmutableJS, so maps are immutable:
someMap.set('key', value);
someMap.get('key') // undefined
someMap = someMap.set('key', value);
someMap.get('key') // value
With this in place, the Messages
component can fetch the error from the store
in its getStateFromStores
, and, if there is an error, display an error using
the Alert
component, e.g.
let error = this.state.error;
if (error != null) {
return <Alert>
Feilmelding som skal vises til brukeren
</Alert>
}
The easiest way to test this is by refreshing the application, stopping the backend server, then going to the other channel. This should try to load the channel's messages and fail, and it should display an error message to the user. If you then start the server again, and go back and forth between the channels, the messages should be loaded.
The server has support for sending messages via WebSockets. There is a connection
setup already in MessagesActionCreator
. If you connect when App.jsx
mounts
and implements receive_messages
in MessagesStore
, you will receive all
messages sent to the server. To test this, open a new browser window and send
some messages and see them appear in the other window.