I have also added the ability to bind functions to be run when API responses are received. Functions can be bound to error responses, to success responses, and to all responses (both success and error). See methods for this below 'Making Requests' section.
To make requests you can use the standard 'get', 'put', 'post', 'del', and 'patch' methods. If you're server-side you need to set the host because node-fetch can't determine the base route
import simpleIsoFetch from 'simple-iso-fetch';
// absolute routes are needed server-side until Node.js implements native fetch,
// you can set the base URL for server-side via the method below (host, port), or with 'process.env.BASE_URL'
// this "base URL" will be prepended to all requests
simpleIsoFetch.setBaseUrl('http://localhost', 3000);
// example usage for get request to 'http://locahost:3000'
simpleIsoFetch.get({
route: '/'
})
.then(res => {
console.log(res); // => all html returned from 'http://locahost:3000'
});
// identical to the above, convenience default for when no body/customization is needed (just uses string passed as route)
simpleIsoFetch.get('/').then(res => {
console.log(res); // => all html returned from 'http://locahost:3000'
});
// set to your app's hostname + port, (if hostname not provided, defaults to localhost, if hostname provided without port, 80 is assumed, if neither hostname nor port provided, http://localhost: + (process.env.PORT || 3000) used, function returns resulting base URL (note this is a static method, on class itself not instance)
simpleIsoFetch.setBaseUrl('http://localhost', 3000);
// normal usage
const aJsonObject = {
prop: 'example'
}
const exampleParam = 'paramparamparam';
// the below will make a POST request to:
// 'http://localhost:3000/api/paramparamparam?prop=valvalval&prop2=anotherVal'
simpleIsoFetch.post({
route: '/api',
params: exampleParam,
query: {
prop: 'valvalval',
prop2: 'anotherVal'
},
body: aJsonObject
})
.then(res => console.log(res)) // console.logs whatever the response is
.catch(err => console.log(err)); // console.logs whatever the error is
// there is flexibility built in to allow you to provide the route as the first argument and additional options as the second
// the below will make a PUT request to:
// 'http://localhost:3000/api/paramparamparam?prop=valvalval&prop2=anotherVal'
simpleIsoFetch.put('/api', {
params: exampleParam,
query: {
prop: 'valvalval',
prop2: 'anotherVal'
},
body: aJsonObject
})
.then(res => console.log(res)) // console.logs whatever the response is
.catch(err => console.log(err)); // console.logs whatever the error is
// the below will make a DELETE request to:
// 'http://localhost:3000/api/paramparamparam?prop=valvalval&prop2=anotherVal'
// (note that DELETE and GET requests can't have a 'body' property per W3C spec)
simpleIsoFetch.del({
route: '/api',
params: exampleParam,
query: {
prop: 'valvalval',
prop2: 'anotherVal'
}
})
.then(res => console.log(res)) // console.logs whatever the response is
.catch(err => console.log(err)); // console.logs whatever the error is
// full configurable options exposed below
//// dummy body
const blogPost = {
title: 'Hey Guys',
body: 'I\'m o simple!'
}
//// dummy params
const id = '1234';
const location = 'place';
// the below will make a POST request to:
// 'http://localhost:3000/api/posts/1234/place/?anAnalyticsThing={"aDeeplyNestedProperty":"example"}&anotherProperty=example2'
simpleIsoFetch.makeRequest({
// instead of 'makeRequest method + 'method' property you just use simpleFetch.<lowercase method> instead of
// simpleFetch.makeRequest for GET, PUT, and POST, DELETE uses the simpleFetch 'del' method as 'delete'
// is a reserved word. The makeRequest method allows you to specify the method and therefore allows
// for less common methods.
method: 'post',
route: '/api/posts',
params: [id, location],
query: {
anAnalyticsThing: {
// must be using bodyParser middleware with urlencoded method's extended property set to true
// for nested objects in 'query' to work (it's the default but many examples set this to false):
// 'bodyParser.urlencoded();' or 'bodyParser.urlencoded({ extended: true});'
aDeeplyNestedProperty: 'example'
},
anotherProperty: 'example2'
},
body: blogPost,
headers: {
// note you should not set the 'Content-Type' header yourself unless you really think you have to
// as this is being inferred for you by simple-iso-fetch
aHeadersProperty: 'value'
},
// when 'includeCreds' property is set to true, credentials will be included in the request no matter
// where the request is being made to, if this is set to false only 'same-origin' (internal to app) requests
// will include credentials which means they'll never be included in requests coming from server until Node.js
// implements native Fetch API. 'credentials' must be included for authentication
includeCreds: true,
// FOR ALL RESPONSE TYPES OTHER THAN ARRAYBUFFER YOU DON'T NEED TO USE 'responseType' PROPERTY AS TYPE WILL BE INFERRED.
// For an 'arrayBuffer' response this is needed however, as there's no way (that I've found)
// to infer that a response is an arrayBuffer vs. a blob
responseType: 'arrayBuffer'
})
.then(res => console.log(res)) // console.logs whatever the response is
.catch(err => console.log(err)); // console.logs whatever the error is
import simpleIsoFetch from 'simple-iso-fetch';
// set host to your app's hostname for server-side fetching
simpleIsoFetch.setBaseUrl('http://localhost:3000');
// bind function to error response, returns function to stop binding this function (useful for React's ComponentWillUnmount)
const unbindThisErrorFunction = simpleIsoFetch.bindToError(res => {
console.log('There was an error!');
});
// unbinds the function that was bound above, so it will no longer get run upon error responses
const wasBound = unbindThisErrorFunction();
// the unbinding function returns 'true' if the function it tried to unbind was actually bound when it was called and 'false' if it was not
console.log(wasBound);
// bind function to success response, returns function to unbind this function (useful for React's ComponentWillUnmount)
const unbindThisSuccessFunction = simpleIsoFetch.bindToSuccess(res => {
console.log('There was a successful response from a fetch!');
});
// unbinds the function that was bound above, so it will no longer get run upon success responses
const wasBound = unbindThisErrorFunction();
// the unbinding function returns 'true' if the function it tried to unbind was actually bound when it was called and 'false' if it was not
console.log(wasBound);
// bind function to all responses (success and error), returns function to unbind this function (useful for React's ComponentWillUnmount)
const unbindThisResponseFunction = simpleIsoFetch.bindToResponse(res => {
console.log('There was an error or successful response from a fetch!');
})
// unbinds the function that was bound above, so it will no longer get run upon responses
const wasBound = unbindThisErrorFunction();
// the unbinding function returns 'true' if the function it tried to unbind was actually bound when it was called and 'false' if it was not
console.log(wasBound);
// you can reference the arrays of bound functions with the below properties, note that if you modify these arrays directly and affect order or overwrite functions, your unbind functions will no longer work
simpleIsoFetch.boundToError: [], // array of functions to be called upon error
simpleIsoFetch.boundToSuccess: [], // array of functions to be called upon success responses
simpleIsoFetch.boundToResponse: [], // array of functions to be called upon all responses
Not far into making this library I had to solve the problem of being able to pass an instance of "SimpleIsoFetch" throughout my redux application in order to put persist the authentication cookie for server side requests in a universal app.
I provided a solution for this with using redux middleware and also provided a way to put the functions you have bound to API responses on your redux state and modify them with actions.
Note that if you are not using redux or not making a universal app that has authentication, you can still use everything above this point and have a nifty fetching tool, but if you do need to handle the isomorphic redux thing, you're covered below :).
The key point is that you can use simpleIsoFetch as a class and make an instance of it where you pass in an express "request" object, this will bind the cookie contained in that request to all uses of that instance. This instance can then be passed throughout your application to your redux action creators.
// on your root universal route
app.get('/*', (req, res, next) => {
const simpleIsoFetch = new SimpleIsoFetch(req); // make an instance of
const store = configureStore(..., simpleIsoFetch, ...);
...
});
Since redux-thunk is very common for handling async requests with redux I have included middleware for this pattern, you can of course feel free to make your own sand just pass your 'simpleIsoFetchInstance' into that.
// in 'configureStore' file/function
import thunk from 'redux-thunk';
import { simpleIsoFetchThunk } from 'simple-iso-fetch';
import rootReducer from '../reducers';
export function configureStore(..., simpleIsoFetchInstance, ...) {
const finalCreateStore = applyMiddleware(simpleIsoFetchThunk(simpleIsoFetchInstance), thunk, ...)(createStore);
const store = finalCreateStore(rootReducer, initialState);
return store
}
// Your async action creators will now be curried with 'simpleIsoFetch' preceding 'dispatch', see example async action creator below
...
export function logIn(body) {
return simpleIsoFetch => dispatch =>
simpleIsoFetch.post({
route: '/api/login',
body
})
.then(({body: user}) =>
dispatch({
type: 'LOGIN_SUCCESS',
user
})
.catch(error => {
dispatch({
type: 'LOGIN_FAIL',
error
})
});
}
...
In order to still have functions bound to API responses on our instance and have those carried through our isomorphic app we need to place the arrays of bound functions ('boundToError', 'boundToSuccess', and 'boundToResponse') on our redux state and make them modifiable with actions, here's how we do it
/// in root reducer file
import { bindingsReducer } from simpleIsoFetch;
...
/// assuming you're using 'combineReducers'
export default combineReducers({
...
simpleIsoFetch: bindingsReducer // simpleIsoFetch is expected name, can be modified
})
import SimpleIsoFetch, { syncBindingsWithStore } from '../shared/lib/api';
// create simpleIsoFetch instance
const simpleIsoFetch = new SimpleIsoFetch();
// configure store
const store = configureStore(..., simpleIsoFetch, ...);
// feed store and instance into 'syncBindingsWithStore' function to place 'boundToError', 'boundToSuccess', and 'boundToResponse' arrays on state
syncBindingsWithStore(simpleIsoFetch, store);
Here is an example of how to send a 'react-toastr' (http://tomchentw.github.io/react-toastr/) message upon error responses with a status code of 500 or greater.
(to actually get react-toastr fully working requires stylesheets as well, see a fully working example implemented here, try logging in without creating a user.)
// react-toastr library needs
import {
ToastContainer,
ToastMessage,
} from 'react-toastr';
const ToastMessageAnim = ToastMessage.animation;
// used to create actions to bind functions to API call responses
import { bindToErrorAction, unbindFromErrorAction } from 'simple-iso-fetch';
@connect()
export default class App extends Component {
static propTypes = {
dispatch: PropTypes.func,
children: PropTypes.object.isRequired
}
componentDidMount() {
// function for creating toast errors upon responses with status of 500 or greater
this.errorToastFunc = (res) => {
res.status >= 500 &&
(res.body.errors || [{errorMessage: res.body}]).forEach(error =>
this.refs.container.error(
process.env.NODE_ENV === 'production' ?
'Sorry! ...please refresh the page' :
`${error.errorCode || 500}: ${error.errorMessage} \n ${res.method},${res.url}`,
`${res.body && res.body.status || res.status || 500} (internal)` || 'There was a server-side error',
{closeButton: true}
));
}
// transform response
this.props.dispatch(bindToErrorAction(this.errorToastFunc));
}
componentWillUnmount() {
// this is needed to avoid binding twice on hot reloading (good in principal regardless)
this.props.dispatch(unbindFromErrorAction(this.errorToastFunc));
}
render() {
return (
<div>
<ToastContainer
toastMessageFactory={props =>
<ToastMessageAnim {...props}
timeOut={6000}/>}
ref='container'
className='toast-top-right'/>
{this.props.children}
</div>
);
}
}