├── .nvmrc ├── examples ├── twitter │ ├── common │ │ ├── utilities │ │ │ ├── parseErrorJson.js │ │ │ ├── parseJson.js │ │ │ ├── checkHttpStatus.js │ │ │ ├── normalizeResponse.js │ │ │ ├── routes.jsx │ │ │ ├── propTypes.js │ │ │ ├── configureRoutes.jsx │ │ │ └── configureStore.js │ │ ├── selectors │ │ │ ├── getUser.js │ │ │ ├── getHomeTimeline.js │ │ │ ├── getUserTimeline.js │ │ │ └── getHomeTimelineErrors.js │ │ ├── components │ │ │ ├── Timeline.css │ │ │ ├── Application.css │ │ │ ├── Login.jsx │ │ │ ├── NotFound.jsx │ │ │ ├── TimelineItem.css │ │ │ ├── Timeline.jsx │ │ │ ├── Header.jsx │ │ │ ├── InternalServerError.jsx │ │ │ ├── Root.jsx │ │ │ ├── TimelineItem.jsx │ │ │ ├── UserTimeline.jsx │ │ │ ├── Application.jsx │ │ │ ├── Html.jsx │ │ │ ├── UserProfile.css │ │ │ ├── UserProfile.jsx │ │ │ └── HomeTimeline.jsx │ │ ├── reducers │ │ │ ├── authorization.js │ │ │ ├── user.js │ │ │ ├── index.js │ │ │ ├── userTimeline.js │ │ │ └── homeTimeline.js │ │ ├── constants │ │ │ └── ActionTypes.js │ │ └── actions │ │ │ ├── fetchHomeTimeline.js │ │ │ ├── fetchUser.js │ │ │ └── fetchUserTimeline.js │ ├── .babelrc │ ├── client │ │ ├── assets │ │ │ └── images │ │ │ │ └── favicon.ico │ │ └── index.jsx │ ├── config │ │ ├── custom-environment-variables.json │ │ └── default.json │ ├── server │ │ ├── controllers │ │ │ ├── twitter.js │ │ │ └── renderer.jsx │ │ ├── settings.js │ │ └── index.js │ ├── README.md │ ├── webpack.config.js │ ├── index.js │ └── package.json └── .eslintrc ├── .browserslistrc ├── CONTRIBUTORS ├── .eslintignore ├── src ├── isError.js ├── getDisplayName.js ├── uniqueId.js ├── ReadyState.js ├── ActionTypes.js ├── formatError.js ├── isSameLocation.js ├── useFetch.jsx ├── index.js ├── FetchContainer.jsx ├── selectors.js ├── reducer.js ├── FetchReadyStateRenderer.jsx ├── actions.js └── FetchRootContainer.jsx ├── .mocharc.js ├── test ├── utilities │ ├── createMockStore.js │ └── createMockRouterState.js ├── specs │ ├── index.spec.js │ ├── FetchContainer.spec.jsx │ ├── FetchReadyStateRenderer.spec.jsx │ ├── selectors.spec.js │ ├── reducer.spec.js │ ├── actions.spec.js │ └── FetchRootContainer.spec.jsx ├── .eslintrc └── setup.js ├── .travis.yml ├── .nycrc ├── .editorconfig ├── .eslintrc ├── docs └── guides │ ├── ErrorHandling.md │ └── ActivityIndicator.md ├── babel.config.js ├── LICENSE ├── .gitignore ├── package.json ├── CHANGELOG.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /examples/twitter/common/utilities/parseErrorJson.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | >0.2% 2 | not dead 3 | not op_mini all 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Christian Muñoz 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/coverage/** 2 | **/lib/** 3 | **/node_modules/** 4 | -------------------------------------------------------------------------------- /src/isError.js: -------------------------------------------------------------------------------- 1 | export default function isError(candidate) { 2 | return candidate instanceof Error; 3 | } 4 | -------------------------------------------------------------------------------- /examples/twitter/common/selectors/getUser.js: -------------------------------------------------------------------------------- 1 | export default function getUser(state) { 2 | return state.user; 3 | } 4 | -------------------------------------------------------------------------------- /examples/twitter/common/components/Timeline.css: -------------------------------------------------------------------------------- 1 | .Timeline { 2 | margin: 0; 3 | padding: 0; 4 | list-style: none; 5 | } 6 | -------------------------------------------------------------------------------- /examples/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-unresolved": "off", 4 | "import/extensions": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/twitter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@flowio/babel-preset-flowio"], 3 | "plugins": ["react-hot-loader/babel"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/twitter/common/selectors/getHomeTimeline.js: -------------------------------------------------------------------------------- 1 | export default function getHomeTimeline(state) { 2 | return state.homeTimeline.timeline; 3 | } 4 | -------------------------------------------------------------------------------- /examples/twitter/common/selectors/getUserTimeline.js: -------------------------------------------------------------------------------- 1 | export default function getUserTimeline(state) { 2 | return state.userTimeline.timeline; 3 | } 4 | -------------------------------------------------------------------------------- /src/getDisplayName.js: -------------------------------------------------------------------------------- 1 | export default function getDisplayName(Component) { 2 | return Component.displayName || Component.name || 'Component'; 3 | } 4 | -------------------------------------------------------------------------------- /examples/twitter/client/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flowcommerce/redux-fetch/HEAD/examples/twitter/client/assets/images/favicon.ico -------------------------------------------------------------------------------- /src/uniqueId.js: -------------------------------------------------------------------------------- 1 | let idCounter = 0; 2 | 3 | const uniqueId = () => { 4 | idCounter += 1; 5 | return idCounter.toString(); 6 | }; 7 | 8 | export default uniqueId; 9 | -------------------------------------------------------------------------------- /examples/twitter/common/reducers/authorization.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | 3 | const initialState = {}; 4 | 5 | export default handleActions({}, initialState); 6 | -------------------------------------------------------------------------------- /src/ReadyState.js: -------------------------------------------------------------------------------- 1 | const ReadyState = { 2 | FAILURE: 'failure', 3 | LOADING: 'loading', 4 | PENDING: 'pending', 5 | SUCCESS: 'success', 6 | }; 7 | 8 | export default ReadyState; 9 | -------------------------------------------------------------------------------- /examples/twitter/common/utilities/parseJson.js: -------------------------------------------------------------------------------- 1 | export default function parseJson(text) { 2 | try { 3 | return JSON.parse(text); 4 | } catch (error) { 5 | return text; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/twitter/config/custom-environment-variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "twitter": { 3 | "consumerKey": "CONF_TWITTER_CONSUMER_KEY", 4 | "consumerSecret": "CONF_TWITTER_CONSUMER_SECRET" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ui: 'bdd', 3 | require: [ 4 | '@babel/register', 5 | './test/setup.js' 6 | ], 7 | recursive: true, 8 | colors: true, 9 | checkLeaks: true, 10 | }; 11 | -------------------------------------------------------------------------------- /src/ActionTypes.js: -------------------------------------------------------------------------------- 1 | const ActionTypes = { 2 | FETCH_FAILURE: '@@fetch/FETCH_FAILURE', 3 | FETCH_REQUEST: '@@fetch/FETCH_REQUEST', 4 | FETCH_SUCCESS: '@@fetch/FETCH_SUCCESS', 5 | }; 6 | 7 | export default ActionTypes; 8 | -------------------------------------------------------------------------------- /examples/twitter/common/components/Application.css: -------------------------------------------------------------------------------- 1 | .Application__spinner { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | background-color: rgba(0, 0, 0, 0.25); 8 | z-index: 9999; 9 | } 10 | -------------------------------------------------------------------------------- /test/utilities/createMockStore.js: -------------------------------------------------------------------------------- 1 | import configureStore from 'redux-mock-store'; 2 | import thunk from 'redux-thunk'; 3 | 4 | const middlewares = [thunk]; 5 | 6 | const createMockStore = configureStore(middlewares); 7 | 8 | export default createMockStore; 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm run lint -- --quiet 9 | - npm run coverage 10 | branches: 11 | only: 12 | - main 13 | - /^greenkeeper/.*$/ 14 | -------------------------------------------------------------------------------- /src/formatError.js: -------------------------------------------------------------------------------- 1 | import isError from './isError'; 2 | 3 | export default function formatError(error) { 4 | if (!isError(error)) return error; 5 | return { 6 | message: error.message, 7 | name: error.name, 8 | stack: error.stack, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/isSameLocation.js: -------------------------------------------------------------------------------- 1 | const isSameLocation = (prevLocation, nextLocation) => ( 2 | prevLocation.pathname === nextLocation.pathname 3 | && prevLocation.search === nextLocation.search 4 | && prevLocation.key === nextLocation.key 5 | ); 6 | 7 | export default isSameLocation; 8 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*.{js,jsx}" 4 | ], 5 | "extension": [ 6 | ".js", 7 | ".jsx" 8 | ], 9 | "reporter": [ 10 | "html", 11 | "text" 12 | ], 13 | "cache": true, 14 | "sourceMap": false, 15 | "instrument": false 16 | } 17 | -------------------------------------------------------------------------------- /examples/twitter/common/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | 4 | const Login = () => ( 5 | 6 | ); 7 | 8 | Login.displayName = 'Login'; 9 | 10 | export default Login; 11 | -------------------------------------------------------------------------------- /examples/twitter/config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "hostname": "0.0.0.0", 3 | "port": "7051", 4 | "jwt": { 5 | "cookie": "token", 6 | "secret": "V8ehNaecfgvxxVtVSUrrMX1lAcq7cIdAZq", 7 | "ttl": 86400000 8 | }, 9 | "twitter": { 10 | "password": "hLKfgIOlfJKMb7x5AKucOUKTTMokQx70d6MNXR" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/specs/index.spec.js: -------------------------------------------------------------------------------- 1 | import * as ReduxFetch from '../../src/index'; 2 | 3 | // This test also serves as an entry point to import 4 | // the entire library for coverage reporting. 5 | 6 | describe('ReduxFetch', () => { 7 | it('should have exports', () => { 8 | expect(ReduxFetch).to.exist; 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | }, 9 | "rules": { 10 | "import/no-extraneous-dependencies": 0, 11 | "no-unused-expressions": 0, 12 | "react/no-multi-comp": 0, 13 | "react/prefer-stateless-function": 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | # unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # use utf-8 as default charset for scripts and styles 13 | [*.{js,css}] 14 | charset = utf-8 15 | -------------------------------------------------------------------------------- /examples/twitter/common/selectors/getHomeTimelineErrors.js: -------------------------------------------------------------------------------- 1 | export default function getHomeTimelineErrors(state) { 2 | let errors = []; 3 | 4 | if (state.user.error) { 5 | errors = errors.concat(state.user.error); 6 | } 7 | 8 | if (state.homeTimeline.error) { 9 | errors = errors.concat(state.homeTimeline.error); 10 | } 11 | 12 | return errors; 13 | } 14 | -------------------------------------------------------------------------------- /src/useFetch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FetchRootContainer from './FetchRootContainer'; 3 | 4 | export default function useFetch(options) { 5 | return { 6 | renderRouterContext: (child, renderProps) => ( 7 | 8 | {child} 9 | 10 | ), 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './selectors'; 2 | export { default as FetchRootContainer } from './FetchRootContainer'; 3 | export { fetchRouteData } from './actions'; 4 | export { default as reducer } from './reducer'; 5 | export { default as useFetch } from './useFetch'; 6 | export { default as withFetch } from './FetchContainer'; 7 | export { default as ActionTypes } from './ActionTypes'; 8 | -------------------------------------------------------------------------------- /examples/twitter/common/utilities/checkHttpStatus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple utility that rejects a request on any non 2XX response. 3 | */ 4 | export default function checkHttpStatus(response) { 5 | const { statusCode, statusText } = response; 6 | 7 | if (statusCode >= 200 && statusCode < 300) { 8 | return response; 9 | } 10 | 11 | const error = new Error(statusText); 12 | error.response = response; 13 | throw error; 14 | } 15 | -------------------------------------------------------------------------------- /test/utilities/createMockRouterState.js: -------------------------------------------------------------------------------- 1 | import defaultsDeep from 'lodash/defaultsDeep'; 2 | 3 | const defaults = { 4 | location: { 5 | pathname: 'search/cats', 6 | search: '', 7 | hash: '', 8 | action: 'PUSH', 9 | query: {}, 10 | }, 11 | routes: [], 12 | params: {}, 13 | components: [], 14 | }; 15 | 16 | const createMockRouterState = (state) => defaultsDeep(state, defaults); 17 | 18 | export default createMockRouterState; 19 | -------------------------------------------------------------------------------- /examples/twitter/common/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | import keyMirror from 'keymirror'; 2 | 3 | export default keyMirror({ 4 | FETCH_HOME_TIMELINE_REQUEST: null, 5 | FETCH_HOME_TIMELINE_SUCCESS: null, 6 | FETCH_HOME_TIMELINE_FAILURE: null, 7 | FETCH_USER_REQUEST: null, 8 | FETCH_USER_SUCCESS: null, 9 | FETCH_USER_FAILURE: null, 10 | FETCH_USER_TIMELINE_REQUEST: null, 11 | FETCH_USER_TIMELINE_SUCCESS: null, 12 | FETCH_USER_TIMELINE_FAILURE: null, 13 | }); 14 | -------------------------------------------------------------------------------- /examples/twitter/common/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import assign from 'lodash/assign'; 3 | 4 | import ActionTypes from '../constants/ActionTypes'; 5 | 6 | const initialState = { 7 | error: null, 8 | }; 9 | 10 | export default handleActions({ 11 | [ActionTypes.FETCH_USER_SUCCESS]: (state, action) => assign({}, state, action.payload), 12 | [ActionTypes.FETCH_USER_FAILURE]: (state, action) => assign({}, state, { error: action.payload }), 13 | }, initialState); 14 | -------------------------------------------------------------------------------- /src/FetchContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import hoistNonReactStatics from 'hoist-non-react-statics'; 3 | import getDisplayName from './getDisplayName'; 4 | 5 | export default (fetchAsyncState) => (WrappedComponent) => { 6 | const FetchContainer = (props) => (); 7 | FetchContainer.displayName = `WithFetch(${getDisplayName(WrappedComponent)})`; 8 | FetchContainer.fetchAsyncState = fetchAsyncState; 9 | return hoistNonReactStatics(FetchContainer, WrappedComponent); 10 | }; 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "jsx-a11y/anchor-is-valid": ["error", { 10 | "components": ["Link"], 11 | "specialLink": ["to"], 12 | "aspects": ["noHref", "invalidHref", "preferButton"] 13 | }], 14 | "max-classes-per-file": "warn", 15 | "react/forbid-prop-types": "warn", 16 | "react/jsx-props-no-spreading": "warn", 17 | "react/require-default-props": "warn" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/twitter/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reducer as fetch } from '@flowio/redux-fetch'; 3 | import { routerReducer as routing } from 'react-router-redux'; 4 | 5 | import authorization from './authorization'; 6 | import homeTimeline from './homeTimeline'; 7 | import user from './user'; 8 | import userTimeline from './userTimeline'; 9 | 10 | export default combineReducers({ 11 | authorization, 12 | fetch, 13 | homeTimeline, 14 | routing, 15 | user, 16 | userTimeline, 17 | }); 18 | -------------------------------------------------------------------------------- /examples/twitter/common/utilities/normalizeResponse.js: -------------------------------------------------------------------------------- 1 | import parseJson from './parseJson'; 2 | 3 | export default function normalizeResponse(response) { 4 | return new Promise((resolve, reject) => { 5 | response.text() 6 | .then(parseJson) 7 | .then((json) => { 8 | resolve({ 9 | ok: response.ok, 10 | data: json, 11 | statusCode: response.status, 12 | statusText: response.statusText, 13 | }); 14 | }).catch((error) => { 15 | reject(error); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /examples/twitter/common/utilities/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRedirect } from 'react-router'; 3 | 4 | import Application from '../components/Application'; 5 | import HomeTimeline from '../components/HomeTimeline'; 6 | import UserTimeline from '../components/UserTimeline'; 7 | 8 | export default ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /examples/twitter/common/reducers/userTimeline.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import assign from 'lodash/assign'; 3 | 4 | import ActionTypes from '../constants/ActionTypes'; 5 | 6 | const initialState = { 7 | error: null, 8 | timeline: [], 9 | }; 10 | 11 | export default handleActions({ 12 | [ActionTypes.FETCH_USER_TIMELINE_SUCCESS]: 13 | (state, action) => assign({}, state, { timeline: action.payload }), 14 | [ActionTypes.FETCH_USER_TIMELINE_FAILURE]: 15 | (state, action) => assign({}, state, { error: action.payload }), 16 | }, initialState); 17 | -------------------------------------------------------------------------------- /src/selectors.js: -------------------------------------------------------------------------------- 1 | import ReadyState from './ReadyState'; 2 | 3 | export const getLocation = (state) => state.fetch.location; 4 | export const getReadyState = (state) => state.fetch.readyState; 5 | export const getError = (state) => state.fetch.error; 6 | export const getIsPending = (state) => getReadyState(state) === ReadyState.PENDING; 7 | export const getIsLoading = (state) => getReadyState(state) === ReadyState.LOADING; 8 | export const getIsSuccess = (state) => getReadyState(state) === ReadyState.SUCCESS; 9 | export const getIsFailure = (state) => getReadyState(state) === ReadyState.FAILURE; 10 | -------------------------------------------------------------------------------- /examples/twitter/common/components/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Col, Grid, Jumbotron, Row, 4 | } from 'react-bootstrap'; 5 | import { Link } from 'react-router'; 6 | 7 | const NotFound = () => ( 8 | 9 | 10 | 11 | 12 |

Not Found

13 |

This is not the webpage that you are looking for.

14 | Home 15 |
16 | 17 |
18 |
19 | ); 20 | 21 | NotFound.displayName = 'NotFound'; 22 | 23 | export default NotFound; 24 | -------------------------------------------------------------------------------- /examples/twitter/common/reducers/homeTimeline.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions'; 2 | import assign from 'lodash/assign'; 3 | 4 | import ActionTypes from '../constants/ActionTypes'; 5 | 6 | const initialState = { 7 | error: null, 8 | timeline: [], 9 | }; 10 | 11 | export default handleActions({ 12 | [ActionTypes.FETCH_HOME_TIMELINE_SUCCESS]: 13 | (state, action) => assign({}, state, { timeline: action.payload, error: null }), 14 | [ActionTypes.FETCH_HOME_TIMELINE_FAILURE]: 15 | (state, action) => assign({}, state, { error: action.payload, timeline: [] }), 16 | }, initialState); 17 | -------------------------------------------------------------------------------- /examples/twitter/common/components/TimelineItem.css: -------------------------------------------------------------------------------- 1 | .TimelineItem { 2 | padding: 9px 12px; 3 | border-top: 1px solid #e1e8ed; 4 | border-right: 1px solid #e1e8ed; 5 | border-left: 1px solid #e1e8ed; 6 | background: #fff; 7 | background-clip: padding-box; 8 | } 9 | 10 | .TimelineItem:first-child { 11 | border-radius: 6px 6px 0 0; 12 | } 13 | 14 | .TimelineItem:last-child { 15 | border-bottom: 1px solid #e1e8ed; 16 | border-radius: 0 0 6px 6px; 17 | } 18 | 19 | .TimelineItem__media { 20 | float: left; 21 | width: 58px; 22 | } 23 | 24 | .TimelineItem__content { 25 | margin-left: 58px; 26 | } 27 | -------------------------------------------------------------------------------- /examples/twitter/common/utilities/propTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const { 4 | arrayOf, number, object, string, shape, 5 | } = PropTypes; 6 | 7 | export const errorShape = shape({ 8 | statusCode: number, 9 | error: string, 10 | message: string.isRequired, 11 | attributes: arrayOf(object), 12 | }); 13 | 14 | export const userShape = shape({ 15 | user: shape({ 16 | name: string.isRequired, 17 | screen_name: string.isRequired, 18 | }), 19 | }); 20 | 21 | export const timelineShape = shape({ 22 | text: string, 23 | user: shape({ 24 | name: string, 25 | }), 26 | }); 27 | -------------------------------------------------------------------------------- /examples/twitter/common/utilities/configureRoutes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRedirect } from 'react-router'; 3 | 4 | import Application from '../components/Application'; 5 | import HomeTimeline from '../components/HomeTimeline'; 6 | import UserTimeline from '../components/UserTimeline'; 7 | 8 | export default function configureRoutes() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/twitter/common/components/Timeline.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import BemHelper from 'react-bem-helper'; 4 | import map from 'lodash/map'; 5 | import TimelineItem from './TimelineItem'; 6 | import { timelineShape } from '../utilities/propTypes'; 7 | 8 | if (process.browser) { 9 | require('./Timeline.css'); // eslint-disable-line global-require 10 | } 11 | 12 | const classes = new BemHelper('Timeline'); 13 | 14 | const Timeline = ({ timeline }) => ( 15 |
    16 | {map(timeline, (tweet, index) => ( 17 | 18 | ))} 19 |
20 | ); 21 | 22 | Timeline.displayName = 'Timeline'; 23 | 24 | Timeline.propTypes = { 25 | timeline: PropTypes.arrayOf(timelineShape), 26 | }; 27 | 28 | export default Timeline; 29 | -------------------------------------------------------------------------------- /docs/guides/ErrorHandling.md: -------------------------------------------------------------------------------- 1 | # Error Handling 2 | 3 | Redux Fetch uses [`Promise.all`](https://mzl.la/29uN1k8) to wait until data requirements for route components matching a specific location are fulfilled. Therefore, the first rejected promise will indicate that data requirements cannot be fulfilled. 4 | 5 | It's up to you to decide what is considered an error in your application. For example, you may choose to reject on API responses outside the 2xx status code range or to never reject and store API errors and/or runtime errors that occur into the Redux store. 6 | 7 | Redux Fetch assumes that the first argument passed into its rejection handler 8 | is the error that occured and will store that into your Redux store by 9 | dispatching a `@@fetch/FETCH_FAILURE` action. Therefore, you should aggregate 10 | any information you want into the first argument of your rejection. 11 | -------------------------------------------------------------------------------- /examples/twitter/common/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navbar, Nav, NavItem } from 'react-bootstrap'; 3 | import { LinkContainer } from 'react-router-bootstrap'; 4 | 5 | const Header = () => ( 6 | 7 | 8 | Twitter 9 | 10 | 24 | 25 | ); 26 | 27 | Header.displayName = 'Header'; 28 | 29 | export default Header; 30 | -------------------------------------------------------------------------------- /examples/twitter/common/utilities/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import { createLogger } from 'redux-logger'; 4 | import reduxThunk from 'redux-thunk'; 5 | import combinedReducers from '../reducers'; 6 | 7 | const middleware = [reduxThunk]; 8 | 9 | if (process.browser) { 10 | middleware.push(createLogger({ level: 'info', collapsed: true })); 11 | } 12 | 13 | export default function configureStore(initialState = {}) { 14 | const store = createStore( 15 | combinedReducers, 16 | initialState, 17 | composeWithDevTools(applyMiddleware(...middleware)), 18 | ); 19 | 20 | if (module.hot) { 21 | module.hot.accept('../reducers', () => { 22 | // eslint-disable-next-line global-require 23 | store.replaceReducer(require('../reducers')); 24 | }); 25 | } 26 | 27 | return store; 28 | } 29 | -------------------------------------------------------------------------------- /examples/twitter/common/components/InternalServerError.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | Button, Col, Grid, Jumbotron, Row, 5 | } from 'react-bootstrap'; 6 | 7 | const InternalServerError = ({ message, retry, ...otherProps }) => ( 8 | 9 | 10 | 11 | 12 |

Internal Server Error

13 |

An error has occured while trying to fulfill your request.

14 |
15 |             {message}
16 |           
17 | 18 |
19 | 20 |
21 |
22 | ); 23 | 24 | InternalServerError.displayName = 'InternalServerError'; 25 | 26 | InternalServerError.propTypes = { 27 | message: PropTypes.string, 28 | retry: PropTypes.func, 29 | }; 30 | 31 | export default InternalServerError; 32 | -------------------------------------------------------------------------------- /examples/twitter/server/controllers/twitter.js: -------------------------------------------------------------------------------- 1 | import { OAuth } from 'oauth'; 2 | import Config from 'config'; 3 | import Url from 'url'; 4 | 5 | export default function (request, reply) { 6 | const oauth = new OAuth( 7 | 'https://api.twitter.com/oauth/request_token', 8 | 'https://api.twitter.com/oauth/access_token', 9 | Config.get('twitter.consumerKey'), 10 | Config.get('twitter.consumerSecret'), 11 | '1.0A', 12 | null, 13 | 'HMAC-SHA1', 14 | ); 15 | 16 | const url = Url.format({ 17 | protocol: 'https', 18 | hostname: 'api.twitter.com', 19 | pathname: `/1.1/${request.params.endpoint}`, 20 | query: request.query, 21 | }); 22 | 23 | oauth.get( 24 | url, 25 | request.auth.credentials.token, 26 | request.auth.credentials.secret, 27 | (error, data) => { 28 | if (error) { 29 | reply(JSON.parse(error.data)).code(error.statusCode); 30 | } else { 31 | reply(JSON.parse(data)); 32 | } 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /examples/twitter/common/components/Root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { applyRouterMiddleware, browserHistory, Router } from 'react-router'; 4 | import { Provider } from 'react-redux'; 5 | import { syncHistoryWithStore } from 'react-router-redux'; 6 | import { useFetch } from '@flowio/redux-fetch'; 7 | 8 | import configureRoutes from '../utilities/configureRoutes'; 9 | 10 | const routes = configureRoutes(); 11 | 12 | const Root = ({ store }) => ( 13 | 14 | 19 | 20 | ); 21 | 22 | Root.displayName = 'Root'; 23 | 24 | Root.propTypes = { 25 | store: PropTypes.shape({ 26 | subscribe: PropTypes.func.isRequired, 27 | dispatch: PropTypes.func.isRequired, 28 | getState: PropTypes.func.isRequired, 29 | }), 30 | }; 31 | 32 | export default Root; 33 | -------------------------------------------------------------------------------- /examples/twitter/common/components/TimelineItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BemHelper from 'react-bem-helper'; 3 | import { timelineShape } from '../utilities/propTypes'; 4 | 5 | if (process.browser) { 6 | require('./TimelineItem.css'); // eslint-disable-line global-require 7 | } 8 | 9 | const classes = new BemHelper('TimelineItem'); 10 | 11 | const TimelineItem = ({ tweet }) => ( 12 |
  • 13 |
    14 | {tweet.user.name} 15 |
    16 |
    17 |

    18 | {tweet.user.name} 19 | 20 | @ 21 | {tweet.user.screen_name} 22 | 23 |

    24 |

    {tweet.text}

    25 |
    26 |
  • 27 | ); 28 | 29 | TimelineItem.displayName = 'TimelineItem'; 30 | 31 | TimelineItem.propTypes = { 32 | tweet: timelineShape.isRequired, 33 | }; 34 | 35 | export default TimelineItem; 36 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const { version: babelRuntimeVersion } = require('@babel/runtime/package.json'); 2 | 3 | module.exports = { 4 | presets: [ 5 | ['@babel/preset-env', { 6 | // Exclude transforms that make all code slower. 7 | // See https://github.com/facebook/create-react-app/pull/5278 8 | exclude: ['transform-typeof-symbol'], 9 | }], 10 | '@babel/preset-react', 11 | ], 12 | plugins: [ 13 | ['@babel/plugin-transform-runtime', { 14 | corejs: false, 15 | helpers: true, 16 | regenerator: false, 17 | // By default, babel assumes babel/runtime version 7.0.0-beta.0, 18 | // explicitly resolving to match the provided helper functions. 19 | // https://github.com/babel/babel/issues/10261 20 | version: babelRuntimeVersion, 21 | }], 22 | '@babel/plugin-proposal-class-properties', 23 | ['@babel/plugin-proposal-object-rest-spread', { 24 | useBuiltIns: true, 25 | }], 26 | ], 27 | env: { 28 | test: { 29 | plugins: ['babel-plugin-istanbul'], 30 | }, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Flow Commerce, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in the 5 | Software without restriction, including without limitation the rights to use, 6 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 7 | Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/twitter/client/index.jsx: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { AppContainer } from 'react-hot-loader'; 6 | 7 | import configureStore from '../common/utilities/configureStore'; 8 | 9 | const store = configureStore(window.$REDUX_STATE); 10 | 11 | const rootElement = document.querySelector('#react-root'); 12 | 13 | function render() { 14 | // eslint-disable-next-line global-require 15 | const Root = require('../common/components/Root').default; 16 | 17 | ReactDOM.render( 18 | 19 | 20 | , 21 | rootElement, 22 | ); 23 | } 24 | 25 | render(); 26 | 27 | // Hot Module Replacement API 28 | if (module.hot) { 29 | module.hot.accept('../common/components/Root', () => { 30 | // Workaround to support hot swapping of routes. 31 | // Follow this thread to understand the reasoning behind it. 32 | // https://github.com/ReactTraining/react-router/issues/2182 33 | setTimeout(() => { 34 | ReactDOM.unmountComponentAtNode(rootElement); 35 | render(); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /examples/twitter/common/components/UserTimeline.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Grid, Row, Col } from 'react-bootstrap'; 4 | import { compose } from 'redux'; 5 | import { connect } from 'react-redux'; 6 | import { withFetch } from '@flowio/redux-fetch'; 7 | 8 | import Timeline from './Timeline'; 9 | import { timelineShape } from '../utilities/propTypes'; 10 | import getUserTimeline from '../selectors/getUserTimeline'; 11 | import fetchUserTimeline from '../actions/fetchUserTimeline'; 12 | 13 | const UserTimeline = ({ timeline }) => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | 23 | UserTimeline.propTypes = { 24 | timeline: PropTypes.arrayOf(timelineShape), 25 | }; 26 | 27 | function fetchAsyncState(dispatch) { 28 | return dispatch(fetchUserTimeline()); 29 | } 30 | 31 | function mapStateToProps(state) { 32 | return { 33 | timeline: getUserTimeline(state), 34 | }; 35 | } 36 | 37 | export default compose( 38 | withFetch(fetchAsyncState), 39 | connect(mapStateToProps), 40 | )(UserTimeline); 41 | -------------------------------------------------------------------------------- /examples/twitter/common/components/Application.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Spinner from 'react-spinner'; 4 | import BemHelper from 'react-bem-helper'; 5 | import { connect } from 'react-redux'; 6 | import { getIsPending, getIsLoading } from '@flowio/redux-fetch'; 7 | import Header from './Header'; 8 | 9 | if (process.browser) { 10 | require('react-spinner/react-spinner.css'); // eslint-disable-line global-require 11 | require('./Application.css'); // eslint-disable-line global-require 12 | } 13 | 14 | const classes = new BemHelper('Application'); 15 | 16 | const Application = ({ children, loading }) => ( 17 |
    18 | {loading && ( 19 |
    20 | 21 |
    22 | )} 23 |
    24 | {children} 25 |
    26 | ); 27 | 28 | Application.displayName = 'Application'; 29 | 30 | Application.propTypes = { 31 | children: PropTypes.node, 32 | loading: PropTypes.bool, 33 | }; 34 | 35 | const mapStateToProps = (state) => ({ 36 | loading: getIsLoading(state) || getIsPending(state), 37 | }); 38 | 39 | export default connect(mapStateToProps)(Application); 40 | -------------------------------------------------------------------------------- /examples/twitter/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | Before running this example application you will need to make sure all 4 | dependencies are installed and the environment is configured with your 5 | Twitter consumer secret and key. 6 | 7 | ### Installing dependencies 8 | 9 | To install all dependencies navigate to the root of the project and run the 10 | `npm install` command. 11 | 12 | ```sh 13 | git clone git@github.com:flowcommerce/redux-fetch.git 14 | cd redux-fetch/examples/twitter 15 | npm install 16 | ``` 17 | 18 | ### Configuring environment 19 | 20 | You will need to create a Twitter application to obtain a **consumer key** 21 | and **consumer secret**. Instructions to create a Twitter application can be 22 | found at [http://bit.ly/2hWz5X5](http://bit.ly/2hWz5X5). 23 | 24 | Once you have created a Twitter application, define two environment variables 25 | with the consumer key and secret. 26 | 27 | ```sh 28 | export CONF_TWITTER_CONSUMER_KEY=XXXXXXXXXXXXXXXXX 29 | export CONF_TWITTER_CONSUMER_SECRET=XXXXXXXXXXXXXXXXXXXXXX 30 | ``` 31 | 32 | ### Running application 33 | 34 | To run this application execute the `npm start` command from the root directory 35 | and navigate to `https://localhost:7050` using your favorite browser. 36 | -------------------------------------------------------------------------------- /examples/twitter/server/settings.js: -------------------------------------------------------------------------------- 1 | import Config from 'config'; 2 | import Path from 'path'; 3 | import webpackConfig from '../webpack.config'; 4 | 5 | export default { 6 | connections: [{ 7 | host: Config.get('hostname'), 8 | port: Config.get('port'), 9 | routes: { 10 | files: { 11 | relativeTo: Path.resolve(__dirname, '../client/assets'), 12 | }, 13 | }, 14 | }], 15 | registrations: [{ 16 | plugin: 'bell', 17 | }, { 18 | plugin: 'hapi-auth-jwt2', 19 | }, { 20 | plugin: 'inert', 21 | }, { 22 | plugin: { 23 | register: 'good', 24 | options: { 25 | ops: { interval: 1500 }, 26 | reporters: { 27 | console: [{ 28 | module: 'good-squeeze', 29 | name: 'Squeeze', 30 | args: [{ log: '*', request: '*', response: '*' }], 31 | }, { 32 | module: 'good-console', 33 | }, 'stdout'], 34 | }, 35 | }, 36 | }, 37 | }, { 38 | plugin: { 39 | register: 'hapi-webpack-dev-middleware', 40 | options: { 41 | config: webpackConfig, 42 | options: { 43 | noInfo: true, 44 | publicPath: webpackConfig.output.publicPath, 45 | }, 46 | }, 47 | }, 48 | }, { 49 | plugin: 'hapi-webpack-hot-middleware', 50 | }], 51 | }; 52 | -------------------------------------------------------------------------------- /examples/twitter/webpack.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import path from 'path'; 3 | 4 | export default { 5 | devtool: '#inline-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | 'react-hot-loader/patch', 9 | path.resolve(__dirname, './client/index.jsx'), 10 | ], 11 | output: { 12 | path: path.resolve(__dirname, './client/assets'), 13 | publicPath: '/assets/', 14 | filename: 'bundle.js', 15 | }, 16 | resolve: { 17 | extensions: ['.js', '.jsx', '.json'], 18 | }, 19 | module: { 20 | rules: [{ 21 | test: /\.(js|jsx)$/, 22 | use: [ 23 | { loader: 'babel-loader' }, 24 | ], 25 | include: [ 26 | path.resolve(__dirname, './client'), 27 | path.resolve(__dirname, './common'), 28 | ], 29 | }, { 30 | test: /\.(css)$/, 31 | use: [ 32 | { loader: 'style-loader' }, 33 | { loader: 'css-loader' }, 34 | ], 35 | include: [ 36 | path.resolve(__dirname, './client'), 37 | path.resolve(__dirname, './common'), 38 | path.resolve(__dirname, './node_modules/react-spinner'), 39 | ], 40 | }], 41 | }, 42 | plugins: [ 43 | new webpack.HotModuleReplacementPlugin(), 44 | new webpack.NamedModulesPlugin(), 45 | new webpack.NoEmitOnErrorsPlugin(), 46 | ], 47 | }; 48 | -------------------------------------------------------------------------------- /examples/twitter/index.js: -------------------------------------------------------------------------------- 1 | // use babel-register to precompile ES6 syntax 2 | require('babel-register'); 3 | 4 | const Chokidar = require('chokidar'); 5 | const Hoek = require('hoek'); 6 | const composer = require('./server').default; 7 | 8 | composer.then((server) => { 9 | // Do "hot-reloading" of Hapi stuff on the server 10 | // Throw away cached modules and re-require next time 11 | // Ensure there's no important state in there! 12 | const watcher = Chokidar.watch('./server'); 13 | 14 | watcher.on('ready', () => { 15 | watcher.on('all', () => { 16 | console.log('Clearing /server/* module cache from server'); 17 | Object.keys(require.cache).forEach((id) => { 18 | if (/[/\\]server[/\\]/.test(id)) delete require.cache[id]; 19 | }); 20 | }); 21 | }); 22 | 23 | // Do "hot-reloading" of react stuff on the server 24 | // Throw away the cached client modules and let them be re-required next time 25 | server.app.webpackCompiler.plugin('done', () => { 26 | console.log('Clearing /common/* module cache from server'); 27 | Object.keys(require.cache).forEach((id) => { 28 | if (/[/\\]common[/\\]/.test(id)) delete require.cache[id]; 29 | }); 30 | }); 31 | 32 | server.start((error) => { 33 | Hoek.assert(!error, error); 34 | console.info(`Server running at: ${server.info.uri}`); 35 | }); 36 | }).catch((error) => { 37 | Hoek.assert(!error, error); 38 | }); 39 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import ActionTypes from './ActionTypes'; 2 | import ReadyState from './ReadyState'; 3 | 4 | const defaultState = { 5 | readyState: ReadyState.PENDING, 6 | }; 7 | 8 | const isSameFetchId = (state, action) => state.fetchId === action.fetchId; 9 | 10 | const requestReducer = (state, action) => ({ 11 | ...state, 12 | fetchId: action.fetchId, 13 | location: action.location, 14 | readyState: ReadyState.LOADING, 15 | }); 16 | 17 | const failureReducer = (state, action) => { 18 | if (!isSameFetchId(state, action)) return state; 19 | return { 20 | ...state, 21 | error: action.error, 22 | fetchId: action.fetchId, 23 | location: action.location, 24 | readyState: ReadyState.FAILURE, 25 | }; 26 | }; 27 | 28 | const successReducer = (state, action) => { 29 | if (!isSameFetchId(state, action)) return state; 30 | return { 31 | ...state, 32 | fetchId: action.fetchId, 33 | location: action.location, 34 | readyState: ReadyState.SUCCESS, 35 | }; 36 | }; 37 | 38 | export default function (state = defaultState, action) { 39 | switch (action.type) { 40 | case ActionTypes.FETCH_FAILURE: 41 | return failureReducer(state, action); 42 | case ActionTypes.FETCH_REQUEST: 43 | return requestReducer(state, action); 44 | case ActionTypes.FETCH_SUCCESS: 45 | return successReducer(state, action); 46 | default: 47 | return state; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/twitter/common/actions/fetchHomeTimeline.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import { createAction } from 'redux-actions'; 3 | 4 | import ActionTypes from '../constants/ActionTypes'; 5 | import checkHttpStatus from '../utilities/checkHttpStatus'; 6 | import normalizeResponse from '../utilities/normalizeResponse'; 7 | 8 | const fetchHomeTimelineRequest = createAction(ActionTypes.FETCH_HOME_TIMELINE_REQUEST); 9 | const fetchHomeTimelineSuccess = createAction(ActionTypes.FETCH_HOME_TIMELINE_SUCCESS); 10 | const fetchHomeTimelineFailure = createAction(ActionTypes.FETCH_HOME_TIMELINE_FAILURE); 11 | 12 | export default function fetchHomeTimeline() { 13 | return (dispatch, getState) => { 14 | const state = getState(); 15 | 16 | const fetchUrl = url.format({ 17 | protocol: 'http', 18 | host: 'localhost:7051', 19 | pathname: '/api/statuses/home_timeline.json', 20 | }); 21 | 22 | const fetchOptions = { 23 | headers: { 24 | Authorization: `Bearer ${state.authorization.token}`, 25 | }, 26 | }; 27 | 28 | dispatch(fetchHomeTimelineRequest()); 29 | 30 | return fetch(fetchUrl, fetchOptions) 31 | .then(normalizeResponse) 32 | .then(checkHttpStatus) 33 | .then((response) => { 34 | dispatch(fetchHomeTimelineSuccess(response.data)); 35 | }) 36 | .catch((error) => { 37 | const payload = error.response ? error.response.data : error; 38 | dispatch(fetchHomeTimelineFailure(payload)); 39 | }); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /examples/twitter/common/actions/fetchUser.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import { createAction } from 'redux-actions'; 3 | 4 | import ActionTypes from '../constants/ActionTypes'; 5 | import checkHttpStatus from '../utilities/checkHttpStatus'; 6 | import normalizeResponse from '../utilities/normalizeResponse'; 7 | 8 | const fetchUserRequest = createAction(ActionTypes.FETCH_USER_REQUEST); 9 | const fetchUserSuccess = createAction(ActionTypes.FETCH_USER_SUCCESS); 10 | const fetchUserFailure = createAction(ActionTypes.FETCH_USER_FAILURE); 11 | 12 | export default function fetchUser() { 13 | return (dispatch, getState) => { 14 | const state = getState(); 15 | 16 | const fetchUrl = url.format({ 17 | protocol: 'http', 18 | host: 'localhost:7051', 19 | pathname: '/api/users/show.json', 20 | query: { 21 | user_id: state.authorization.user_id, 22 | screen_name: state.authorization.screen_name, 23 | }, 24 | }); 25 | 26 | const fetchOptions = { 27 | headers: { 28 | Authorization: `Bearer ${state.authorization.token}`, 29 | }, 30 | }; 31 | 32 | dispatch(fetchUserRequest()); 33 | 34 | return fetch(fetchUrl, fetchOptions) 35 | .then(normalizeResponse) 36 | .then(checkHttpStatus) 37 | .then((response) => { 38 | dispatch(fetchUserSuccess(response.data)); 39 | }) 40 | .catch((error) => { 41 | const payload = error.response ? error.response.data : error; 42 | dispatch(fetchUserFailure(payload)); 43 | }); 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /examples/twitter/common/components/Html.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | // A utility function to safely escape JSON for embedding in a