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

by James Gillmore

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