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
Text
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 handles efficiently DOM updates after our data changes. No need to define or even think about DOM mutation.
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
Redux State
connect
connect
connect
React Transformation
Browser DOM
Redux Saga
<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
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!
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.
by ChaosGroup
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'
}
}
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());
}
}
}