-
Notifications
You must be signed in to change notification settings - Fork 1
Exemplary Control Flow
Relevant files:
src/pages/rotation_create.jssrc/components/group_form.jssrc/actions/rotations.jssrc/reducers/root.js
-
The user clicks the "Create Rotation" button on this page:

-
The button's
onClickhandler 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'sonSubmitprop, 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.onSubmitcalls theRotationCreate'screateRotationprop, 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
createRotationfromsrc/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()), ]) )); }; }
-
createRotationreturns a thunk, which is intercepted and executed immediately by Redux Thunk. -
The thunk returned by
createRotationmakes a POST request to/api/serieswith the new rotation.
-
The
/api/seriesroute and POST method map to thecogs.routes.api.rotations.createhandler: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
fetchLatestRotationandfetchRotationYears(both operate in a very similar fashion, so onlyfetchLatestRotationis 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
requestRotationsandrequestLatestRotationactions 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, });
-
combineReducersinvokes all four reducers it is passed, but only therotationsreducer 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.fetchingis incremented). -
Any components which declare a dependency on
state.rotations.fetchingin theirmapStateToPropsare updated by React Redux. -
axios.get(...)is executed, and a GET request is made to/api/series/latest.
-
The
/api/series/latestroute and GET method map to thecogs.routes.api.rotations.latesthandler:app.router.add_get('/api/series/latest', api.rotations.latest)
so that function is invoked on the server.
-
latestjust 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)
-
getretrieves 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
receiveRotationandreceiveLatestRotationactions 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
createRotationresolves, so the chained function inRotationCreate.onSubmitruns: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
historyprop (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 theRotationCreateand mount theMainPagein its place).