├── .babelrc
├── .circleci
└── config.yml
├── .eslintrc.json
├── .gitignore
├── .jestrc.json
├── .npmignore
├── .prettierrc.json
├── README.md
├── codecov.yml
├── package.json
├── rollup.config.js
├── src
├── connect.js
├── context.js
├── handler.js
├── index.js
├── map-dispatch-to-props.js
├── map-request-to-props.js
└── provider.js
├── tests
├── config.test.js
├── connect.test.js
├── errors-on-map-requests-to-props.test.js
├── library.test.js
├── map-dispatch-to-props.test.js
└── map-requests-to-props.test.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {"modules": false}]
4 | ],
5 | "plugins": ["transform-object-rest-spread"],
6 | "env": {
7 | "test": {
8 | "presets": ["env", "react"]
9 | },
10 | "rollup": {
11 | "presets": [["env", {"modules": false}], "react"],
12 | "plugins": ["transform-object-rest-spread", "external-helpers"]
13 | },
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | test:
4 | docker:
5 | - image: circleci/node:9.11.1
6 | working_directory: ~/repo
7 | steps:
8 | - checkout
9 | - restore_cache:
10 | name: Restore Yarn Package Cache
11 | keys:
12 | - yarn-packages-{{ .Branch }}-{{ checksum "yarn.lock" }}
13 | - yarn-packages-{{ .Branch }}
14 | - yarn-packages-master
15 | - yarn-packages-
16 | - run: yarn install
17 |
18 | - save_cache:
19 | name: Save Yarn Package Cache
20 | key: yarn-packages-{{ .Branch }}-{{ checksum "yarn.lock" }}
21 | paths:
22 | - node_modules/
23 | - run: yarn test
24 | - run: sudo yarn global add codecov
25 | - run: codecov --token=$CODECOV_TOKEN
26 |
27 | build:
28 | docker:
29 | - image: circleci/node:9.11.1
30 | working_directory: ~/repo
31 | steps:
32 | - checkout
33 | - restore_cache:
34 | name: Restore Yarn Package Cache
35 | keys:
36 | - yarn-packages-{{ .Branch }}-{{ checksum "yarn.lock" }}
37 | - yarn-packages-{{ .Branch }}
38 | - yarn-packages-master
39 | - yarn-packages-
40 | - run: yarn build
41 | - persist_to_workspace:
42 | root: ~/repo
43 | paths:
44 | - lib
45 |
46 | publish:
47 | docker:
48 | - image: circleci/node:9.11.1
49 | working_directory: ~/repo
50 | steps:
51 | - checkout
52 | - attach_workspace:
53 | at: ~/repo
54 | - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
55 | - run: npm publish
56 |
57 | workflows:
58 | version: 2
59 | test-build-and-deploy:
60 | jobs:
61 | - test
62 | - build:
63 | requires:
64 | - test
65 | - publish:
66 | requires:
67 | - build
68 | filters:
69 | branches:
70 | only:
71 | - publish
72 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["airbnb", "prettier", "prettier/react"],
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "jest": true
8 | },
9 | "rules": {
10 | "semi": ["error", "never"],
11 | "no-console": [
12 | "error",
13 | {
14 | "allow": ["warn", "info", "error"]
15 | }
16 | ],
17 | "import/named": "error",
18 | "import/prefer-default-export": "off",
19 | "no-multiple-empty-lines": [
20 | "error",
21 | {
22 | "max": 1,
23 | "maxEOF": 1,
24 | "maxBOF": 0
25 | }
26 | ],
27 | "jsx-a11y/href-no-hash": 0,
28 | "import/order": [
29 | "error",
30 | {
31 | "groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
32 | "newlines-between": "always"
33 | }
34 | ],
35 | "react/jsx-filename-extension": "off",
36 | "arrow-parens": ["error", "as-needed"],
37 | "prettier/prettier": [
38 | "error",
39 | {
40 | "singleQuote": true,
41 | "trailingComma": "es5",
42 | "bracketSpacing": true,
43 | "jsxBracketSameLine": false,
44 | "semi": false,
45 | "parser": "babylon",
46 | "printWidth": 100
47 | }
48 | ]
49 | },
50 | "plugins": ["react", "prettier"]
51 | }
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage
2 | node_modules
3 | lib
4 |
--------------------------------------------------------------------------------
/.jestrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "verbose": true,
3 | "collectCoverage": true,
4 | "setupFiles": ["./tests/config.test.js"],
5 | "testPathIgnorePatterns": ["./tests/config.test.js"],
6 | "transform": {
7 | "^.+\\.js?$": "babel-jest"
8 | },
9 | "moduleFileExtensions": ["js"]
10 | }
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | .circleci
3 | coverage
4 | tests
5 | .babelrc
6 | .eslintrc.json
7 | .prettierrc.json
8 | codecov.yml
9 | rollup.config.js
10 | yarn.lock
11 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "trailingComma": "es5",
6 | "bracketSpacing": true,
7 | "parser": "babylon",
8 | "semi": false
9 | }
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-fetches
2 |
3 | [](https://greenkeeper.io/)
4 | [](https://codecov.io/gh/dleitee/react-fetches)
5 | [](https://circleci.com/gh/dleitee/react-fetches/tree/master)
6 |
7 | React Fetches is a simple and efficient way to make requests into your REST API's.
8 |
9 | ## Table of Contents
10 |
11 | - [Motivation](#motivation)
12 | - [Install](#install)
13 | - [Basic Example](#basic-example)
14 | - [API Reference](#api-reference)
15 | - [connect](#connectmaprequesttoprops-mapdispatchtopropscomponent)
16 | - [mapRequestToProps](#maprequesttoprops--http-map--object)
17 | - [mapDispatchToProps](#mapdispatchtoprops--http-dispatch--object)
18 | - [Inspirations](#inspirations)
19 | - [Support](#support)
20 | - [How to Contribute](#how-to-contribute)
21 | - [License](#license)
22 |
23 | ## Motivation
24 |
25 | Me and my friends were tired to use a lot of boilerplate code to do our requests.
26 |
27 | We used to build our projects with a set of libraries as listed below:
28 |
29 | - [Redux](https://github.com/reduxjs/redux)
30 | - [React Redux](https://github.com/reduxjs/react-redux)
31 | - [ImmutableJS](https://github.com/facebook/immutable-js)
32 | - [Normalizr](https://github.com/paularmstrong/normalizr)
33 | - [Reselect](https://github.com/reduxjs/reselect)
34 | - [redux-promise-middleware](https://github.com/pburtchaell/redux-promise-middleware)
35 | - [redux-thunk](https://github.com/reduxjs/redux-thunk)
36 |
37 | We needed to make a request, normalize the response, put it into a reducer, listen to the reducer, unnormalize the data came from reducer, show it on the view.
38 |
39 | OMG!!!
40 |
41 | So I created the `react-fetches`.
42 |
43 | **PS: Thank you for all of these libraries that helped us until now to build awesome projects.**
44 |
45 | ## Install
46 |
47 | ```sh
48 | npm install --save fetches react-fetches
49 | ```
50 |
51 | ## Basic Example
52 |
53 | **app.js**
54 | ```es6
55 | import React from 'react'
56 | import { render } from 'react-dom'
57 | import { createClient } from 'fetches'
58 | import { Provider } from 'react-fetches'
59 |
60 | import View from './view'
61 |
62 | const client = createClient('https://your-api.com/api/v1/')
63 |
64 | const Root = () => (
65 |
66 |
67 |
68 | )
69 |
70 | render(, document.getElementById('root'))
71 | ```
72 |
73 | **view.js**
74 | ```es6
75 | import React, { Component, Fragment } from 'react'
76 | import { connect } from 'react-fetches'
77 |
78 | const mapRequestsToProps = (http, map) => ({
79 | userID: map(http.get('user'), (user) => user.id),
80 | groups: http.get('groups'),
81 | })
82 |
83 | const mapDispatchToProps = (http, dispatch) => ({
84 | addGroup: dispatch(http.post('group'))
85 | })
86 |
87 | class View extends Component {
88 |
89 | constructor(props) {
90 | super(props)
91 | this.state = {
92 | addedGroups: [],
93 | }
94 | }
95 |
96 | addGroup() {
97 | this.props.addGroup({ name: 'Name of Group' }).then(({data}) => {
98 | this.setState((prevState) => ({
99 | addedGroups: [...prevState.addedGroupd, data]
100 | }))
101 | })
102 | }
103 |
104 | render() {
105 | if (this.props.loading) {
106 | return 'Loading...'
107 | }
108 |
109 | const groups = [...this.props.groups, ...this.state.addedGroups]
110 |
111 | return (
112 |
113 |
114 | {groups.map((group) => (
115 | - {group.name}
116 | ))}
117 |
118 |
119 |
120 | )
121 | }
122 |
123 | }
124 |
125 | export default connect(mapRequestsToProps, mapDispatchToProps)(View)
126 | ```
127 |
128 | ## API Reference
129 |
130 | ### connect(mapRequestToProps, mapDispatchToProps)(Component)
131 |
132 | Adds some props, came from mapRequestToProps and mapDispatchToProps, into your component.
133 |
134 | - **mapRequestToProps** props
135 | - **loading** - Boolean - *default: false* - identifies if a request is performing.
136 | - **errors** - Object - *default: undefined* - identifies if occurred some error in requests.
137 | - **responses** - Object - *default: undefined* - Response object from each one request.
138 | - **\** - Object - *default: undefined* - the response body of request.
139 |
140 | **Example:**
141 | ```es6
142 | const props = {
143 | loading: false,
144 | errors: {
145 | exampleRequest: {
146 | name: 'name is not defined',
147 | },
148 | }
149 | responses: {
150 | exampleRequest: Response,
151 | },
152 | exampleRequest: null,
153 | }
154 | ```
155 |
156 | - **mapDispatchToProps** props
157 | - **\** - Function - the function to make your request, this function returns for you a promise.
158 | - This function can be called with two parameters **data** and **map**
159 |
160 | **Example:**
161 | ```es6
162 | const props = {
163 | exampleFunction: Function => Promise,
164 | }
165 |
166 | const data = { name: 'param name' }
167 | exampleFunction(data, (response) => response.id)
168 | ```
169 |
170 | ### mapRequestToProps = (http, map) => Object
171 |
172 | Should be a function that receive two arguments **http** and **map**, and should return an object with the props.
173 |
174 | ```es6
175 | const mapRequestsToProps = (http, map) => ({
176 | groups: http.get('groups'),
177 | userID: map(http.get('user'), (user) => user.id),
178 | })
179 | ```
180 |
181 | - **http** - an object with the HTTP methods as a function.
182 | - **get(uri, [params, [options]])**
183 | - **uri** - String, Array\ - the complement of your main URI.
184 | - **params** - Object - *optional* - URL query params.
185 | - **options** - Object - *optional* - The same custom settings accepted by [fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Syntax)
186 |
187 | - **post(uri, [params, [options]])**
188 | - **put(uri, [params, [options]])**
189 | - **patch(uri, [params, [options]])**
190 | - **delete(uri, [params, [options]])**
191 | - **uri** - String, Array\ - the complement of your main URI.
192 | - **data** - Object - *optional* - The body of request.
193 | - **options** - Object - *optional* - The same custom settings accepted by [fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Syntax)
194 |
195 | - **map(request, (response) => mappedObject)** - a function that permits you to map your responses as well as you want.
196 |
197 |
198 | ### mapDispatchToProps = (http, dispatch) => Object
199 |
200 | Should be a function that receive two arguments **http** and **dispatch**, and should return an object with the props.
201 |
202 | **NOTE:** Here the **http** is a bit different of the **mapRequestToProps http**.
203 |
204 | ```es6
205 | const mapDispatchToProps = (http, dispatch) => ({
206 | addGroup: dispatch(http.post('group'))
207 | })
208 | ```
209 |
210 | - **http** - an object with the HTTP methods as a function.
211 | - **get(uri)**
212 | - **post(uri)**
213 | - **put(uri)**
214 | - **patch(uri)**
215 | - **delete(uri)**
216 | - **uri** - String, Array\ - the complement of your main URI.
217 |
218 | - **dispatch(request, config)** - in mapDispatchToProps, each item always must call the dispatch function.
219 | - **request** - the **http** request
220 | - **config** - Object - *optional* - The same custom settings accepted by [fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Syntax)
221 |
222 | **NOTE:** The function returned as a prop can receive two parameters **data** and **map**
223 | - **data** - Object - *optional* - The query params for the get method and body of the request for the others.
224 | - **map = (response) => mappedObject** - a function that permits you to map your responses as well as you want.
225 |
226 | ## Inspirations
227 |
228 | - [React Apollo](https://github.com/apollographql/react-apollo) - to make the responses and dispatches as props.
229 | - [React Redux](https://github.com/reduxjs/react-redux) - to name our functions.
230 |
231 |
232 | ## Support
233 |
234 | React 16+
235 |
236 | Fetches is based on [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), which the most of modern browsers already are compatible, but if you need to be compatible with an older browser, you may use this [polyfill](https://github.com/github/fetch)
237 |
238 | ## How to Contribute
239 |
240 | 1. Fork it!
241 | 1. Create your feature branch: `git checkout -b my-new-feature`
242 | 1. Commit your changes: `git commit -m 'Add some feature'`
243 | 1. Push to the branch: `git push origin my-new-feature`
244 | 1. Submit a pull request :)
245 |
246 | ## License
247 |
248 | MIT License
249 |
250 | Copyright (c) 2018 Daniel Leite de Oliveira
251 |
252 | Permission is hereby granted, free of charge, to any person obtaining a copy
253 | of this software and associated documentation files (the "Software"), to deal
254 | in the Software without restriction, including without limitation the rights
255 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
256 | copies of the Software, and to permit persons to whom the Software is
257 | furnished to do so, subject to the following conditions:
258 |
259 | The above copyright notice and this permission notice shall be included in all
260 | copies or substantial portions of the Software.
261 |
262 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
263 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
264 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
265 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
266 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
267 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
268 | SOFTWARE.
269 |
270 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | parsers:
2 | javascript:
3 | enable_partials: yes
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-fetches",
3 | "version": "1.0.3",
4 | "description": "A new way to perform fetch requests with react.",
5 | "main": "./lib/module.js",
6 | "scripts": {
7 | "test": "BABEL_ENV=test jest --config .jestrc.json",
8 | "build": "NODE_ENV=rollup rollup -c",
9 | "eslint": "eslint src tests",
10 | "prepublish": "npm run build"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/dleitee/fetches.git"
15 | },
16 | "license": "ISC",
17 | "bugs": {
18 | "url": "https://github.com/dleitee/fetches/issues"
19 | },
20 | "homepage": "https://github.com/dleitee/fetches#readme",
21 | "author": "Daniel Leite de Oliveira (https://github.com/dleitee)",
22 | "dependencies": {
23 | "fetches": "^0.2.0",
24 | "lodash.frompairs": "^4.0.1",
25 | "p-is-promise": "^1.1.0",
26 | "prop-types": "^15.6.1",
27 | "react": "16.5.1",
28 | "react-dom": "^16.4.0"
29 | },
30 | "devDependencies": {
31 | "babel-core": "^6.26.3",
32 | "babel-eslint": "^9.0.0",
33 | "babel-jest": "^23.0.1",
34 | "babel-plugin-external-helpers": "^6.22.0",
35 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
36 | "babel-preset-env": "^1.7.0",
37 | "babel-preset-react": "^6.24.1",
38 | "dom-testing-library": "^3.0.0",
39 | "eslint": "^5.0.0",
40 | "eslint-config-airbnb": "^17.0.0",
41 | "eslint-config-cheesecakelabs": "^2.0.3",
42 | "eslint-plugin-import": "^2.12.0",
43 | "eslint-plugin-jsx-a11y": "^6.0.3",
44 | "eslint-plugin-react": "^7.9.1",
45 | "form-data": "^2.3.2",
46 | "jest": "^23.1.0",
47 | "lodash.get": "^4.4.2",
48 | "nock": "^10.0.0",
49 | "node-fetch": "^2.1.2",
50 | "react-testing-library": "5.0.1",
51 | "rollup": "^0.66.0",
52 | "rollup-plugin-babel": "^3.0.4",
53 | "rollup-plugin-commonjs": "^9.1.3",
54 | "rollup-plugin-node-resolve": "^3.3.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve'
2 | import commonjs from 'rollup-plugin-commonjs'
3 | import babel from 'rollup-plugin-babel'
4 |
5 | export default {
6 | input: 'src/index.js',
7 | output: {
8 | file: 'lib/module.js',
9 | format: 'cjs',
10 | },
11 | plugins: [
12 | babel({ exclude: 'node_modules/**' }),
13 | resolve({
14 | modulesOnly: true,
15 | jsnext: true,
16 | customResolveOptions: {
17 | moduleDirectory: 'node_modules',
18 | },
19 | }),
20 | commonjs(),
21 | ],
22 | external: ['fetches', 'lodash.frompairs', 'p-is-promise', 'prop-types', 'react', 'react-dom'],
23 | }
24 |
--------------------------------------------------------------------------------
/src/connect.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Client } from 'fetches'
4 |
5 | import { FetchesContext } from './context'
6 | import { makeDispatches } from './map-dispatch-to-props'
7 | import { makeResponses, makeRequests } from './map-request-to-props'
8 |
9 | const connect = (mapRequestsToProps, mapDispatchToProps) => WrappedComponent => {
10 | if (!mapRequestsToProps && !mapDispatchToProps) {
11 | return WrappedComponent
12 | }
13 | class Wrapper extends React.Component {
14 | constructor(props) {
15 | super(props)
16 | this.state = {
17 | loading: !!mapRequestsToProps,
18 | dispatching: false,
19 | ...this.getDispatchAsProps(),
20 | }
21 | }
22 | componentDidMount() {
23 | if (!this.props.client) {
24 | return
25 | }
26 | this.getResponseAsProps()
27 | }
28 |
29 | getDispatchAsProps() {
30 | if (mapDispatchToProps) {
31 | return makeDispatches(this.props.client)(mapDispatchToProps, this.props)
32 | }
33 | return {}
34 | }
35 |
36 | getResponseAsProps() {
37 | if (mapRequestsToProps) {
38 | makeRequests(this.props.client, props => {
39 | const responses = makeResponses(props)
40 |
41 | this.setState(() => ({
42 | loading: false,
43 | ...responses.body,
44 | errors: responses.errors,
45 | responses: responses.responses,
46 | }))
47 | })(mapRequestsToProps, this.props)
48 | }
49 | }
50 |
51 | render() {
52 | return React.createElement(WrappedComponent, Object.assign({}, this.props, this.state))
53 | }
54 | }
55 |
56 | Wrapper.propTypes = {
57 | client: PropTypes.instanceOf(Client).isRequired,
58 | }
59 |
60 | const FecthesComponent = props => (
61 |
62 | {client => }
63 |
64 | )
65 | return FecthesComponent
66 | }
67 |
68 | export { connect }
69 |
--------------------------------------------------------------------------------
/src/context.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export const FetchesContext = React.createContext(null)
4 |
--------------------------------------------------------------------------------
/src/handler.js:
--------------------------------------------------------------------------------
1 | const defaultParser = data => data
2 |
3 | const contentTypeIsJSON = header => header && header.includes('application/json')
4 |
5 | const handler = (request, parser = defaultParser) => index =>
6 | new Promise(resolve => {
7 | request
8 | .then(response => {
9 | const clone = response.clone()
10 | if (contentTypeIsJSON(response.headers.get('content-type'))) {
11 | return response.json().then(data => Promise.resolve({ data, response: clone }))
12 | }
13 | return response.text().then(data => Promise.resolve({ data, response: clone }))
14 | })
15 | .then(({ data, response } = {}) => {
16 | const hasError = 399 % response.status === 399
17 | resolve([
18 | index,
19 | {
20 | error: hasError ? data : false,
21 | response,
22 | data: !hasError ? parser(data) : null,
23 | },
24 | ])
25 | })
26 | .catch(error =>
27 | resolve([
28 | index,
29 | {
30 | error,
31 | response: null,
32 | data: null,
33 | },
34 | ])
35 | )
36 | })
37 |
38 | export default handler
39 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export * from './provider'
2 | export * from './connect'
3 |
--------------------------------------------------------------------------------
/src/map-dispatch-to-props.js:
--------------------------------------------------------------------------------
1 | import { getHTTPMethods } from 'fetches'
2 |
3 | import handler from './handler'
4 |
5 | const dispatchHandler = (request, config) => (data = {}, parser) => {
6 | const completeRequest = request.bind(null, data, config)
7 | return handler(completeRequest(), parser)('default').then(args => args[1])
8 | }
9 |
10 | const makeDispatches = client => (mapDispatchToProps, currentProps) => {
11 | const http = getHTTPMethods(client)
12 | const keys = Object.keys(http)
13 |
14 | const httpMethods = keys.reduce(
15 | (previous, current) => ({
16 | ...previous,
17 | [current]: uri => http[current].bind(null, uri),
18 | }),
19 | {}
20 | )
21 |
22 | return mapDispatchToProps(httpMethods, dispatchHandler, currentProps)
23 | }
24 |
25 | export { makeDispatches }
26 |
--------------------------------------------------------------------------------
/src/map-request-to-props.js:
--------------------------------------------------------------------------------
1 | import isPromise from 'p-is-promise'
2 | import { getHTTPMethods } from 'fetches'
3 | import _fromPairs from 'lodash.frompairs'
4 |
5 | import handler from './handler'
6 |
7 | const getPromiseFromKey = (values, key) =>
8 | isPromise(values[key]) ? handler(values[key])(key) : values[key](key)
9 |
10 | const asyncFunction = (requests, cb) => {
11 | const keys = Object.keys(requests)
12 | Promise.all(keys.map(getPromiseFromKey.bind(null, requests))).then(args => cb(_fromPairs(args)))
13 | }
14 |
15 | const makeRequests = (client, cb) => (mapRequestsToProps, currentProps) => {
16 | const http = getHTTPMethods(client)
17 | const requests = mapRequestsToProps(http, handler, currentProps)
18 | asyncFunction(requests, cb)
19 | }
20 |
21 | const makeResponses = data => {
22 | const keys = Object.keys(data)
23 | return keys.reduce((previous, current) => {
24 | const body = {
25 | ...previous.body,
26 | [current]: data[current].data,
27 | }
28 | const errors = {
29 | ...previous.errors,
30 | [current]: data[current].error,
31 | }
32 | const responses = {
33 | ...previous.responses,
34 | [current]: data[current].response && data[current].response.clone(),
35 | }
36 | return {
37 | body,
38 | errors,
39 | responses,
40 | }
41 | }, {})
42 | }
43 |
44 | export { makeResponses, makeRequests }
45 |
--------------------------------------------------------------------------------
/src/provider.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Client } from 'fetches'
4 |
5 | import { FetchesContext } from './context'
6 |
7 | const Provider = props => (
8 |
9 | {React.Children.only(props.children)}
10 |
11 | )
12 |
13 | Provider.propTypes = {
14 | client: PropTypes.instanceOf(Client).isRequired,
15 | children: PropTypes.element.isRequired,
16 | }
17 |
18 | export { Provider }
19 |
--------------------------------------------------------------------------------
/tests/config.test.js:
--------------------------------------------------------------------------------
1 | import nodeFetch from 'node-fetch'
2 | import formData from 'form-data'
3 |
4 | global.fetch = nodeFetch
5 | global.FormData = formData
6 | global.document = {}
7 |
--------------------------------------------------------------------------------
/tests/connect.test.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { connect } from '../src/connect'
5 |
6 | describe('Connect function', () => {
7 | test('should return only ConnectedComponent', () => {
8 | const SimpleComponent = props => (
9 | {props.loading ? Loading : Loaded}
10 | )
11 |
12 | SimpleComponent.propTypes = {
13 | loading: PropTypes.bool.isRequired,
14 | }
15 |
16 | const ConnectedComponent = connect()(SimpleComponent)
17 | expect(ConnectedComponent).toBe(SimpleComponent)
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/tests/errors-on-map-requests-to-props.test.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { render, wait, cleanup } from 'react-testing-library'
4 | import nock from 'nock'
5 | import { createClient } from 'fetches'
6 |
7 | import { Provider } from '../src/provider'
8 | import { connect } from '../src/connect'
9 |
10 | const EXAMPLE_URI = 'http://example.com/api/v1/'
11 |
12 | const client = createClient(EXAMPLE_URI)
13 |
14 | const View = props => {props.children}
15 |
16 | View.propTypes = {
17 | children: PropTypes.node.isRequired,
18 | }
19 |
20 | describe('connect with mapRequestsToProps and Errors', () => {
21 | afterAll(() => {
22 | nock.cleanAll()
23 | })
24 |
25 | afterEach(cleanup)
26 |
27 | test('should return errors with the named key', async () => {
28 | nock(EXAMPLE_URI)
29 | .get('/name/')
30 | .delay(500)
31 | .replyWithError('something awful happened')
32 | const renderized = jest.fn()
33 | const SimpleComponent = props => {
34 | renderized(props)
35 | return {props.loading ? Loading : Loaded}
36 | }
37 |
38 | SimpleComponent.propTypes = {
39 | loading: PropTypes.bool.isRequired,
40 | }
41 |
42 | const mapRequestsToProps = (http, parser) => ({
43 | name: parser(http.get('name'), item => item.body),
44 | })
45 |
46 | const ConnectedComponent = connect(mapRequestsToProps)(SimpleComponent)
47 | const { getByText } = render(
48 |
49 |
50 |
51 | )
52 | await wait(() => getByText('Loading'))
53 | await wait(() => getByText('Loaded'))
54 | expect(renderized).toHaveBeenCalledTimes(2)
55 | const props = renderized.mock.calls[1][0]
56 | expect(props.prop1).toBe('prop1')
57 | expect(props.errors.name).not.toBeFalsy()
58 | })
59 | test('should return errors with the returned error into the named key', async () => {
60 | nock(EXAMPLE_URI)
61 | .get('/name/')
62 | .delay(500)
63 | .reply(400, () => ({
64 | name: 'Name is required',
65 | }))
66 | const renderized = jest.fn()
67 | const SimpleComponent = props => {
68 | renderized(props)
69 | return {props.loading ? Loading : Loaded}
70 | }
71 |
72 | SimpleComponent.propTypes = {
73 | loading: PropTypes.bool.isRequired,
74 | }
75 |
76 | const mapRequestsToProps = (http, parser) => ({
77 | name: parser(http.get('name'), item => item.body),
78 | })
79 |
80 | const ConnectedComponent = connect(mapRequestsToProps)(SimpleComponent)
81 | const { getByText } = render(
82 |
83 |
84 |
85 | )
86 | await wait(() => getByText('Loading'))
87 | await wait(() => getByText('Loaded'))
88 | expect(renderized).toHaveBeenCalledTimes(2)
89 | const props = renderized.mock.calls[1][0]
90 | expect(props.prop1).toBe('prop1')
91 | expect(props.errors.name).not.toBeFalsy()
92 | expect(props.errors.name.name).toBe('Name is required')
93 | })
94 | })
95 |
--------------------------------------------------------------------------------
/tests/library.test.js:
--------------------------------------------------------------------------------
1 | import { Provider, connect } from '../src/'
2 |
3 | describe('Library exports', () => {
4 | test('should export Provider and connect', () => {
5 | expect(Provider).toBeDefined()
6 | expect(connect).toBeDefined()
7 | })
8 | })
9 |
--------------------------------------------------------------------------------
/tests/map-dispatch-to-props.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { render, cleanup } from 'react-testing-library'
4 | import nock from 'nock'
5 | import { createClient } from 'fetches'
6 |
7 | import { Provider } from '../src/provider'
8 | import { connect } from '../src/connect'
9 |
10 | const EXAMPLE_URI = 'http://example.com/api/v1/'
11 |
12 | const client = createClient(EXAMPLE_URI)
13 |
14 | const View = props => {props.children}
15 |
16 | View.propTypes = {
17 | children: PropTypes.node.isRequired,
18 | }
19 |
20 | describe('connect with mapDispatchToProps', () => {
21 | let renderized
22 | let SimpleComponent
23 | beforeEach(() => {
24 | renderized = jest.fn()
25 | SimpleComponent = props => {
26 | renderized(props)
27 | return Hello
28 | }
29 |
30 | const mapDispatchToProps = (http, dispatch) => ({
31 | save: dispatch(http.get('name')),
32 | })
33 |
34 | const ConnectedComponent = connect(null, mapDispatchToProps)(SimpleComponent)
35 | render(
36 |
37 |
38 |
39 | )
40 | })
41 |
42 | afterAll(() => {
43 | nock.cleanAll()
44 | })
45 |
46 | afterEach(cleanup)
47 |
48 | test('the render function should be called only one time', () => {
49 | expect(renderized).toHaveBeenCalledTimes(1)
50 | })
51 |
52 | test('the render function should receive the props declared below', () => {
53 | expect(renderized).toBeCalledWith(
54 | expect.objectContaining({
55 | save: expect.any(Function),
56 | dispatching: expect.any(Boolean),
57 | })
58 | )
59 | })
60 |
61 | test('the function should return a promise', async () => {
62 | nock(EXAMPLE_URI)
63 | .get('/name/')
64 | .delay(500)
65 | .reply(200, () => ({ body: 'success' }))
66 | const props = renderized.mock.calls[0][0]
67 | const response = await props.save()
68 | expect(response.error).toBeFalsy()
69 | expect(response.data.body).toBe('success')
70 | })
71 |
72 | test('the function should be able to receive params', async () => {
73 | nock(EXAMPLE_URI)
74 | .get('/name?first=a')
75 | .delay(500)
76 | .reply(200, () => ({ body: 'success' }))
77 | const props = renderized.mock.calls[0][0]
78 | const response = await props.save({ first: 'a' })
79 | expect(response.error).toBeFalsy()
80 | expect(response.data.body).toBe('success')
81 | })
82 |
83 | test('the function should be able to map the response', async () => {
84 | nock(EXAMPLE_URI)
85 | .get('/name?first=a')
86 | .delay(500)
87 | .reply(200, () => ({ body: 'success' }))
88 | const props = renderized.mock.calls[0][0]
89 | const response = await props.save({ first: 'a' }, value => value.body)
90 | expect(response.error).toBeFalsy()
91 | expect(response.data).toBe('success')
92 | })
93 | })
94 |
--------------------------------------------------------------------------------
/tests/map-requests-to-props.test.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { render, wait, cleanup } from 'react-testing-library'
4 | import nock from 'nock'
5 | import { createClient } from 'fetches'
6 |
7 | import { Provider } from '../src/provider'
8 | import { connect } from '../src/connect'
9 |
10 | const EXAMPLE_URI = 'http://example.com/api/v1/'
11 |
12 | const client = createClient(EXAMPLE_URI)
13 |
14 | const View = props => {props.children}
15 |
16 | View.propTypes = {
17 | children: PropTypes.node.isRequired,
18 | }
19 |
20 | describe('connect with mapRequestsToProps', () => {
21 | beforeEach(() => {
22 | nock(EXAMPLE_URI)
23 | .get('/name/')
24 | .delay(500)
25 | .reply(200, () => ({ body: 'success' }))
26 | })
27 | afterEach(cleanup)
28 | afterAll(() => {
29 | nock.cleanAll()
30 | })
31 | test('must add a prop called loading with the request status', async () => {
32 | const SimpleComponent = props => (
33 | {props.loading ? Loading : Loaded}
34 | )
35 |
36 | SimpleComponent.propTypes = {
37 | loading: PropTypes.bool.isRequired,
38 | }
39 |
40 | const mapRequestsToProps = http => ({
41 | name: http.get('name'),
42 | })
43 |
44 | const ConnectedComponent = connect(mapRequestsToProps)(SimpleComponent)
45 | const { getByText } = render(
46 |
47 |
48 |
49 | )
50 | await wait(() => getByText('Loading'))
51 | await wait(() => getByText('Loaded'))
52 | })
53 | test('should return a map with { name, errors, responses}', async () => {
54 | const renderized = jest.fn()
55 | const SimpleComponent = props => {
56 | renderized(props)
57 | return {props.loading ? Loading : Loaded}
58 | }
59 |
60 | SimpleComponent.propTypes = {
61 | loading: PropTypes.bool.isRequired,
62 | }
63 |
64 | SimpleComponent.defaultProps = {
65 | name: {},
66 | errors: {},
67 | responses: {},
68 | }
69 |
70 | const mapRequestsToProps = http => ({
71 | name: http.get('name'),
72 | })
73 |
74 | const ConnectedComponent = connect(mapRequestsToProps)(SimpleComponent)
75 | const { getByText } = render(
76 |
77 |
78 |
79 | )
80 | await wait(() => getByText('Loading'))
81 | await wait(() => getByText('Loaded'))
82 |
83 | expect(renderized).toHaveBeenCalledTimes(2)
84 | expect(renderized).toBeCalledWith(
85 | expect.objectContaining({
86 | loading: expect.any(Boolean),
87 | name: expect.any(Object),
88 | errors: expect.any(Object),
89 | responses: expect.any(Object),
90 | })
91 | )
92 | })
93 | test('should return the parsed string', async () => {
94 | const SimpleComponent = props => {
95 | if (!props.loading) {
96 | expect(props.name).toBe('success')
97 | }
98 |
99 | return {props.loading ? Loading : Loaded}
100 | }
101 |
102 | SimpleComponent.propTypes = {
103 | loading: PropTypes.bool.isRequired,
104 | name: PropTypes.string,
105 | }
106 |
107 | SimpleComponent.defaultProps = {
108 | name: undefined,
109 | }
110 |
111 | const mapRequestsToProps = (http, parser) => ({
112 | name: parser(http.get('name'), item => item.body),
113 | })
114 |
115 | const ConnectedComponent = connect(mapRequestsToProps)(SimpleComponent)
116 | const { getByText } = render(
117 |
118 |
119 |
120 | )
121 | await wait(() => getByText('Loading'))
122 | await wait(() => getByText('Loaded'))
123 | })
124 | test('should call the render function only twice', async () => {
125 | const renderized = jest.fn()
126 | const SimpleComponent = props => {
127 | renderized()
128 | return {props.loading ? Loading : Loaded}
129 | }
130 |
131 | SimpleComponent.propTypes = {
132 | loading: PropTypes.bool.isRequired,
133 | }
134 |
135 | const mapRequestsToProps = (http, parser) => ({
136 | name: parser(http.get('name'), item => item.body),
137 | })
138 |
139 | const ConnectedComponent = connect(mapRequestsToProps)(SimpleComponent)
140 | const { getByText } = render(
141 |
142 |
143 |
144 | )
145 | await wait(() => getByText('Loading'))
146 | await wait(() => getByText('Loaded'))
147 | expect(renderized).toHaveBeenCalledTimes(2)
148 | })
149 | test('should be able to return two or more props', async () => {
150 | nock(EXAMPLE_URI)
151 | .get('/first-name/')
152 | .delay(500)
153 | .reply(200, () => ({ body: 'success' }))
154 | nock(EXAMPLE_URI)
155 | .get('/last-name/')
156 | .delay(500)
157 | .reply(200, () => ({ body: 'success' }))
158 | const renderized = jest.fn()
159 | const SimpleComponent = props => {
160 | renderized(props)
161 | return {props.loading ? Loading : Loaded}
162 | }
163 |
164 | SimpleComponent.propTypes = {
165 | loading: PropTypes.bool.isRequired,
166 | }
167 |
168 | const mapRequestsToProps = (http, parser) => ({
169 | name: parser(http.get('name'), item => item.body),
170 | last_name: parser(http.get('last-name'), item => item.body),
171 | first_name: parser(http.get('first-name'), item => item.body),
172 | })
173 |
174 | const ConnectedComponent = connect(mapRequestsToProps)(SimpleComponent)
175 | const { getByText } = render(
176 |
177 |
178 |
179 | )
180 | await wait(() => getByText('Loading'))
181 | await wait(() => getByText('Loaded'))
182 | expect(renderized).toHaveBeenCalledTimes(2)
183 | const props = renderized.mock.calls[1][0]
184 | expect(props.name).toBe('success')
185 | expect(props.last_name).toBe('success')
186 | expect(props.first_name).toBe('success')
187 | })
188 | test('should maintain the component props', async () => {
189 | const renderized = jest.fn()
190 | const SimpleComponent = props => {
191 | renderized(props)
192 | return {props.loading ? Loading : Loaded}
193 | }
194 |
195 | SimpleComponent.propTypes = {
196 | loading: PropTypes.bool.isRequired,
197 | }
198 |
199 | const mapRequestsToProps = (http, parser) => ({
200 | name: parser(http.get('name'), item => item.body),
201 | })
202 |
203 | const ConnectedComponent = connect(mapRequestsToProps)(SimpleComponent)
204 | const { getByText } = render(
205 |
206 |
207 |
208 | )
209 | await wait(() => getByText('Loading'))
210 | await wait(() => getByText('Loaded'))
211 | expect(renderized).toHaveBeenCalledTimes(2)
212 | const props = renderized.mock.calls[1][0]
213 | expect(props.prop1).toBe('prop1')
214 | expect(props.name).toBe('success')
215 | })
216 | test('should return the response using props on request', async () => {
217 | nock(EXAMPLE_URI)
218 | .get('/token?user=1')
219 | .delay(500)
220 | .reply(200, () => ({ body: 'success' }))
221 | const renderized = jest.fn()
222 | const SimpleComponent = props => {
223 | renderized(props)
224 | return {props.loading ? Loading : Loaded}
225 | }
226 |
227 | SimpleComponent.propTypes = {
228 | loading: PropTypes.bool.isRequired,
229 | }
230 |
231 | const mapRequestsToProps = (http, parser, currentProps) => ({
232 | name: parser(http.get('name'), item => item.body),
233 | token: parser(http.get('token', { user: currentProps.userId }), item => item.body),
234 | })
235 |
236 | const ConnectedComponent = connect(mapRequestsToProps)(SimpleComponent)
237 | const { getByText } = render(
238 |
239 |
240 |
241 | )
242 | await wait(() => getByText('Loading'))
243 | await wait(() => getByText('Loaded'))
244 | expect(renderized).toHaveBeenCalledTimes(2)
245 | const props = renderized.mock.calls[1][0]
246 | expect(props.token).toBe('success')
247 | expect(props.name).toBe('success')
248 | })
249 | })
250 |
--------------------------------------------------------------------------------