├── 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 | 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 | --------------------------------------------------------------------------------