React Resource Router (RRR)

repository·master·Indexed 42 days ago

https://github.com/atlassian-labs/react-resource-router

A configuration-driven routing solution for React version 0.30.0 that manages SPA route matching, data fetching, and progressive rendering. It optimizes performance by requesting route-specific data early, ensuring compatibility with React's Concurrent Mode and providing server/client parity.

Tokens
28.7K
Snippets
90
Records
112
Agent score
52%

What's inside React Resource Router

  1. Use resourceContext to provide environment data

    master

    The resourceContext prop allows you to inject data into the lifecycle of your resources. This data is automatically made available to the getKey and getData functions of every resource associated with your routes. This is useful for passing API base URLs, environment flags, or authentication tokens that resources need to construct keys or fetch data.

    import { environment } from './constants';
    
    export const App = () => (
      <Router history={history} routes={routes} resourceContext={{ environment }}>
        <RouteComponent />
      </Router>
    );
  2. How react-resource-router works

    master

    React Resource Router (RRR) is a configuration-driven routing solution for React. It manages single-page application (SPA) route matching, data fetching, and progressive rendering.

    Key architectural concepts:

    • Static Configuration: Routing is driven by a central array of route objects.
    • Asynchronous Data Fetching: Data for a route is requested as early as possible (before the component renders) to improve performance and prepare for React Concurrent Mode.
    • Progressive Rendering: The page renders progressively as asynchronous requests resolve.
    • Server/Client Parity: It works on both client and server without requiring a full traversal of the React tree.
  3. Define dependent and dependency resources

    master

    In react-resource-router, you can establish relationships between resources using the depends field. This allows for orchestrated data fetching and state management.

    • Dependent Resource: A resource that includes a depends field containing a list of resource types. It cannot execute its getData() function until the listed dependency resources are present. Its getData() method receives a dependencies hash containing the current data for those resources.
    • Dependency Resource: A resource listed in another resource's depends field. It behaves like a normal resource but is guaranteed to execute its getData() function before any resources that depend on it.
    • Interrelated Resources: A group consisting of both dependent and dependency resources.
    • Independent Resource: A resource that does not appear in any other resource's depends list.
    • Unlisted Resource: A resource that does not appear in the primary resource list (e.g., the route resource list).
  4. What are Router Resources and how do they work?

    master

    Router Resources are objects used by the router to fetch, cache, and provide data to route components.

    When you include resources in the resources array of a route configuration object, the router manages their lifecycle:

    1. Initial Load: Data is fetched as soon as the Router is mounted.
    2. Route Transitions: Data is re-fetched if the existing resource data has expired.

    By placing the Router high up in your React component tree, you trigger these asynchronous data requests as early as possible, which helps achieve faster meaningful render times.

  5. Rules for configuring resource dependencies

    master

    To prevent circular dependencies and ensure predictable execution, you must follow these rules when defining resources:

    1. Resource List: There must be a well-defined list of resources (typically the route resource list).
    2. Mandatory Listing: Any resource that uses depends must be present in the resource list.
    3. Precedence: A resource may only depend on resources that precede it in the resource list.
    4. Self-Dependency: A resource may depend on itself; this allows its getData() function to access its own previous value.
    5. Unlisted Constraints: Unlisted resources are not allowed to contain dependencies.

    Error Handling: If a resource violates these rules, its getData() function will not be called, and the system will throw a error: ResourceDependencyError.

  6. Access resource state with ResourceSubscriber

    master

    The ResourceSubscriber component allows you to subscribe to the state of a specific resource using a render prop. This is recommended only if you cannot use the useResource hook.

    import { ResourceSubscriber } from 'react-resource-router';
    import { avatarResource } from '../routing/resources';
    
    export const Avatar = () => (
      <ResourceSubscriber resource={avatarResource}>
        {({ data, loading }) => {
          const image = loading ? '' : data;
          return <Circle image={image} />;
        }}
      </ResourceSubscriber>
    );
  7. Configure the Router component

    master

    The Router component is the central entry point for the library and is configured using four primary props:

    1. routes: An array of route configuration objects defining your application's paths and components.
    2. history: A history instance (e.g., created via createBrowserHistory) to manage navigation.
    3. resourceContext (optional): An object containing environment-specific or router-agnostic data passed to all resource getKey and getData functions.
    4. resourceData (optional): An object containing pre-fetched data (e.g., from SSR) to prevent the client from re-fetching resources on mount.
    import {
      Router,
      RouteComponent,
      createBrowserHistory,
    } from 'react-resource-router';
    import { routes } from './routing';
    
    const history = createBrowserHistory();
    
    export const App = () => (
      <Router history={history} routes={routes}>
        <RouteComponent />
      </Router>
    );
  8. Mount the Router in your application

    master

    To enable routing, mount the Router component at the top level of your React tree. The Router requires a RouteComponent as a child.

    Required props for Router:

    • routes: Your array of route configuration objects.
    • history: A history object (e.g., created via createBrowserHistory).
    • plugins: An array of plugins (e.g., createResourcesPlugin).
    import {
      Router,
      RouteComponent,
      createBrowserHistory,
    } from 'react-resource-router';
    import { createResourcesPlugin } from 'react-resource-router/resources';
    import { appRoutes } from './routing/routes';
    
    const history = createBrowserHistory();
    const resourcesPlugin = createResourcesPlugin({});
    
    const App = () => (
      <Router routes={appRoutes} history={history} plugins={[resourcesPlugin]}>
        <RouteComponent />
      </Router>
    );
  9. Implement dependency-aware actions

    master

    When creating custom actions that interact with the Resource Store, you must ensure they correctly handle dependency side-effects by wrapping them appropriately.

    1. Wrap Public Actions: Actions that may produce side-effects must be wrapped using actionWithDependencies() or mapActionWithDependencies(). This allows the system to generate the necessary execution tuples.
    2. Implement Private Actions: The actual logic of the action should be implemented as a private action that is not wrapped. This private action is what gets dispatched during the executing state iteration.
    3. Accessing Dependencies: Inside your private action, you can use:
      • executeForDependents(resource, action): To manually trigger specific actions on dependent resources.
      • getDependencies(resource): To retrieve the current data for the resource's dependencies.
  10. Access the current Router state

    master

    You can access the current state of the router using one of three methods. It is recommended to use the useRouter hook or the RouterSubscriber component for modern implementations. The withRouter higher-order component (HoC) is available primarily to assist with migrations from react-router.

    • Hook: useRouter (Recommended)
    • Component: RouterSubscriber (Recommended)
    • Higher-Order Component: withRouter (Legacy/Migration support)
  11. Handle errors in `getData` functions

    master

    When defining a getData function for a resource, how you return or throw errors determines the shape of the resulting resource state ({ data, error, loading }). There are three primary patterns for handling failures:

    1. Throwing an Error (Recommended): Throwing an error inside getData is the most robust way to signal a failure. This sets data to null and populates the error field with the thrown error object. This is the preferred method for most error scenarios.
    2. Returning an Error Object in Data (Conditional): You can return an object containing an error code (e.g., { errorCode: 'some error' }). This keeps data populated but sets error to null. Use this only if your UI components are specifically designed to inspect the data object for error properties.
    3. Returning an Empty Object (Not Recommended): Returning a resolved empty object (e.g., Promise.resolve({})) results in a state where data is an empty object, error is null, and loading is false. This can lead to subtle bugs where the UI treats a failed request as a successful request with no data.
    import { MyCustomError } from '../common/errors';
    import { getAccountInfoData } from '../api';
    
    const getData = async (routerContext, resourceContext) => {
      const { query } = routerContext;
      const { isAdmin } = resourceContext;
    
      if (!isAdmin) {
        // NOT RECOMMENDED: results in {data: {}, error: null, loading: false}
        return Promise.resolve({});
      }
    
      const data = getAccountInfoData(isAdmin, query);
    
      if (!data) {
        // BETTER IMPLEMENTATION (if component checks data for errorCode): 
        // results in {data: {errorCode: 'some error'}, error: null, loading: false}
        return { errorCode: 'some error' };
      }
    
      if (!data) {
        // RECOMMENDED IMPLEMENTATION: 
        // results in {data: null, error: MyCustomError, loading: false}
        throw new MyCustomError('bad thing happen');
      }
    
      return data;
    };
  12. Implement Router for Server-Side Rendering (SSR)

    master

    When implementing SSR, you cannot use createBrowserHistory because it relies on browser APIs. Instead, use createMemoryHistory from the history package and pass the current request location as the initialEntries.

    On the client side, continue using createBrowserHistory to maintain standard browser navigation behavior.

    // server-app.js
    import { createMemoryHistory } from 'history';
    import { Router } from 'react-resource-router';
    import { App } from '../components';
    import { routes } from '../routing/routes';
    
    export const ServerApp = ({ location }) => (
      <Router
        history={createMemoryHistory({ initialEntries: [location] })}
        routes={routes}>
        <App />
      </Router>
    );
    // client-app.js
    import { Router, createBrowserHistory } from 'react-resource-router';
    import { App } from '../components';
    import { routes } from '../routing/routes';
    
    const history = createBrowserHistory();
    
    export const ClientApp = () => (
      <Router history={history} routes={routes}>
        <App />
      </Router>
    );