Story from the trenches: React
How to objectively reason about your front-end stack and why we ended up with implementing our own router
and published it as open-source
About Me
My first programming book '1988
About Me
15+ Years in professional software development
Text
Martin Dimitrov
Front-end developer @
Our current dev stack:
React, Redux, MobX, Redux-Saga
We already know this, why you are here?
Front-end development is a boiling pot of emerging libraries and new ideas.
Navigating all this ** requires thinking in concepts and ideas instead of implementations!
Think in concepts and ideas
Build your own abstractions about libraries and framework modules!
- Sometimes one library may have more than one abstraction depending on how is used in the context of specific project!
- Continuously validate the quality of your abstractions while the project grows and adjust accordingly!
- Document and share your insights with the rest of the team!
- Accept criticism positively!
Why React?
- Fast - Virtual DOM
- Popular - CV Driven Development (CDD)
- Reusable/Composable Components
- Great Developer Tools
- create-react-app
-
Clean abstraction!
import React from "react";
import { render } from "react-dom";
function ShoppingListItem({ name }) {
return <li>{name}</li>;
}
function ShoppingList({ name, items }) {
return (
<div className="shopping-list">
<h1>Shopping List for {name}</h1>
<ul>{items.map(item => <ShoppingListItem {...item} />)}</ul>
</div>
);
}
const App = () => (
<div className="app">
<ShoppingList
name={"Peter"}
items={[{ key: 1, name: "apple" }, { key: 2, name: "pear" }]}
/>
</div>
);
render(<App />, document.getElementById("root"));
React is a declarative way to define one-way transformation from our data to browser DOM.
React handles efficiently DOM updates after our data changes. No need to define or even think about DOM mutation.
Stay Pure! Pure is good!
Why Redux?
- Predictable state container
- Testable
- Great Developer Tools
- Time traveling debugger
-
Clean abstraction!
Stay Pure! Pure is good!
Redux is the single source of truth about our app state.
Every state transition is triggered by dispatching simple actions and performed by pure reducer function.
State is read-only.
Redux State
connect
connect
connect
React Transformation
Browser DOM
What about Side effects?
Our choice is Redux Saga
- Saga-s are the only place where all our business logic lives.
- Saga-s are easily and synchronously tested. No need for any Mocking at all. We improved Redux Saga Test Engine project.
- Redux Saga is the only middleware that we ever need.
Redux Saga turns Redux to Application Message Bus that trigger our Business logic Coroutines.
Redux State
connect
connect
connect
React Transformation
Browser DOM
Redux Saga
Enter React Router
Declarative routing for React
<Provider store={store}>
<Router history={browserHistory()}>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<hr />
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
</Router>
</Provider>;
Redux State
connect
connect
connect
React Transformation
Browser DOM
React Router
Redux Saga
Let The URL Do The Talking, Part 1: The Pain Of React Router In Redux
by TYLER THOMPSON
To detect route change now we must either listen the `history` object or wire to React lifecycle methods like `componentDidMount()` and initiate redux state update.
As explained by 3 part series
What about `react-router-redux` or `redux-router`?Check Part 2!
Redux-First Router — A Step Beyond Redux-Little-Router
It’s no surprise every few months/years/days we find ourselves confronted by the fact that there’s a better way to do what we’re doing.
"Saga First" Router for React/Redux/Saga Projects
by ChaosGroup
- Full featured in 137 lines (113 sloc).
- Powered by Saga-s.
- Route navigation is triggered only by Redux action.
- Route navigation can start Saga and automatically cancel the last one.
- View layer agnostic.
Redux State
connect
connect
connect
React Transformation
Browser DOM
Redux Saga First Router
Redux Saga
import createHistory from 'history/createBrowserHistory';
import { reducer as routerReducer, saga as routerSaga, buildRoutesMap, route } from 'redux-saga-first-router';
// matched from top to bottom
// less specific routes should appear later
// provided sagas are activated/deactivated on matched route
const routesMap = buildRoutesMap(
route('PROJECT', '/portal/projects/:projectName', projectNavigate),
route('PROJECTS', '/portal/projects', projectsNavigate),
route('DOWNLOAD', '/portal/download'),
// ...
);
const history = createHistory();
const reducer = combineReducers({
// ... other reducers
routing: routerReducer
});
// ... store and saga middleware setup
// other sagas ...
// routerSaga is registered last
sagaMiddleware.run(routerSaga, routesMap, history);
import { navigate } from 'redux-saga-first-router';
const mapDispatchToProps = dispatch => {
return {
// ...
onSelectProject(projectName) {
dispatch(navigate('PROJECT', { projectName }));
},
onProjectDeleted() {
dispatch(navigate('PROJECTS', {}, { replace: true }));
},
// ...
}
}
// -------------------
const projectNavigateAction = {
type: 'router/NAVIGATE',
id: 'PROJECT',
params: {
projectName: 'Project 123'
}
}
All navigation in our application is now controlled by just dispatching redux actions, browser URL and history manipulation are handled automatically and only by the router!
If you want to react on navigation event, use registered route saga!
import React from 'react';
import { connect } from 'react-redux';
import ProjectView from './screens/project-view';
import ProjectList from './screens/project-list';
import NotFound from './screens/not-found';
const Screen = ({ id, params }) =>
(({
PROJECT: () => <ProjectView projectName={params.projectName} />,
PROJECTS: () => <ProjectList />,
}[id] || (() => <NotFound />))());
export default connect(state => state.routing)(Screen);
export function* projectNavigate({ projectName }) {
// sub-saga active only for current route
yield fork(watchProjectRename);
// prepare initial state
yield put(clearStore());
try {
// poll for changes every 3 seconds
while (true) {
// load current project
yield put(getProject(projectName));
yield call(delay, 3000);
}
} finally {
if (yield cancelled()) {
// cleanup on navigating away
yield put(clearStore());
}
}
}
Thank you!
Questions?
Story from the trenches: React
By Martin Dimitrov
Story from the trenches: React
Story from the trenches: React, how to objectively reason about your front-end stack and why we ended up with implementing our own router (and published it as open-source)
- 627