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

@r/platform

v0.17.1

Published

A set of tools to enable easy universal rendering and page navigation on a React + Redux stack

Downloads

498

Readme

r/platform

A set of tools to enable easy universal rendering and page navigation on a React + Redux stack.

Change Log

v0.14.0

Removed the postRouteServerMiddleware configuration option. Middleware will now end with the route handler being fired.

Installation

Currently, just use NPM.

npm install -S @r/platform

You also need to install its peer dependencies. For example:

npm install [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] @r/[email protected]

Usage

Server

// Server.es6.js
import Server from '@r/platform/Server';

const server = Server({
  reducers: {},                      // Reducers for the Redux store.

  routes: [],                        // A list of lists that maps
                                     // routes to handlers. For example:
                                     //
                                     // [
                                     //   ['/', Frontpage],
                                     //   ['/r/:subredditName', Subreddit],
                                     // ]

  template: (data) => { /* ... */ }, // a template function that returns a
                                     // string (likely an HTML string).

  port: 8888,                        // OPTIONAL. port for your server.

  preRouteServerMiddleware: [],      // OPTIONAL. Koa middleware to run
                                     // before a route is handled

  reduxMiddleware: [],               // OPTIONAL. Additional Redux
                                     // middleware. Middleware defined here
                                     // will run before r/platform's
                                     // middleware runs.

  dispatchBeforeNavigation: async (koaCtx, dispatch, getState, utils) => {},
                                     // OPTIONAL. Dispatch additional
                                     // actions before the navigation
                                     // fires.

  getServerRouter: (router) => {}    // OPTIONAL. Return the Koa router if
                                     // needed.
});

// start the server
server();

Client

// Client.es6.js
import Client from '@r/platform/Client';

const client = Client({
  reducers: {},                        // Reducers for the Redux store.

  routes: [],                          // A list of lists that maps
                                       // routes to handlers. For example:
                                       //
                                       // [
                                       //   ['/', Frontpage],
                                       //   ['/r/:subredditName', Subreddit],
                                       // ]

  appComponent: <div/>,                // The React component that
                                       // represents the app.

  container: 'container',              // OPTIONAL. Id of the DOM element
                                       // the Client App will be rendered into.

  dataVar: '___r',                     // OPTIONAL. A key on the 'window' object
                                       // where the data will be written into.

  modifyData: (data) => { /* ... */ }, // OPTIONAL. A function that mutates the
                                       // data object before it is loaded
                                       // into the client side store.

  reduxMiddleware: [],                 // OPTIONAL. Additional Redux middleware.
                                       // Middleware defined here will run
                                       // before r/platform's middleware runs.

  debug: false                         // OPTIONAL. Setting debug to true will
                                       // cause redux actions to be logged
                                       // in the console.
});

// run the client
client();

Creating Routes

r/platform's router differs from most traditional routers. Instead of handlers returning html, they use Redux's dispatch calls to help define a state blob. Methods on the handler are HTTP verbs. Specifically, they are one of get, post, put, patch, and delete. These methods MUST return promises. The easiest way to enforce this is to declare the methods as es7 async functions.

All methods have access to the following properties:

  1. this.originalUrl: the url that spawned this handler
  2. this.urlParams: a dictionary of route defined params. e.g. if '/bar' matches '/:foo', urlParams would look like { foo: 'bar' }.
  3. this.queryParams: a dictionary of query params
  4. this.hashParams: a dictionary of hash params
  5. this.bodyParams: a dictionary of data that would appear in the request body

Each method is also called with the following arguments:

  1. dispatch: a function used to dispatch Redux actions
  2. getState: a function that (when called) returns a snapshot of state in the Redux store
  3. utils: a dictionary of helper methods. Currently contains two methods, waitForState and waitForAction. Visit r/middleware for more details on how these operate.

Example

// routes.es6.js
import { BaseHandler, METHODS } from '@r/platform/router';
import * as actions from '@r/platform/actions';
import * as otherActions from './otherActions';

// Create a handler
class Frontpage extends BaseHandler {  
  async [METHODS.GET](dispatch, getState, { waitForState, waitForAction }) {
    // pull out params if necessary
    const { foo } = this.queryParams;

    // dispatch certain actions synchronously
    dispatch(otherActions.doSomething());

    // if needed, wait on certain tasks to complete before dispatching further.
    // on the Server side, the Server will wait for the entire function to
    // complete before responding to the request with html.
    const importantThing = await importantAsyncFunction();

    // use the utility methods to wait on something in state
    await waitForState(
      state => state.foo === 'foo', // the condition
      state => dispatch(/* something */) // the callback if condition is met
    );

    // further synchronous dispatches are possible. Thanks to es6/7, these won't
    // fire until the previous asynchronous action has completed.
    dispatch(/* something else */);
  }
}

// Export the routes
export default [
  ['/', Frontpage],
];

Keeping the Url in Sync

In addition to routing, it is important that the url is kept in sync with the store state. It is also important that when a popstate event is fired, the state updates to reflect. To that effect, r/platform exports a React component that manages the url. To use it, just drop the component into your app anywhere it won't get unmounted.

import React from 'react';
import { UrlSync } from '@r/platform/components';

export default class App extends React.Component {
  render() {
    return (
      <div>
        {/* many components */}
        <UrlSync/>
      </div>
    )
  }
}

Rendering pages

Often, you would like to render certain components based on the url state. To do so, you can use the <UrlSwitch> component:

import React from 'react';
import { UrlSwitch, Case, Page } from '@r/platform/url';

export default class Foo extends React.Component {
  render() {
    return (
      <div>
        <UrlSwitch>
          <Case
            // do something based on a url. this is the most generic way to use
            // urlSwitch
            url='/'
            exec={ pageData => <div/> }
          />
          <Page
            // as a convenience, if a specific component needs to be rendered,
            // use the <Page/> component instead. this takes a 'component'
            // instead of a function. the props of the component are pageData
            url='/r/:subredditName'
            component={ FooComponent }
          />
          <Case
            url='*' // catch all
            exec={ pageData => <div/> }
          />
        </UrlSwitch>
      </div>
    );
  }
}

Easy routing

Sometimes, routing to a page might happen by clicking an anchor tag. Instead of manually connecting the anchor tag to a dispatch action, @r/platform exports a pre-connected anchor tag component:

import React from 'react';
import { Anchor } from '@r/platform/components';

export default class Foo extends React.Component {
  render() {
    return (
      <div className='Foo'>
        <Anchor
          href='/foo?stuff=yeah'
          className='Foo__anchor'
        >
          Click me!
        </Anchor>
      </div>
    );
  }
}

@r/platform also includes a <BackAnchor/> component. The <BackAnchor/> checks to see if the linked url is the previous url in history. If it is, it calls history.back() (if the history API exists) instead of adding the destination to the browser's history. This makes links that say 'back' actually go back.

If those don't suite your needs, @r/platform also provides <LinkHijacker />. Helpful for when you need to use dangerouslySetInnerHTML, this component will ensure clicking on links will navigate without a new page load. It follows relative links by default, and can be customized via a RegExp api to extract paths from arbitrary urls.

import React from 'react';
import { LinkHijacker } from '@r/platform/components';

export default class Foo extends React.Component {
  render() {
    return (
      <div className='Foo'>
        <LinkHijacker>
          <div
            className='Foo__content'
            dangerouslySetInnerHTML={ { __html: this.props.htmlContent } }
          />
        </LinkHijacker>
      </div>
    );
  }
}

@r/platform exports a pre-connected form as well:

import React from 'react';
import { Form } from '@r/platform/components';

export default class Foo extends React.Component {
  render() {
    return (
      <div className='Foo'>
        <Form
          action='/login'
          className='Foo__form'
        >
          <input name='username'/>
          <input name='password' type='password'/>
        </Form>
      </div>
    );
  }
}

Additional Tools

There are a few additional goodies in r/platform

Reducer

r/platform exports a Redux reducer (@r/platform/reducer). This reducer gets auto added when using the Client and Server functions, so you should never need to import this directly.

Actions

r/platform exposes a few Redux actions you can use to navigate through the app. They are:

  1. setPage(pageType, url, { urlParams, queryParams, hashParams }): pushes a new page onto the navigation stack. Note: there are no bodyParams represented here, as routes that contain a body should not update the url.
  2. gotoPageIndex(pageIndex): navigates to a particular page on the navigation stack.
  3. navigateToUrl(method, pathName, { queryParams, hashParams, bodyParams }): navigate to a url. Note: there is no need to independently include the urlParams here. Simply pass along the url.

Router

r/platform doesn't use a traditional router. So instead, the router exports a Handler and some http verbs.

import { BaseHandler, METHODS } from '@r/platform/router';

console.log(METHODS); // {
                      //   GET: 'get',
                      //   POST: 'post',
                      //   PUT: 'put',
                      //   PATCH: 'patch',
                      //   DELETE: 'delete',
                      // }

console.log(BaseHandler); // Described in the previous section on creating routes.

merge

r/platform includes a helpful utility method for "modifying" state while maintaining the immutability that Redux expects.

import merge from '@r/platform/merge';
import * as actions from '@r/platform/actions';

// reducer
export default function(state={}, action={}) {
  switch(action.type) {
    case actions.GOTO_PAGE_INDEX: {
      const { pageIndex } = action.payload;

      // `merge` lets you just deal with state diffs. just merge your
      // diff with state and `merge` will preserve immutability.
      return merge(state, {
        currentPageIndex: pageIndex,
        currentPage: state.history[pageIndex],
      });
    }
    default: return state;
  }
}

merge can also take options which let the method know how to deal with arrays and empty dictionaries.

merge(state, diff, options={ emptyDict, array })

  1. emptyDict: One of strict, skip, or replace. Defaults to strict. strict will merge in the new dictionary, which will cause the object reference to change. skip will ignore empty dictionaries (thus not changing the object reference in the original). replace will swap out the old dictionary with the empty one.

  2. array: One of replace or concat. Defaults to replace. replace will swap out the old array with the new one. concat will produce a new array with values from both arrays, with values from the original taking precedence.

plugins

You may wish to quickly render a shell of the page- such as a loading screen- and make API requests on the client, rather than the server.

import * as plugins from '@r/platform/plugins';
import Server from '@r/platform/Server';

const server = Server({
  //...
  dispatchBeforeNavigation: async (ctx, dispatch, getState, utils) => {
    //...
    plugins.dispatchInitialShell(ctx, dispatch);
  }
});

This will set state.shell to true or false. If you have a nojs cookie, a nojs querystring, or your user-agent contains the word bot, state.shell will be false during server request handling. Otherwise, it will be true. You can then check state.shell in your handlers to determine whether or not to make API requests.

You will also likely want to run actions.activateClient on the client side to ensure the navigation actions are re-fired client side, with shell set to false. (Otherwise, activateClient is unnecessary unless you need to re-run navigation handlers for some reason.)

import * as actions from '@r/platform/actions';
import Client from '@r/platform/Client';

const client = Client({ /* ... */ });
client.dispatch(actions.activateClient);

Testing

r/platform provides some hooks to make it easier to create tests. Primarily, it exports a test creator that lets you easily set up a test for a component:

createTest([storeOptions,] testFn)

storeOptions are optional and are used to make the store more representative of the actual store the component is wrapped with. It has three optional keys on it:

  1. reducers: object: A dictionary of any reducers the store should contain
  2. middleware: array: A list of middleware to be added to the store
  3. routes: array: A routes list

The testFn is called with a dictionary of helpers: { shallow, mount, render, expect, getStore, sinon }.

  1. shallow: function: Shallow renders your React components. Good for testing the rendering of the component and checking that certain elements exist within in. more info
  2. mount: function: Mounts the component on a jsdom document. Use this to test interactions like clicking, hover, etc. more info
  3. render: function: Renders to static html. more info
  4. expect: function: Assertion function.
  5. getStore: function: Returns a store and a wrapper. Useful to testing components that depend on redux.
  6. sinon: object: The entirety of sinon to help generate spies, stubs, and mocks. more info

Using createTest

import createTest from '@r/platform/createTest';
import Foo from './Foo';

// testing with a connected component
createTest(({ mount, getStore, expect }) => {
  describe('<Foo/>', () => {
    it('should change state when clicked', () => {
      const { store, StoreWrapper } = getStore();
      const container = mount(
        <StoreWrapper>
          <Foo/>
        </StoreWrapper>
      );

      container.find(Foo).simulate('click');
      expect(store.getState().fooValue).to.equal('foo');
    });
  });
});