react-redux-resolved-route - a tiny Route extension for data loading management


(Liam Morley) #1

This is largely a request for feedback on react-redux-resolved-route, a library I built to make sure that data is in the Redux store before components are rendered.

Problem

Let’s say we have a client-side rendered Redux app using react-router-redux. In order to make sure components aren’t rendered unless they have what they need from the store, we (1) pass both the data from the store as well as an action creator to fetch that data from the container to the component, and (2) check within the component if the data is present, and call the action creator if it’s not.

Stuff.js

import React from 'react';
import { connect } from 'react-redux';
import { getStuff } from './actions';

export class Stuff extends React.Component {
  componentDidMount() {
    const { stuff, getStuff } = this.props;
    if (!stuff) getStuff(); // call getStuff() action creator if data isn't present
  }
  render() {
    const { stuff } = this.props;
    return (stuff) ? <div>{stuff}</div> : null; // conditionally render if we have what we need
  }
}

// optimistically pass data down, should it exist
const mapStateToProps = ({ stuff }) => ({ stuff });
// defensively pass action creator for fetching data
const mapDispatchToProps = dispatch => ({ getStuff() { dispatch(getStuff()); } });

export default connect(mapStateToProps, mapDispatchToProps)(Stuff);

index.js

// ...
import Stuff from './Stuff';
// ...

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Route path="/stuff" component={Stuff} />
      <Route path="/things/:id" component={Thing} />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root')
);

There are a few problems with this approach:

  1. The presentational component is responsible for triggering fetch actions, and must be defensive in the case that its data has not been fetched yet
  2. The container has to do double duty to supply the presentational component with everything it needs to both render data as well as fetch data

Some other routers (I’m thinking of Angular’s UI Router) make data loading a router concern. Not finding any graceful way to do this via react-router but not wanting to abandon the community favorite, I decided to bring this functionality to the router via a customized route component.

Solution

Introducing react-redux-resolved-route, a tiny library which wraps a Route with a special render function that only renders the given component if it has what it needs. It takes a resolve function which will be called once with store.dispatch, the current state from store.getState(), and any URL parameters from the route. If resolve returns a function, the route will listen to Redux state changes and will only render the component when that function evaluates to something truthy, given the state.

// ...
import ResolvedRoute from 'react-redux-resolved-route'; // new Route component
// ...

// strictly presentational leaf component
function Stuff({ stuff }) {
  return <div>{stuff}</div>;
}

function resolveStuff(dispatch, state, params) {
  const stuffExists = ({ stuff }) => !!stuff;
  if (!stuffExists(state)) {
    dispatch(getStuff());
    // Return a function that receives the state and returns true if the component can be rendered
    return stuffExists;
  }
}

function resolveThing(dispatch, state, params) {
  const id = parseInt(params.id, 10);
  const thingExists = ({ things }) => things.find(t => t.id === id);
  if (!thingExists(state)) {
    dispatch(getThing(id));
    return thingExists;
  }
}

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <ResolvedRoute path="/stuff" component={Stuff} resolve={resolveStuff} />
      <ResolvedRoute path="/things/:id" component={Thing} resolve={resolveThing} />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root')
);

Next Steps

Before adding a ton of features, I’m looking for early feedback:

  1. At what layer do you believe is responsible for ensuring that data has been fetched and loaded into the store?
  2. Are there existing libraries that manage this well? If so, do you use a library to manage this, or do you wrap this concern with your own custom component in each application you develop?
  3. I was super lazy with the naming of this library - UI Router’s resolve refers to the resolution of a promise, and this has library has nothing to do with promises. (Instead, it relies on the reactive nature of react-redux.) What would you call this?
  4. Is this approach as simple as necessary? Could it be simpler?
  5. If you feel like looking at the implementation, I’m curious to know if this is the simplest way to implement this.

One feature I plan to add is the ability to specify an alternate component that will be rendered if the function returned from resolve returns false (i.e. the given component doesn’t have what it needs to be rendered), but I’m interested in getting feedback before proceeding.


(Troy Rhinehart) #2

I actually think your original example is pretty clear and understandable, I wouldn’t say the same about your proposed solution.

The presentational component is responsible for triggering fetch actions

Seems trivial of a problem, but if your goal is for your presentation component to only be passed data, and not worry about conditionally calling an action I would suggest hoisting your action call into your container or in the mergeProps callback of connect. You can also conditionally fetch the data in the action itself instead of in the container via redux-thunk which enables conditional action creators.

and must be defensive in the case that its data has not been fetched yet

Rendering a null UI while you fetch data IMO is not a good UX, you could be using that condition to render an optimistic UI to show the consumer their request is in flight.

The container has to do double duty to supply the presentational component with everything it needs to both render data as well as fetch data

Supplying data is always going to be the responsibility of the container, and something somewhere is going to need the action; all you’ve done is move the responsibility from one place to another.


(Nick Roberts) #3

First, I’m excited (genuinely) about this package. It seems likely it would be of great use to me.

To give my own answers:

  1. I think this is something that I would like to see organised ‘vertically’ rather than ‘horizontally’ (in layers). I’d like to be able to add a cluster of classes and components (maybe in its own subdirectory) that together provide all the UI and functionality for a separate feature, including the relevant data storage, fetching, and so on. To this end, I’d like to see a convention for features to hook themselves into the root routing (sorry), typically in App.js. The feature should be able to hook in both UI components and data actions, reducers, and accessors at the same time, and to then hook in the dependencies of pages/components on data.

  2. Not to my knowledge.

  3. I think the naming is fine.

  4. I think your approach seems practical. The fact that the ‘resolve’ function returns a (‘condition’) function allows the resolve function to do whatever it takes to set up the condition function, which might need to be efficient, as it might be called many times (returning falsy) before it returns truthy (and the route is accepted).


(Liam Morley) #4

The original example is of course a toy example that fits above the fold; in a production application, the presentational component that might need data might be several levels deep, or have its need scattered across multiple components rendered at the same level. I will agree that the proposed solution might not be clear and understandable – that’s why I’m looking for feedback. :wink:

The thought of a mapStateToProps or mergeProps having a side-effect scares me, which is why I’ve never given serious thought to that approach. Is this a pattern that people have seen a lot?

Agreed. As I mentioned, one feature I plan to add is the ability to specify an alternate component that will be rendered if data isn’t ready yet, but I’m interested in getting feedback before proceeding.

…yeah, that’s the idea. :wink: The question is whether the responsibility has been moved to the right place or the wrong place. I’m trying to put a stake in the ground to say that, in many cases, the job of making sure my app is hydrated with initial data from the server should be raised up as far as is reasonable, and so the router seems like the right place. Sounds like you’re saying that containers and presentational components are in fact better suited for this role. This doesn’t feel right to me, but I appreciate the feedback.