npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

redux-taxi

v1.2.0

Published

Inform server-side rendering from the component level when dealing with asynchronous actions.

Downloads

41

Readme

redux-taxi :taxi:

ReduxTaxi allows for component-driven asynchronous server-side rendering in isomorphic/universal React+Redux apps.

Use case

When you have asynchronous actions that you want to be processed on the server (at instantiation time, e.g. in the constructor) before responding to the client, you'll need to explicitly register them to avoid an Error being thrown. The motivation behind this throw is explained later in this document.

Installation

npm install --save redux-taxi

Usage

Use the decorator to signal at the component level when the component relies on an asynchronous action server-side.

Decorator
/* SomePage.jsx */
import SomePageActions from 'action/SomePageActions';
import {SOME_ASYNC_ACTION} from 'action/types';
import {registerAsyncActions} from 'redux-taxi';

// explicitly register the async action
@registerAsyncActions(SOME_ASYNC_ACTION)
// usual redux store connection decorator
@connect(state => state.somePageState, SomePageActions)
export default class SomePage extends Component {
    constructor(props, context) {
        super(props, context);
        // Dispatch async action
        this.props.someAsyncAction(this.props.data);
    }
    // ... render() and other methods
}

If you're not using ES7 style decorators, you could also call it and export like so:

class SomePage extends Component {
    // ...
}

export default registerAsyncActions(SOME_ASYNC_ACTION)(connect(
    state => state.somePageState,
    somePageActions
)(SomePage))

But this is obviously awkward, and ES7 decorators are recommended :)

Asynchronous Action Format

Currently, ReduxTaxi expects asynchronous actions to have a promise property attached to them. The PromiseMiddleware is provided for convenience, but you're free to use your own. ReduxTaxi detects promises by looking for action.promise.

TODO: In the future, we may be able to make this configurable by adding a 'promise' check function that can be passed in to do the sniffing/collecting of promises.

/* SomePageActions.js */

import {SOME_ASYNC_ACTION} from 'action/types';
import api from 'api';

export function someAsyncAction(data) {
    return {
        type: SOME_ASYNC_ACTION,
        promise: api.someAsyncRequest(data) // Promise collected by ReduxTaxi
    };
}

Server Setup

Since ReduxTaxi is not opinionated about how your server rendering works, it's left up to you to wire in the functionality.

TODO: This may change as this project grows and if common patterns start to emerge it may be provided for you, but for now it's up to you. Below is an example of how your server-rending might make use of ReduxTaxi:

/* server.js */
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';

// react-router imports
import {match, RouterContext} from 'react-router';

// redux imports
import {Provider} from 'react-redux';
import {ReduxTaxi, ReduxTaxiProvider} from 'redux-taxi';

// app imports
import configureStore from './store/configureStore'; // Your store configurator

const server = express();
server.use((req, res) => {
    match({routes, location}, (error, redirectLocation, renderProps) => {
        // ... some error handling ...

        if (renderProps) {
            const reduxTaxi = ReduxTaxi();

            // Your configureStore signature may be different, but here we are passing reduxTaxi as part of an object with a property name of `reduxTaxi`
            // to signal to configureStore to include it when configuring the store for the server (as opposed to the client where reduxTaxi is not needed,
            // and something else may be provided, e.g. browserHistory). This allows us to write configureStore in an "isomorphic" fashion, without having
            // to explicitly signal that its running in server or client contexts.
            const store = configureStore(initialState, {reduxTaxi});

            const initialComponent = (
                <Provider store={store}>
                    <ReduxTaxiProvider reduxTaxi={reduxTaxi}>
                        <RouterContext {...renderProps} />
                    </ReduxTaxiProvider>
                </Provider>
            );

            // Render once to instantiate all components (at the given route)
            // and collect any promises that may be registered.
            let content = ReactDOMServer.renderToString(initialComponent);

            const allPromises = reduxTaxi.getAllPromises();
            if (allPromises.length) {
                // If we have some promises, we need to delay server rendering
                Promise
                    .all(allPromises)
                    .then(() => {
                        content = ReactDOMServer.renderToString(initialComponent);
                        res.end(content)
                    })
                    .catch(() => {
                        // some error happened, respond with error page
                    })
            } else {
                // otherwise, we can just respond with our rendered app
                res.end(content);
            }
        }
    });
});

configureStore

Your configure store just needs to apply the ReduxTaxiMiddleware with the ReduxTaxi instance passed in from server.js. Here's a real-world example:

import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import {syncHistory} from 'react-router-redux';
import {ReduxTaxiMiddleware, PromiseMiddleware} from 'redux-taxi';
import rootReducer from './reducers'; // Your app reducers

export default function configureStore(initialState, instance) {
    const middleware = applyMiddleware(
        // In a server context, reduxTaxi will be provided,
        // in a client context, history will be provided.
        // This keeps configureStore "isomorphic" in the sense that it's basically unaware of what context it's being rendered in.
        instance.reduxTaxi ? ReduxTaxiMiddleware(instance.reduxTaxi) : syncHistory(instance.history),

        // You do not have to use `ReduxTaxi`'s PromiseMiddleware, but it's provided for convenience
        PromiseMiddleware,

        // Your other middleware...
        thunk
    );

    return createStore(rootReducer, initialState, middleware);
}

Motivation

Because ReactDOMServer.renderToString() is synchronous, we need a way to signal to the server that it should wait for any asynchronous actions to complete before responding to the client. Without this, asynchronous actions will be fired and forgotten on the server, and repeated on the client.

But why the @registerAsyncActions() decorator?

We wanted the decision to slow the server response down to be a very deliberate decision. To achieve this, we opted for a simple decorator where you can explicitly register which asynchronous actions you want to allow to slow down server rendering. Without explicitly registering your asynchronous actions, a fatal Error will be thrown.

Ok, but what if I don't want to block the server rendering, but fire the action as soon as the page loads?

Simply put your action dispatching in componentDidMount() lifecycle method, since this method only executes in a client context. This actually follows Facebook's recommended pattern.

/* SomePage.jsx
   (Modified to only dispatch async action on client)
*/

// (Notice no need to import action type and redux-taxi)

@connect( // ... usual store connection decorator
    state => state.somePageState,
    SomePageActions
)
export default class SomePage extends Component {

    // This will only execute in a client context
    componentDidMount() {
        // Dispatch async action
        this.props.someAsyncAction(this.props.data);
    }

    // ... render() and other methods
}

How does it work?

In a nutshell, there's special middleware (ReduxTaxiMiddleware.js along with PromiseMiddleware.js) that intercepts all actions that have a promise defined and checks to see if they're registered with ReduxTaxi.js (which is provided at the request-level via ReduxTaxiProvider.js). Then, when rendering on the server, any promises that have been collected will be .all().then()'d and then the server re-renders the markup and finally responds to the client.

NOTE: This re-rendering (rendering twice) is necessary because React does not currently provide a way to instantiate React Components without actually rendering them to something (e.g. the constructor does not get called). To mitigate any (albeit marginal) time cost to this, the re-rendering should only be done when there are actually asynchronous actions that fired. A completely synchronous page should render immediately.

Alternative Projects

  • async-props - Data-fetching knowledge lives at the Routing level.
  • redial - Data-fetching knowledge lives at the Routing level.
  • ground-control - Data-fetching knowledge lives at the Routing level.
  • react-transmit - Provides a Relay-inspired API, but with Promises instead of GraphQL queries.
  • react-resolver - Similar philosophy to redux-taxi (knowledge is confined at the component level), but is mostly usable outside of a Redux architecture.