Giter Site home page Giter Site logo

atlassian-labs / react-resource-router Goto Github PK

View Code? Open in Web Editor NEW
195.0 15.0 23.0 2.92 MB

Configuration driven routing solution for React SPAs that manages route matching, data fetching and progressive rendering

Home Page: https://atlassian-labs.github.io/react-resource-router

License: Apache License 2.0

JavaScript 1.13% TypeScript 98.87%
router spa react

react-resource-router's Introduction

react-resource-router logo

react-resource-router

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

Why?

React Resource Router was developed by Atlassian for Jira primarily to improve performance and prepare for compatibility with React's forthcoming Concurrent Mode on both client and server. You can read more about its development and impact here.

Features

  • Fully driven by a static configuration of route objects
  • Each route object contains the following core properties
    • path - the path to match
    • component - the component to render
    • resources - an array of objects containing fetch functions that request the route component's data
  • Data for a route is requested asynchronously and as early as possible, with the page progressively rendering as the requests resolve. This results in quicker meaningful render times
  • Works on both client and server without having to traverse the React tree

Usage

Create your resources

Resources describe and provide the data required for your route. This data is safely stored and accessed via the useResource hook or ResourceSubscriber component.

import { createResource } from 'react-resource-router/resources';
import { fetch } from '../common/utils';

export const homeResource = createResource({
  type: 'HOME',
  getKey: () => 'home-resource-key',
  getData: () => fetch('https://my-api.com/home'),
});

export const aboutResource = createResource({
  type: 'ABOUT',
  getKey: () => 'about-resource-key',
  getData: () => fetch('https://my-api.com/about'),
});

Create your components

These are the React components that get rendered for your routes. As mentioned, they can be wired into the state of your resources via the useResource hook or ResourceSubscriber component.

import { useResource } from 'react-resource-router/resources';
import { aboutResource, homeResource } from '../routes/resources';
import { Loading, Error } from './common';

export const Home = () => {
  const { data, loading, error } = useResource(homeResource);

  if (error) {
    return <Error error={error} />;
  }

  if (loading) {
    return <Loading />;
  }

  return <div>{data.home.content}</div>;
};

export const About = () => {
  const { data, loading, error } = useResource(aboutResource);

  if (error) {
    return <Error error={error} />;
  }

  if (loading) {
    return <Loading />;
  }

  return <div>{data.about.content}</div>;
};

Create your routes

Your route configuration is the single source of truth for your application's routing concerns.

import { Home, About } from '../components';
import { homeResource, aboutResource } from './resources';

export const appRoutes = [
  {
    name: 'home',
    path: '/',
    exact: true,
    component: Home,
    resources: [homeResource],
  },
  {
    name: 'about',
    path: '/about',
    exact: true,
    component: About,
    resources: [aboutResource],
  },
];

Use the Router

Now that you've set up your resources, components and configuration correctly, all you need to do is mount the Router in your react tree with a RouteComponent as a child. It will do the rest!

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>
);

Installation

npm install react-resource-router

# or

yarn add react-resource-router

Documentation

Check the docs website or the docs folder.

Examples

You can checkout the repo and play around with the examples we have setup to demonstrate how the API can be used for various use cases.

  1. Clone the repo and install dependencies
  2. Run npm start
  3. Local dev site will launch with all the examples

Thanks

Big thanks to Thinkmill for their involvement in this project.

License

Copyright (c) 2020 Atlassian and others. Apache 2.0 licensed, see LICENSE file.


With ❤️ from Atlassian

react-resource-router's People

Contributors

albertogasparin avatar alexanderkaran avatar aroramayur avatar atlassianrubberduck avatar bholloway avatar biniek-io avatar bobrovnik avatar charlescarey avatar dalehurwitz avatar dependabot[bot] avatar flexdinesh avatar github-actions[bot] avatar haskellcamargo avatar jackbrown avatar jakelane avatar kiddkai avatar kvtruong avatar liamqma avatar mahmood-sajjadi avatar marionebl avatar monicaolejniczak avatar nickrobson avatar patrickshaw avatar prithveeshgoel avatar rohan-deshpande avatar sgolovenko avatar snyk-bot avatar thekashey avatar tpuric avatar yamadapc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-resource-router's Issues

push state

I've noticed that we can't pass state object when push().

const { push } = useRouterActions();
push(path, state); // state isn't supported

State doesn't get passed in as an argument. https://github.com/atlassian-labs/react-resource-router/blob/master/src/controllers/router-store/index.tsx#L239

That probably means types are incorrect.

I wonder if we should fix the types or support push(path, state). I guess there isn't need to pass state object since query params can be used. Unless, the state is a big nested object. 🙄

I am happy to submit a PR. It should be simple though. Just want to hear your thoughts on which direction we should go about.

Allow transition delay

RRR has a few goals to have a flat routing, the ability to preload code or data is one of them. But we cannot use this ability "properly" out of the box.

It might provide a better user experience(in terms of perceived performance and layout trashing) if RRR can delay the transition until:

  • code for the new location is loaded. Custom code is required to check/preload route target.
  • data for the new location loaded/present. Resource information might need to be extended to indicate the importance of it and describe should it be "transition blocking" or not.

Minimal implementation:

  • patch history to override .push
  • match new URL to the set of given
  • manually handle components, resources and any/or other fields
  • call original .push when required promises are resolved.

Ideally that history patch should be built-in functionality and resource management can be implemented only "inside" RRR.

Prefetching triggers refresh of expired resources

When using prefetch on links, if the resource is expired, it will trigger a refresh causing a re-render and possibly back to skeleton.
Instead, it should not override values in the resource cache yet while prefetching, but use a separate cache and then we should get the value from it on actual transition.

Did you ever try with redux ?

Hello,
I found your library trying to improve perf on react-router. So first nice move and good job 👍

Did you try your lib with redux & redux saga ?

I want to give a try
Thanks you

Ability to respond to query parameter changes with a resource update

As it is also implied by the documentation;

... Note: resources are only refreshed on route change. The router does not poll or update resources in the background. Navigation within the same route, e.g. query param change, will not trigger a refresh of resources.
The resource is not refreshed when the query is changed by;

  const [page = '1', setPage] = useQueryParam('page');
  // call setPage() somewhere

Only way to force a refetch seems to be via an additional;

const resource = useResource(routeResource);
// resource.refetch() forces a proper update

Then, in resource's getData function the query is not properly updated and only way to practically access it is via window.location, which is an implementation detail and should not be ideally used:

getData: (routerContext) => {
  const params = new URLSearchParams(window.location.search);
  const page = params.get('page') || '1';
  // want to use page here, routerContext.query is a stale value from the previous route change for some reason
}

Ideally I would expect it to get triggered for query param changes as well. As it has probably now became a public behaviour maybe we can do it behind a boolean option?

Even this is not implemented, routerContext should get properly updated before getData is called so that we have a workaround.

Retain state while refreshing

I’ve just started using this on a project and have a situation where I want to refresh the data after a change. At the moment it seems like there’s no easy way to differentiate these states:

  • Loading for the first time
  • Reloading after the initial fetch is successful

... and I wondered if there was scope to handle that in some way. Retaining the existing value of data would probably be enough I think (though I imagine that has flow-on effects that I am not seeing).

Add preload/prefetch API on Link hover

We should support resources to be fetched on hover. Its also needs to

  • handle maxAge properly (avoid double fetch)
  • provide an AbortController so requests can be dropped
  • expose API so consumers can to run custom code (to preload RLL bundles for instance)
  • this behaviour should be optional, with a customisable delay

Render as you Fetch?

Do these resources go in-flight when the route transitions, but parallel to fetching async code? Definitely keen to try this out!

react-resource-router does not render matched component on redirect or hash change with hashrouter

If you use createHashHistory with react-resource-router like so:

<Router routes={routes} history={createHashHistory()}>
        <RouteComponent />
</Router>

When you change the url hash through or manually it does not render anything (or the matching route component).

When reloading the page with the correct hash route it then matches and renders the correct component.

We've created a minimal reproducible example below:
https://codesandbox.io/s/react-resource-router-hash-router-bug-lmzfe

Wrap RouterContainer children in ResourceContainer, pass resourceData and resourceContext to it

Doing this will give us some level of separation of concerns between the two stores. Currently inside of bootstrapStore we imperatively get the resource store, but we would not need to do this if we did the following

      <RouterContainer
        routes={routes}
        history={history}
        isGlobal={isGlobal}
      >
        <ResourceContainer 
            isGlobal={isGlobal} 
            resourceContext={resourceContext} 
            resourceData={resourceData}
         >
          {children}
        </ResourceContainer>
      </RouterContainer>

Then we would need a bootstrapStore method inside the ResourceContainer to do this in the onInit

  actions.hydrate({ resourceContext, resourceData });

Make and expose only one router implementation

Current split between Router, MemoryRouter and UniversalRouter is causing issue are features are not supported across the board and we increase even more the chances of misalignment.

We should kill both UniversalRouter and MemoryRouter, exposing just one Router that simply accepts history and allows isGlobal to be set on both router and resources stores (fixing #76).

Support for fetch reference (e.g. for Relay)

Hi, I love the getData and useResource pattern, the DX would be really nice for straight fetching data.

Relay doesn't set up its API in a way that would work nicely with this approach. It has two main functions:

// In your routes
const queryRef = loadQuery(/* ... */);

// In your component
const { data } = usePreloadedQuery(queryRef, /*... */);

Instead of using a promise to figure out if the fetch is complete, it uses a reference that acts as a handle on the request. This would work okay (if a bit strange) with getData and useResource, I could just ignore the loading and error statuses. Except that the queryRef needs to be actively disposed when it is no longer displayed.

Potentially a solution would be to add a more customisable get and dispose combo of options to the router config.

update function should accept just new data

As per the documentation current update method accepts a callback function, we have some cases where we get the latest version of data entirely from the backend and we write logics like
promise(body).then(res => update(() => res))

Maybe changing update method can accept both of them?
update(currentData => ({ ...currentData, username: newUsername, })
update(latestData)

And usage would be more flexible.
promise(body).then(update)

Do not expose a data key until loading has finished and there is no error to make more restful.

The current implementation of RouteResourceResponseBase includes a data key which will always be null as defined in

I suggest we remove the key entirely and let it only be set by the response successfully returning data.

This will have the flow on effect of improving the typescript checking and will force developers to check loading and error before the data key will even appear.

Like this:

export const Home = () => {
  const homeResponse = useResource(homeResource);

  if (homeResponse.error) {
    return <Error error={home.error} />;
  }

  if (homeResponse.loading) {
    return <Loading />;
  }
 
  // before this there is no data key on homeResponse
  return <div>{homeResponse.data.home.content}</div>;
};

Update Packages

A lot of dependencies have had minor and major package changes. Update as many packages as possible without introducing breaking changes.

Not able to update resource context

As I understand resourceContext might be used to fed getKey/getData with extra props, not reflected in the query. However the updates to it are ignored, and context is never updated.

Ref: https://atlassian-labs.github.io/react-resource-router/#/router/configuration?id=resourcecontext

If context is not supposed to be used in that way - then what can I do to provide extra variables to my resources? For example language - it might be not a part of URL, but might the changed in runtime.

Why are resources required on the hook call and route definition?

Interesting library. I saw that the created resources are used twice, once when you define the routes and once when you useResource.

// routes.js
export const appRoutes = [
  {
    name: 'home',
    path: '/',
    exact: true,
    component: Home,
    resources: [homeResource],
  },
];

// components.jsx
import homeResource from '../resources';

const resource = useResource(homeResource)

Why is that?
Shouldn't I be able to do

// resources: [homeResource]
const homeResource = useResource()
// resources: [homeResource, aboutResource]
const [homeResource, aboutResource] = useResource()

TypeScript@3 peer dependency?

Receiving a peer dependency warning for react-resource-router

➤ YN0060: │ @atlassian/canvas@workspace:. provides typescript (p87734) with version 4.5.5, which doesn't satisfy what react-resource-router requests

I don't see why it would depend on an explicit TypeScript major, could the peer dependency be removed or widened?

Async resource definition

Feature request

  • Allow route definition level code splitting.

Details

Right now the only way to code split resource logic is to implement getDataLoader for a resource, while resources are more or less static property on the route definition.

While getDataLoader seems to be the correct way of doing it - it allows some "already loaded resources" to act sooner - it creates some over complications if has to be compulsorily used, and in our case it is.

Proposal

  • extend resources field of Route to also accept a function resolving into a set of promises
  • or extend resource in resources to accept a LazyResource, and expose a helper function to convert import('my-resource') into such object.

What is solves

In feature request is not changing anything for the end customer, however makes life of a developer a little easier, decreasing the initial bundle size as well by making it easier to defer resources without any extra code written.

resolving peer dependency warnings for react 17

can we update the react dependencies in the package.json to remove these warnings?

warning " > [email protected]" has incorrect peer dependency "react@^16.8.0".
warning " > [email protected]" has incorrect peer dependency "react-dom@^16.8.0".

for context, our project is on react 17. would we be able to add

"peerDependencies": {
    "react": "^16.8 || ^17.0",
    "react-dom": "^16.8 || ^17.0",
  },

Support for nested routes

Hi!

This router looks great for render-as-you-fetch. But I noticed it doesn't appear to support nested routes?

This relay demo renders nested routing by rendering child routes as children. May be a possible implementation path.

Cannot change resourceContext between stories

Discovered in Storybook environment, when was not able to reset resourceContext between different stories.

Expectation: mounting/unmounting Router and especially MemoryRouter are not by previously used components.

Router state persists last value even when router is removed

Codesandbox: https://codesandbox.io/s/react-resource-router-basic-routing-example-forked-x621v9

When the router is present, the query param updates as expected.
However, when the router is removed, the query param value persists the latest value (although it does not update when the query param is changed again). I would expect the value to be undefined (or perhaps an error to be thrown) since there is no router present, but it appears that the useQueryParam reads the value from a global state?

I encountered this with some jest tests, where a component utilizing query parameters had two tests. One wrapped, one not wrapped. Modifying the query param within the component from the wrapped test would affect the value of the query param in the test where the component was not wrapped, behavior that I would not have expected.

E.g.

test 1:
    <MemoryRouter><Component /></MemoryRouter>
    perform some action that changes the query parameter

test 2:
    <Component />
    the query parameter value from the useQueryParam hook is persisted from the previous test

Advice on how to do "nested" routing

The lack of development on React router and my need for prefetching queries for React Relay has driven me to this project and I really like what I'm seeing. But I do need some advice (or an example) on how to deal with "nested" routes. I did read the existing issue on this topic but I don't fully understand what is said there: #89

Let's say I have a /about with several tabs: "teams" and "history". Which would the routes /about/teams and about/history respectively. As I understand from the issue above I would need to create "slots" in my About component that can then be filled depending on the route, so something like this:

export const routes = [
  {
      path: '/about/teams',
      name: 'ABOUT_TEAMS',
      component: () => <About slot={<AboutTeams/>}/>
  },
  {
      path: '/about/history',
      name: 'ABOUT_HISTORY',
      component: () => <About slot={<AboutHistory/>}/>
  }
]

the problem with this is since component is specified as ()=> it is re-rendered on every route change. If I instead follow the example the component is not re-rendered but now I can't specify the slot for sub-routes in any way

export const routes = [
  {
      path: '/about/teams',
      name: 'ABOUT_TEAMS',
      component: About // now how to specify the "slot" property
  },
  {
      path: '/about/history',
      name: 'ABOUT_HISTORY',
      component: About
  }
]

EDIT: having read #89 again and again I'm starting to think something like this is hinted at:

export const routes = [
  {
      path: '/about/teams',
      name: 'ABOUT_TEAMS',
      component: About // now how to specify the "slot" property
      slot: <Teams/> // or maybe just 'Teams'
  },
  {
      path: '/about/history',
      name: 'ABOUT_HISTORY',
      component: About
      slot: <History/> // or just 'History'
  }
]

but then how to get to that "slot" property in the About component? useRouter returns access to the matched route but the typescript definition doesn't account for the 'slot' property?

Dependant resources

Having used this a little more recently, one of the things that I’ve come across a few times is that I’d like to have resources that are dependent on the result of another resource.

Something like:

import { createResource, useResource } from "react-resource-router"
import accountResource from "./resources"

export const dependentOnAccountResource = createResource({
  type: "DEPENDENT",
  getKey: () => "dependent",
  resources: [accountResource],
  getData: async => {
    const [{ data }] = useResource(accountResource)
    return await fetch(`/feature_dependent_on_account_config/${data.account.id}`)
  },
})

This exact structure will fail obviously (not least because data will be null) but I wondered if it was something that felt possible/useful in the scope of this library. The reason I like something like it is that means:

  • Interfaces for fetching data can be consistent
  • Avoids having to do caching for dependent structures within backend infra

Would be interested to know if you’d felt similar pains, and if you’d solved them in other ways (or if I’ve missed something that means this is already easy). Thanks!

Routing for new tabs or windows are not recognising `basePath`

Given basePath is enabled.

When attempting to open a link in a new tab (or window) via a ctrl/cmd click or right click new tab (or window), the new location omits the basePath prefix.

This behaviour is apparent in the basic-routing example.

E.g.
When on http://localhost:8081/basic-routing/about, opening the Go to home link in a new tab (or window) will lead to http://localhost:8081/.

[Suggestion] Change `ResourceStoreContext` to use an interface instead of type so consumers can do declaration merging

When using react-resource-router, it would be great if the following:

export declare type ResourceStoreContext = any;

could be replaced with:

export declare interface ResourceStoreContext {}

So that consumers of the library can do this to declare a context type and get type information in TypeScript:

// react-resource-router.d.ts
import 'react-resource-router';
import type { MyResourceContext } from './my-resource-context';

declare module 'react-resource-router' {
    export interface ResourceStoreContext extends MyResourceContext {}
}

This is similar to what styled-components does with their global theme: https://styled-components.com/docs/api#create-a-declarations-file

Unfortunately it cannot be done (as far as I am aware) when using a type, so changing to an interface would be greatly appreciated :)

Edit: It actually seems the issue is using any, not type vs interface.
It seems this would fix the problem:

export declare type ResourceStoreContext = {}; // ignore

Edit 2: 99% sure I was actually right the first time, it has to be an interface not a type. Sorry about the confusion!

Allow creating custom hooks to reduce `useRouter` re-renders

Using useRouter everywhere is suboptimal as triggers a re-renders even when we might just care about route.name.
So we should allow consumers to create their own optimised selector hooks:

const useRouteName = createRouterSelector((o) => o.route.name);

History stack introspection

I think a router library should provide a mechanism to track the route history. Latest react-router have the useLocation hook to listen for route changes, which you can then push onto a stack if you like.

Either, we should have a mechanism to inspect the current history stack (no need for write access - just inspection) or a similar hook to listen for all route changes.

The ability to listen to changes is not very useful for implementing breadcrumbs for example so inspecting the history is the better API in my opinion.

Is there a practical way of listening to all route changes or preferably inspecting history with the current API?

Cleanup shape of useResource hook

Current useResource returns an array that has no reason to exist. It should be changed to:

const { data, loading, error } = useResource(myResource);

Stale while invalidate

Inside RRR there are two logics that are fighting with each other a little bit:


Questions


Proposal - add explicit stateWhileInvalidate flag for resource and exempt them from cleanExpiredResources giving control to the end user.

Breadcrumbs

react-router has a mechanism to allow creating breadcrumbs from the component tree. This is not something that is currently possible with RRR as there is no child route concept as of now.

The other potential solution is exposing an abstracted array of history entries so that we can use it to decide on the currently displayed breadcrumb. This is not ideal but is possible with some tinkering and a few limitations (like it is not possible to have same route twice in the breadcrumbs - which is not required in practice anyways).

Are there any plans on implementing such a mechanism?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.