├── README.md
├── src
├── withCache.js
├── Timeout.js
├── index.js
├── matchPath.js
├── MiniRouter.js
└── News.js
├── package.json
└── public
└── index.html
/README.md:
--------------------------------------------------------------------------------
1 | # react-suspense-playground
2 |
3 | This is a little Stock Market News app. I built it to play around with React suspense and other async stuff.
4 |
--------------------------------------------------------------------------------
/src/withCache.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SimpleCache } from 'simple-cache-provider';
3 |
4 | export function withCache(Component) {
5 | return props => (
6 |
7 | {cache => }
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/Timeout.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 |
3 | export function Timeout({ ms, fallback, children }) {
4 | return (
5 |
6 | {didTimeout => (
7 |
8 | {children}
9 | {didTimeout ? fallback : null}
10 |
11 | )}
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-suspense-demo-market-news",
3 | "version": "1.0.0",
4 | "description": "Playing around with React suspense",
5 | "keywords": ["react", "suspense", "future"],
6 | "homepage": "https://codesandbox.io/s/new",
7 | "main": "src/index.js",
8 | "dependencies": {
9 | "glamor": "2.20.40",
10 | "history": "latest",
11 | "path-to-regexp": "2.1.0",
12 | "react": "16.4.0-alpha.0911da3",
13 | "react-dom": "16.4.0-alpha.0911da3",
14 | "react-router-dom": "4.2.2",
15 | "react-scripts": "1.1.0",
16 | "simple-cache-provider": "0.3.0-alpha.0911da3"
17 | },
18 | "devDependencies": {},
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test --env=jsdom",
23 | "eject": "react-scripts eject"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { createElement } from 'glamor/react';
4 | /* @jsx createElement */
5 | import { withCache } from './withCache';
6 | import { createResource } from 'simple-cache-provider';
7 | import { Route, Link, Router } from './MiniRouter';
8 |
9 | // This is a little news app that reads stock market news. It leverages
10 | // React suspense.
11 |
12 | // this resource lazy loads the news component
13 | const getNewsComponent = createResource(() =>
14 | import('./News').then(module => module.default)
15 | );
16 |
17 | // this just gets the component or reads it from the cache
18 | const NewsLoader = withCache(props => {
19 | const News = getNewsComponent(props.cache);
20 | return ;
21 | });
22 |
23 | function App() {
24 | return (
25 |
31 |
32 | } />
33 | }
36 | />
37 |
38 |
39 | );
40 | }
41 |
42 | const container = document.getElementById('root');
43 | const root = ReactDOM.createRoot(container);
44 |
45 | root.render(
46 |
47 |
48 |
49 | );
50 |
--------------------------------------------------------------------------------
/src/matchPath.js:
--------------------------------------------------------------------------------
1 | import pathToRegexp from 'path-to-regexp';
2 |
3 | const patternCache = {};
4 | const cacheLimit = 10000;
5 | let cacheCount = 0;
6 |
7 | const compilePath = (pattern, options) => {
8 | const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
9 | const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});
10 |
11 | if (cache[pattern]) return cache[pattern];
12 |
13 | const keys = [];
14 | const re = pathToRegexp(pattern, keys, options);
15 | const compiledPattern = { re, keys };
16 |
17 | if (cacheCount < cacheLimit) {
18 | cache[pattern] = compiledPattern;
19 | cacheCount++;
20 | }
21 |
22 | return compiledPattern;
23 | };
24 |
25 | /**
26 | * Public API for matching a URL pathname to a path pattern.
27 | */
28 | export const matchPath = (pathname, options = {}, parent) => {
29 | if (typeof options === 'string') options = { path: options };
30 |
31 | const { path, exact = false, strict = false, sensitive = false } = options;
32 |
33 | if (path == null) return parent;
34 |
35 | const { re, keys } = compilePath(path, { end: exact, strict, sensitive });
36 | const match = re.exec(pathname);
37 |
38 | if (!match) return null;
39 |
40 | const [url, ...values] = match;
41 | const isExact = pathname === url;
42 |
43 | if (exact && !isExact) return null;
44 |
45 | return {
46 | path, // the path pattern used to match
47 | url: path === '/' && url === '' ? '/' : url, // the matched portion of the URL
48 | isExact, // whether or not we matched exactly
49 | params: keys.reduce((memo, key, index) => {
50 | memo[key.name] = values[index];
51 | return memo;
52 | }, {}),
53 | };
54 | };
55 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | React App
24 |
25 |
26 |
27 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/MiniRouter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createBrowserHistory } from 'history';
3 | import { matchPath } from './matchPath';
4 | const RouterContext = React.createContext(null);
5 |
6 | export const withRouter = Comp => props => (
7 |
8 | {routeProps => }
9 |
10 | );
11 |
12 | export class Router extends React.Component {
13 | history = createBrowserHistory();
14 |
15 | state = {
16 | location: this.history.location,
17 | };
18 |
19 | componentDidMount() {
20 | this.history.listen(() => {
21 | this.setState({
22 | location: this.history.location,
23 | });
24 | });
25 | }
26 |
27 | render() {
28 | return (
29 |
35 | {this.props.children}
36 |
37 | );
38 | }
39 | }
40 |
41 | class RouteImpl extends React.Component {
42 | state = {
43 | match: matchPath(this.props.location.pathname, this.props.path),
44 | };
45 |
46 | static getDerivedStateFromProps(props) {
47 | return { match: matchPath(props.location.pathname, props.path) };
48 | }
49 |
50 | render() {
51 | const { path, location, history, render, component: Component, exact } = this.props;
52 | const { match } = this.state;
53 | const props = { location, history, match };
54 | if (match && match.isExact) {
55 | if (render) {
56 | return render(props);
57 | } else if (Component) {
58 | return ;
59 | } else {
60 | return null;
61 | }
62 | } else {
63 | return null;
64 | }
65 | }
66 | }
67 |
68 | export const Route = withRouter(RouteImpl);
69 |
70 | class LinkImpl extends React.Component {
71 | handleClick = e => {
72 | e.preventDefault();
73 | this.props.history.push(this.props.to);
74 | };
75 |
76 | render() {
77 | return (
78 |
79 | {this.props.children}
80 |
81 | );
82 | }
83 | }
84 |
85 | export const Link = withRouter(LinkImpl);
86 |
--------------------------------------------------------------------------------
/src/News.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createElement } from 'glamor/react';
3 | /* @jsx createElement */
4 | import { withCache } from './withCache';
5 | import { createResource } from 'simple-cache-provider';
6 | import { Link } from './MiniRouter';
7 |
8 | const readNews = createResource(async function fetchNews(ticker) {
9 | const res = await fetch(`https://api.iextrading.com/1.0/stock/${ticker}/news`);
10 | return await res.json();
11 | });
12 |
13 | class News extends React.Component {
14 | componentDidUpdate(prevProps) {
15 | if (prevProps.ticker !== this.props.ticker) {
16 | window.scrollTo(0, 0);
17 | }
18 | }
19 | render() {
20 | const { cache, ticker = 'aapl' } = this.props;
21 | const results = readNews(cache, ticker);
22 | return (
23 |
24 | {ticker} news
25 |
26 | {results &&
27 | results.length > 0 &&
28 | results.map(r => (
29 |
30 |
{r.headline}
31 |
35 |
36 | Related:{' '}
37 | {r.related
38 | .split(',')
39 | .filter(z => z.length === 4)
40 | .map(co => (
41 |
52 | {co}
53 |
54 | ))}
55 |
56 |
57 | ))}
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | export default News;
65 |
--------------------------------------------------------------------------------