-
Notifications
You must be signed in to change notification settings - Fork 1
Exemplary Control Flow
Relevant files:
src/pages/rotation_create.js
src/components/group_form.js
src/actions/rotations.js
src/reducers/root.js
-
The user clicks the "Create Rotation" button on this page:
-
The button's
onClick
handler is executed:<button type="submit" className="btn btn-primary btn-lg" onClick={this.props.onSubmit} // <-- this line disabled={!validity.every(i=>i)} > {this.props.submitName} </button>
which executes the
GroupForm
'sonSubmit
prop, passed down fromRotationCreate
:<GroupForm deadlines = {this.state.deadlines} rotationHeader = {this.renderRotationHeader()} submitName = "Create Rotation" updateDeadline = {(deadlineName, date) => { this.setState((state, props) => { const newDeadline = update(state.deadlines[deadlineName], {$merge: {value: date}}); return update(state, { deadlines: {$merge: {[deadlineName]: newDeadline}} }); }); }} onSubmit = {() => {this.onSubmit()}} // <-- this line afterSubmit = {() => {}} />
which executes
RotationCreate.onSubmit
:onSubmit() { const deadlines = Object.keys(this.state.deadlines).reduce((obj, x) => { obj[x] = this.state.deadlines[x].value.format("YYYY-MM-DD"); return obj; }, {}); deadlines.series = this.state.series; deadlines.part = this.state.part; this.props.createRotation(deadlines).then(() => { this.props.history.push("/"); }); }
-
RotationCreate.onSubmit
calls theRotationCreate
'screateRotation
prop, which was injected by React Redux (see the docs onmapDispatchToProps
– this component uses the object shorthand form, but many components also use the function form):import { connect } from 'react-redux'; import {createRotation} from '../actions/rotations'; // ... const mapDispatchToProps = { createRotation, // <-- this line }; export default connect( mapStateToProps, mapDispatchToProps )(RotationCreate);
which resolves to
createRotation
fromsrc/actions/rotations.js
:export function createRotation(rotation) { return function (dispatch) { return axios.post(`${api_url}/api/series`, rotation).then(response => ( Promise.all([ dispatch(fetchLatestRotation()), dispatch(fetchRotationYears()), ]) )); }; }
-
createRotation
returns a thunk, which is intercepted and executed immediately by Redux Thunk. -
The thunk returned by
createRotation
makes a POST request to/api/series
with the new rotation.
-
The
/api/series
route and POST method map to thecogs.routes.api.rotations.create
handler:app.router.add_post('/api/series', api.rotations.create)
so that function is invoked on the server.
-
The request handler retrieves the data describing the rotation from the request, checks that there is no existing rotation with this series and part, then:
- creates the rotation
- stores it in the database
- sends out emails announcing the rotation
- schedules the jobs necessary to change the rotation's state over time
- returns HTTP 201 ("Created")
-
The POST request finishes, so the promise returned by
axios.post(...)
resolves, and the chained function is executed:export function createRotation(rotation) { return function (dispatch) { return axios.post(`${api_url}/api/series`, rotation).then(response => ( // execution resumes here Promise.all([ dispatch(fetchLatestRotation()), dispatch(fetchRotationYears()), ]) )); }; }
-
This function executes
fetchLatestRotation
andfetchRotationYears
(both operate in a very similar fashion, so onlyfetchLatestRotation
is explained here):export function fetchLatestRotation() { return function (dispatch) { dispatch(requestRotations(1)); dispatch(requestLatestRotation()); return axios.get(`${api_url}/api/series/latest`).then(response => { const rotation = response.data; dispatch(receiveRotation(rotation)); dispatch(receiveLatestRotation(rotation.data.id)); }); } }
-
As explained previously, the Redux Thunk indirection takes place.
-
The
requestRotations
andrequestLatestRotation
actions are dispatched:export const REQUEST_ROTATIONS = 'REQUEST_ROTATIONS'; export const REQUEST_LATEST_ROTATION = 'REQUEST_LATEST_ROTATION'; export function requestRotations(noRotations) { return { type: REQUEST_ROTATIONS, noRotations } } function requestLatestRotation() { return { type: REQUEST_LATEST_ROTATION, } }
These are ordinary Redux actions, so the reducer is invoked to deal with them:
const rootReducer = combineReducers({ projects, users, rotations, emails, });
-
combineReducers
invokes all four reducers it is passed, but only therotations
reducer knows how to deal with the rotation-related actions (the other reducers just return the state passed to them):function rotations(state={ fetching: 0, rotations: {}, yearList: [], latestID: null }, action) { switch (action.type) { case REQUEST_ROTATIONS: // <-- this case first return update(state, { fetching: {$set: state.fetching + action.noRotations} }); // ... default: // <-- and then this case on the second time around, for REQUEST_LATEST_ROTATION return state; } }
-
The global state is updated by Redux to the new state returned by the reducer (that is,
state.rotations.fetching
is incremented). -
Any components which declare a dependency on
state.rotations.fetching
in theirmapStateToProps
are updated by React Redux. -
axios.get(...)
is executed, and a GET request is made to/api/series/latest
.
-
The
/api/series/latest
route and GET method map to thecogs.routes.api.rotations.latest
handler:app.router.add_get('/api/series/latest', api.rotations.latest)
so that function is invoked on the server.
-
latest
just serves a (307 Temporary) redirect to the latest rotation:db = request.app["db"] latest = db.get_most_recent_group() return HTTPTemporaryRedirect(f"/api/series/{latest.series}/{latest.part}")
which is handled by
cogs.routes.api.rotations.get
:app.router.add_get('/api/series/{group_series}/{group_part}', api.rotations.get)
-
get
retrieves the specified (series/part) rotation from the database, and if the user is allowed to view the rotation (if they are staff or the rotation has been made visible to students), returns the serialised rotation.
-
The GET request finishes, so the promise returned by
axios.get(...)
resolves, and the chained function is executed:export function fetchLatestRotation() { return function (dispatch) { dispatch(requestRotations(1)); dispatch(requestLatestRotation()); return axios.get(`${api_url}/api/series/latest`).then(response => { // execution resumes here const rotation = response.data; dispatch(receiveRotation(rotation)); dispatch(receiveLatestRotation(rotation.data.id)); }); } }
-
The
receiveRotation
andreceiveLatestRotation
actions are dispatched:export const RECEIVE_ROTATION = 'RECEIVE_ROTATION'; export const RECEIVE_LATEST_ROTATION = 'RECEIVE_LATEST_ROTATION'; export function receiveRotation(rotation) { return { type: RECEIVE_ROTATION, rotation } } function receiveLatestRotation(rotationID) { return { type: RECEIVE_LATEST_ROTATION, rotationID } }
-
Again, the global state is updated by Redux to the new state returned by the reducer:
function rotations(state={ fetching: 0, rotations: {}, yearList: [], latestID: null }, action) { switch (action.type) { // ... case RECEIVE_ROTATION: return update(state, { fetching: {$set: state.fetching-1}, rotations: {$merge: {[action.rotation.data.id]: action.rotation}} }); case RECEIVE_LATEST_ROTATION: return update(state, { latestID: {$set: action.rotationID} }); // ... } }
-
Any components which need updating (see
mapStateToProps
) are updated by React Redux. -
The promise returned by
createRotation
resolves, so the chained function inRotationCreate.onSubmit
runs:onSubmit() { const deadlines = Object.keys(this.state.deadlines).reduce((obj, x) => { obj[x] = this.state.deadlines[x].value.format("YYYY-MM-DD"); return obj; }, {}); deadlines.series = this.state.series; deadlines.part = this.state.part; this.props.createRotation(deadlines).then(() => { // execution continues here this.props.history.push("/"); }); }
-
The
history
prop (provided by React Router) is used to redirect the user to/
(at which point the React Router route-matching machinery takes over, and will unmount theRotationCreate
and mount theMainPage
in its place).