Upload
indeedeng
View
569
Download
1
Embed Size (px)
Citation preview
Indeed My JobsA case study in ReactJS and Redux
Gaurav MathurSoftware Engineer
I help people get jobs.
What happens next?
Following up
Scheduling an interview
Preparing an interview
Negotiating your offer
Making a decision
Why React?
Virtual DOM
JSX
Data flow
MainDisplay
ViewList
JobTable
JobRow
MainDisplay
ViewListJobTable
JobRow
MainDisplayState: jobs, counts
ViewListJobTable
JobRow
Props: counts
MainDisplayState: jobs, counts
ViewListJobTable
JobRow
MainDisplayState: jobs, counts
ViewListJobTable
JobRow
Props: jobs, updateHandler
MainDisplayState: jobs, counts
ViewListJobTable
JobRow
Props: job, updateHandler
JobRow
MainDisplay
ViewListJobTable
this.props.handleUpdate(job, state)
this.props.handleUpdate(job, state)
JobRow
MainDisplay
ViewListJobTable
this.setState({jobs:newJobs, counts:newCounts});
JobRow
MainDisplay
ViewListJobTable
this.render(); //automatically fired
JobRow
MainDisplay
ViewListJobTable
Component
Component
Component
Component
this.props.handleUpdate(job, state)
this.props.handleUpdate(job, state)
this.props.handleUpdate(job, state)
Flux
View
View
Action
View
Dispatcher
Action
View
Dispatcher
StoreAction
View
Dispatcher
StoreAction
Transition the job
Transition Job
Job Row
Action = { type: JobActions.APPLY, job: job};
Dispatch the action
MyJobsDispatcher.handleAction({ type: JobActions.APPLY, job: job});
Transition Job
Dispatcher
Job Row
function handleChange(action) { ... switch (action.type) { case JobActions.APPLY: job.set('state', States.APPLIED); break; ... } JobStore.emitChange();}JobStore.dispatchToken = MyJobsDispatcher.register(handleChange);
Update Job Store
Transition Job
Dispatcher
Job Store
Job Row
function handleChange(action) { MyJobsDispatcher.waitFor( [JobStore.dispatchToken] ); ... JobCountsStore.emitChange();}
Update Job Counts
Transition Job
Dispatcher
Job StoreJob Count Store
Job Row
ListenableStore.prototype = { emitChange() { for (let i = 0; i < this.listeners.length; i++) { this.listeners[i](); } }, ...}
Listen for changes
const changeAggregator = new ChangeAggregator( MyJobsDispatcher, storesToListenTo);const MainDisplay = React.createClass({ componentDidMount() { changeAggregator.addChangeListener(this.update); } ...});
Collect changes
const MainDisplay = React.createClass({ ... update() { this.setState({ jobs: JobStore.getJobs(), counts: JobCountStore.getCounts(), ... }); }});
Transition Job
Dispatcher
Job StoreJob Count Store
Job Row
View List Job Table
Update views
Shortcomings
Boilerplate
ListenableStore
ChangeAggregator
MyjobsDispatcher
Best Practices & Testability
// TODO: Should this be in a different place..?window.scrollTo(0, 0);ViewStore.emitChange();
ViewStore.js
Borrowed from: https://twitter.com/denisizmaylov/status/672390003188703238
Jing WangSoftware Engineer
I help people get jobs.
Borrowed from: https://twitter.com/denisizmaylov/status/672390003188703238
Single source of truth
Application State Tree
State is read only
Dispatcher & Actions
Pure functions
Reducers
Redux Store
Single source of truthconst createStore = (reducer) => { let state = {}; const getState = () => state; let listeners = []; const subscribe = (listener) => { listeners.push(listener); } const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; return {getState, dispatch, subscribe};}
State is read onlyconst createStore = (reducer) => { let state = {}; const getState = () => state; let listeners = []; const subscribe = (listener) => { listeners.push(listener); } const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; return {getState, dispatch, subscribe};}
Changes are made with pure functionsconst createStore = (reducer) => { let state = {}; const getState = () => state; let listeners = []; const subscribe = (listener) => { listeners.push(listener); } const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; return {getState, dispatch, subscribe};}
Emit changesconst createStore = (reducer) => { let state = {}; const getState = () => state; let listeners = []; const subscribe = (listener) => { listeners.push(listener); } const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; return {getState, dispatch, subscribe};}
Single reducerconst createStore = (reducer) => { let state = {}; const getState = () => state; let listeners = []; const subscribe = (listener) => { listeners.push(listener); } const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; return {getState, dispatch, subscribe};}
const myjobsReducer(state = new AppState(), action) => { state = state.set('jobs', jobsReducer(state.jobs, action)); state = state.set('jobCounts', jobCountsReducer(state.jobCounts, action, state.jobs)); ... return state;}
Combining reducers
const myjobsStore = createStore(myjobsReducer);<MainDisplay store={myjobsStore} .../>
Store as an explicit prop
const myjobsStore = createStore(myjobsReducer);<MainDisplay store={myjobsStore} .../><ViewList store={this.props.myjobsStore} .../>
Store as an explicit prop
const myjobsStore = createStore(myjobsReducer);<MainDisplay store={myjobsStore} .../><ViewList store={this.props.myjobsStore} .../><JobTable store={this.props.myjobsStore} .../>
Store as an explicit prop
react-redux Providerconst myjobsStore = createStore(myjobsReducer);<Provider store={myjobsStore}> <MainDisplay /></Provider>
MainDisplay
Provider
myjobsStore
const myjobsStore = createStore(myjobsReducer);<Provider store={myjobsStore}> <MainDisplay /></Provider>
react-redux Provider
JobRow
MainDisplay
ViewListJobTable
Provider
myjobsStore
Provided store: {myjobsStore}
connect(mapStateToProps, mapDispatchToProps, ...) {}
react-redux connect
mapStateToProps
function mapStateToProps(state) { return { jobsState: state.jobs };}
const mapDispatchToProps = (dispatch) => { return { handleAction: (action) => { dispatch(action); } };}
mapDispatchToProps
connect(mapStateToProps, mapDispatchToProps, ...) { ... return function wrapWithConnect(WrappedComponent) { class Connect extends Component { ... return Connect; }; }}
react-redux connect
react-redux connect
Connect
constructor(props, context) { this.store = props.store || context.store; this.state = { this.store.getState() };};
Connect
...componentDidMount() { this.store.subscribe(this.handleChange.bind(this));}handleChange() { this.setState({ this.store.getState() })}
react-redux connect
Connect
...render() { this.renderedElement = createElement( WrappedComponent, this.mergedProps ); return this.renderedElement;}
react-redux connect
Connect
...render() { this.renderedElement = createElement( WrappedComponent, this.mergedProps ); return this.renderedElement;}
mergedProps: { ...parentProps, ...mapStateToProps, ...mapDispatchToProps}
react-redux connect
import { connect } from 'react-redux';...const matchStateToProps => (state) { return { jobsState: state.jobs };}module.exports = connect(matchStateToProps)(JobTable);
JobTable.js
JobRow.js
import { connect } from 'react-redux';...module.exports = connect()(JobRow);
Connect
Connect Connect
Connect
JobRow
MainDisplay
ViewListJobTable
const createStore = (reducer) => {... const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); };}
myjobsStore{type:"APPLY",jobKey:"7b14...",...}
Job Row
const createStore = (reducer) => {... const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); };}
Job Row
myjobsStore
myjobsReducer
state
const createStore = (reducer) => {... const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); };}
Job Row
myjobsStore
myjobsReducer
state
newState
const createStore = (reducer) => {... const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); };}
JobTable
Job Row
myjobsStore
myjobsReducer
state
newState
jobReducerJobStore
Flux stores to Redux reducers
...
Flux Redux
jobCountsReducerJobCountsStore
... store.subscribe(() => { if (view !== newView) { window.scrollTo(0, 0); } });
Side effects in separate handlers
Less boilerplate code
createStoreListenableStoreChangeAggregatorMyJobsDispatcher
Flux ReduxFlux Redux
Less boilerplate code
connectaddChangeListener(
)JobStore.getJobs()
Flux Redux
Less boilerplate code
combineReducerswaitFor()
Flux Redux
Easier to unit test
Well unit tested state transition logic
Less code to maintain
Code easier to follow
Results
engineering.indeed.com
www.indeed.jobs