Skip to content

Commit

Permalink
Merge pull request #28 from sheaivey/withAxios
Browse files Browse the repository at this point in the history
@achepukov thanks for your initial work and input for a more robust `withAxios(options)(ComponentToBeWrapped)` HoC. I have gone ahead and published the react-axios `v2.0.2` with the changes from this branch.
  • Loading branch information
sheaivey authored Nov 23, 2018
2 parents aaf9ab8 + 1f130e0 commit f1ed8d0
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 34 deletions.
1 change: 1 addition & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"presets" : [
"./tools/babelPreset",
"stage-2",
"react"
],
"env": {
Expand Down
45 changes: 37 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ This is intended to allow in render async requests.

- Same great features found in [Axios](https://github.com/mzabriskie/axios)
- Component driven
- Child function callback ***(error, response, isLoading, onReload) => { }***
- Child function callback ***(error, response, isLoading, onReload, axios) => { }***
- Auto cancel previous requests
- Debounce to prevent rapid calls.
- Request only invoked on prop change and *isReady* state.
- Callback props for ***onSuccess***, ***onError***, and ***onLoading***
- Supports custom axios instances through ***props*** or a ***<AxiosProvider ... >***
- Create your own request components wrapped using the withAxios({options})(ComponentToBeWrapped) HoC

## Installing

Expand Down Expand Up @@ -80,7 +81,7 @@ render() {
return (
<div>
<Get url="/api/user" params={{id: "12345"}}>
{(error, response, isLoading, onReload) => {
{(error, response, isLoading, onReload, axios) => {
if(error) {
return (<div>Something bad happened: {error.message} <button onClick={() => onReload({ params: { reload: true } })}>Retry</button></div>)
}
Expand All @@ -107,6 +108,8 @@ render() {

`onReload(props)` Function to invoke another XHR request. This function accepts new temporary props that will be overloaded with the existing props for this request only.

`axios` current instance of axios being used.


## Custom Axios Instance

Expand All @@ -124,7 +127,7 @@ Pass down through a provider
```jsx
<AxiosProvider instance={axiosInstance}>
<Get url="test">
{(error, response, isLoading, onReload) => {
{(error, response, isLoading, onReload, axios) => {
...
}}
</Get>
Expand All @@ -134,7 +137,7 @@ Pass down through a provider
Or pass down through props
```jsx
<Get url="test" instance={axiosInstance}>
{(error, response, isLoading, onReload) => {
{(error, response, isLoading, onReload, axios) => {
...
}}
</Get>
Expand All @@ -143,7 +146,15 @@ Or pass down through props
Retrieve from custom provider (when you need to directly use axios).
The default instance will be passed if not inside an `<AxiosProvider/>`.
```jsx
const MyComponent = withAxios(class MyComponentImpl extends React.Component {
<AxiosProvider instance={axiosInstance}>
<MyComponent/>
</AxiosProvider>
```

## withAxios(Component) HoC
If you need basic access to the axios instance but don't need anything else you can wrap a component using withAxios() higher order component.
```jsx
const MyComponent = withAxios(class MyComponentRaw extends React.Component {
componentWillMount() {
this.props.axios('test').then(result => {
this.setState({ data: result.data })
Expand All @@ -154,8 +165,26 @@ const MyComponent = withAxios(class MyComponentImpl extends React.Component {
return <div>{JSON.stringify(data)}</div>
}
})
```

<AxiosProvider instance={axiosInstance}>
<MyComponent/>
</AxiosProvider>
## withAxios(options)(Component) HoC
If you want to create your own component with the full react-axios request `options`. You can override the initial options supplied to withAxios at any time by passing `options` prop to your wrapped component. See below on how this works.

```jsx
const MyComponent = withAxios({
url: '/api/user'
params: {id: "12345"}
})(class MyComponentRaw extends React.Component {
render() {
const {error, response, isLoading, onReload, axios} = this.props
if(error) {
return (<div>Something bad happened: {error.message}</div>)
} else if(isLoading) {
return (<div className="loader"></div>)
} else if(response !== null) {
return (<div>{response.data.message}</div>)
}
return null
}
})
```
66 changes: 62 additions & 4 deletions __tests__/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ describe('utils', () => {
test('debounce test', () => {
return debounceTest.then((res)=> {
expect(res).toBe(1)
})
}, (err) => {})
})
test('debounce test immediate', () => {
return debounceTestImmediate.then((res)=> {
expect(res).toBe(2)
})
}, (err) => {})
})
})
})
Expand Down Expand Up @@ -183,12 +183,12 @@ describe('components', () => {
})
})

describe('#withAxios', () => {
describe('#withAxios(component) basic HoC', () => {
const Component = withAxios(props => {
props.onRendered(props.axios)
return <div/>
})
test('provides default instance', () => {
test('provides default axios instance', () => {
let seenAxios
renderer.create(
<Component onRendered={passedAxios => {
Expand All @@ -210,4 +210,62 @@ describe('components', () => {
expect(seenAxios).toBe(axiosInstance)
})
})

describe('#withAxios(options)(component) request HoC', () => {
const Component = withAxios({ method: 'get' })(props => {
props.onRendered(props)
return <div/>
})
test('provides default axios instance', () => {
let seenAxios
renderer.create(
<Component onRendered={props => {
seenAxios = props.axios
}}/>
)
expect(typeof seenAxios).toBe('function')
})
test('respects AxiosProvider', () => {
const axiosInstance = axios.create()
let seenAxios
renderer.create(
<AxiosProvider instance={axiosInstance}>
<Component onRendered={props => {
seenAxios = props.axios
}}/>
</AxiosProvider>
)
expect(seenAxios).toBe(axiosInstance)
})
test('passes custom props', () => {
let customProp
renderer.create(
<Component customProp="Hello World!" onRendered={props => {
customProp = props.customProp
}}/>
)
expect(customProp).toBe('Hello World!')
})
test('overload options', () => {
let props
renderer.create(
<Component options={{ method: 'post' }} onRendered={p => {
props = p
}}/>
)
expect(props.options.method).toBe('post')
})
test('passes down Request child function props', () => {
let props
renderer.create(
<Component onRendered={p => {
props = p
}}/>
)
expect(props.error).toBe(null)
expect(props.response).toBe(null)
expect(props.isLoading).toBe(false)
expect(typeof props.onReload).toBe('function')
})
})
})
4 changes: 4 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ <h1>React Axios Advanced Examples</h1>
<li><a href="/provider"><b>AxiosProvider</b></a>
<code>Create axios instance and pass down through prop or a provider</code>
</li>
<li><a href="/withAxios"><b>withAxios() HoC</b></a>
<code>Use the HoC withAxios to wrap existing components with react-axios and supplied .
export default withAxios({options})(ComponentToBeWrapped)</code>
</li>
</ul>
</body>
</html>
49 changes: 49 additions & 0 deletions examples/withAxios/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { withAxios } from 'react-axios'
import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.Component {
render() {
return (
<div>
<code>
<h2>Basic withAxios HoC</h2>
<DemoComponentHoC />

<h2>Custom props withAxios HoC</h2>
<DemoComponentHoC customProp={'Custom Prop'} />

<h2>Overloading withAxios HoC config options</h2>
<DemoComponentHoC options={{ method: 'post', url: '/api/overloaded' }} />

<h2>Children withAxios HoC</h2>
<DemoComponentHoC><div className="success">children</div></DemoComponentHoC>
</code>
</div>
)
}
}
/* eslint react/prop-types: 0 */
class DemoComponent extends React.Component {
render() {
const { error, response, isLoading, children, customProp, onRendered } = this.props
if(error) {
return (<div>Something bad happened: {error.message}</div>)
} else if(isLoading) {
return (<div className="loader"></div>)
} else if(response !== null) {
return (<div>{response.data.message} {customProp} {children}</div>)
}
return customProp || null
}
}

const DemoComponentHoC = withAxios({
method:'get',
url: '/api/request',
})(DemoComponent)

ReactDOM.render(
<App />,
document.getElementById('app')
)
13 changes: 13 additions & 0 deletions examples/withAxios/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<title>Examples</title>
<link href="/style.css" rel="stylesheet"/>
</head>
<body>
<h1 class="breadcrumbs"><a href="/">React Axios Examples</a> / withAxios</h1>
<div id="app"></div>
<script src="/__build__/shared.js"></script>
<script src="/__build__/withAxios.js"></script>
</body>
</html>
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
"dependencies": {},
"peerDependencies": {
"axios": "^0.15.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.0",
"prop-types": "^15.5.0"
"prop-types": "^15.5.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.0"
},
"devDependencies": {
"axios": "^0.17.1",
Expand All @@ -55,13 +55,14 @@
"babel-plugin-add-module-exports": "^0.2.1",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-2": "^6.24.1",
"cross-env": "^3.1.3",
"eslint": "^3.2.0",
"eslint-config-rackt": "^1.1.1",
"eslint-plugin-react": "^6.7.1",
"express": "^4.14.0",
"express-urlrewrite": "^1.2.0",
"jest": "^17.0.3",
"jest": "^23.6.0",
"raf": "^3.4.0",
"react": "^16.1.1",
"react-dom": "^16.1.1",
Expand Down
13 changes: 0 additions & 13 deletions src/components/AxiosProvider.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import axios from 'axios'
import React from 'react'
import PropTypes from 'prop-types'

Expand Down Expand Up @@ -29,15 +28,3 @@ AxiosProvider.propTypes = {
}

export default AxiosProvider

export const withAxios = (WrappedComponent) => {
const AxiosExtracter = (props, context) => {
return <WrappedComponent axios={context.axios || axios} {...props}/>
}

AxiosExtracter.contextTypes = {
axios: PropTypes.func,
}

return AxiosExtracter
}
17 changes: 12 additions & 5 deletions src/components/Request.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class Request extends React.Component {

makeRequest(config) {
const _axios = this.props.instance || this.context.axios || axios
if (!this._mounted) {
if (!this._mounted || !this.props.isReady || this.props.url === undefined) {
return
}
// setup cancel tokens
Expand Down Expand Up @@ -97,7 +97,14 @@ class Request extends React.Component {

render() {
if (typeof this.props.children === 'function') {
return this.props.children(this.state.error, this.state.response, this.state.isLoading, (props) => this.onReload(props))
const _axios = this.props.instance || this.context.axios || axios
return this.props.children(
this.state.error,
this.state.response,
this.state.isLoading,
(props) => this.onReload(props),
_axios
)
}
return null
}
Expand All @@ -108,7 +115,7 @@ Request.contextTypes = {
}

Request.defaultProps = {
url: '',
url: undefined,
method: 'get',
data: {},
config: {},
Expand All @@ -119,8 +126,8 @@ Request.defaultProps = {

Request.propTypes = {
instance: PropTypes.func,
url: PropTypes.string.isRequired,
method: PropTypes.string.isRequired,
url: PropTypes.string,
method: PropTypes.oneOf([ 'get', 'delete', 'head','post','put','patch', 'options' ]).isRequired,
data: PropTypes.object,
params: PropTypes.object,
config: PropTypes.object,
Expand Down
Loading

0 comments on commit f1ed8d0

Please sign in to comment.