react-redux-with-networking-helper
v0.0.35
Published
A tool to make dropping redux into a project easy, fast, and provide a good way to do networking as well
Downloads
27
Maintainers
Readme
react-redux-with-networking-helper
Required versions
- react > 16.8.6
- or
- react-native > 0.60.0
Hooks
Hooks are fully supported, examples and documentation includes connect
based and hook based methods for implementation.
Project
This project aims to produce a low code, high productivity solution to working with React/Redux and networking.
It stores networking state and 'global' state separately, but provides tools for combining the two effectively - keeping a single source of truth about your application while simultaneously updating it with network data.
In addition, the library provides some utilities to handle disconnection and network retry as well as 'expiration' of network requests over time.
This project was built for React Native, although it also works with React DOM
Getting Started
First, install the package
yarn add react-redux-with-networking-helper
Then you need to wrap your upper most component (react-native: usually the one registered with AppRegistry.registerComponent
) like this:
import { ReduxWrapper } from 'react-redux-with-networking-helper';
export default class Entrypoint extends Component {
render() {
return (
<ReduxWrapper>
{/* Your app children here */}
</ReduxWrapper>
);
}
}
Then you are ready to go - you can connect up any components to the store and use any of the API's below, or create and use your own.
Examples
React Native
In this example we use the data API like the this.setState API, note that this example will persist over reloads and application crashes due to the persistor. You would probably not make a controlled input, using this library instead of setState, in most cases - this is just for the example.
TODO things
- Using the Data API
- Using the Networking API
- Using the Data and Networking API together
- Advanced and fun stuff
React DOM
TODO
How To Think About It
The whole point of this library is to have a single source of data truth - the 'global state' - this state can then be 'tuned in to' by various components using the connect method from react-redux in order to get updates. We have a bunch of selectors for selecting this data in a convenient way.
This 'global state' is then updated, either directly through actions on components - i.e. when you need to update the user's current handle - or indirectly through network data - i.e. when you just logged in and need to update the current users object.
Under the Hood
Under the hood we use the following libraries:
- axios - for all the networking
- react-redux - for the global state
- redux-saga - to handle sagary stuff (networking, in this case)
- reselect - to let us efficiently subscribe to state changes on components without causing re-renders
- immer - for easy global state manipulation
A note for react-native
You need to pass in a storage mechanism explicitly as of redux-persist v6.0.0, this is how you do this:
import AsyncStorage from '@react-native-community/async-storage';
...
<ReduxWrapper
persistorStorageOverride={ AsyncStorage }
<
...
</ReduxWrapper>
...
There are problems with react native on android with large states when usinig the redux-persist library. If you want to work around this, you need to install redux-persist-filesystem-storage into your project and link it correctly, then pass the storage into the Wrapper like this:
import FilesystemStorage from 'redux-persist-filesystem-storage';
import AsyncStorage from '@react-native-community/async-storage';
import { Platform } from 'react-native';
...
<ReduxWrapper
persistorStorageOverride={ Platform.OS === 'android' ? FilesystemStorage : AsyncStorage }
<
...
</ReduxWrapper>
...
Something about the persistor
We only persist network transactions to the storage once they are finished, this is to stop the case where the runtime crashes, and is then restarted but with half complete network transaction states in the global state that no saga is operating on and will never complete.
Locations
Locations are an important part of the library. They are specified as string with the following syntax and effectively they dictate where you read and write data:
some.location.in.the.data.store
You can also include array indices like this
some.location.with.an.array.[3].in.the.[0].data.store
- Whenever putting data into the data store, it will create all levels specified if they do not exist, as objects.
- Whenever pulling data out of the data store, traversal stops once a level is reached that does not exist and an empty object
{}
is returned or whatever was specified as the default value when creating the selector.
Selectors - makeGet and get
Bear in mind that under the hood we are using Reselect for our selectors, there are, therefore, two types of every selector - you can read about this in detail in the reselect documentation here - but the cheatsheet version is - if you only have one instance of a component you can use the get***
function, if you will have multiple instances of a component you need to use the equivalent makeGet***
function to build your selectors. To be on the safe side, use the makeGet***
.
For some selectors we allow for caching, that caching is applied to the makeGet***
functions so that instead of having multiple components with many functions that do the same thing, we have multiple components with a single function that does the same thing.
When using hooks, the same rules apply but with different syntax, full explination here with documentation and examples below.
APIs
There are two different API's that have actions and selectors: the Data API and the Network API. There is no link from the Data API to the Network API, but there is a link from the Network API to the Data API - that is to say, network requests can modify the 'global state'.
In addition, there is the ReduxWrapper API which is the main component you will use to wrap your application.
The ReduxWrapper API
| Property | Default | Description
| --- | --- | ---
| setDebugWithCurlirize
| false
| Will print all network out as curl commands
| networkExceptionCallback
| (err) => {}
| Is called for any exceptions that occur in axios, application order can be seen here
| globalResponseIntercept
| (obj) => {}
| Is called for all network responses with the obj formatted like this:{ type
: 'error' or 'success', insertData
: 'data after relevant formatters are applied', responseData
: 'the original network response data' or 'exception object if responseStatusCode is -1', responseStatusCode:
'response status code' or '-1 if an exception', responseHeaders
: 'response headers' or 'empty object if an exception',}application order can be seen here
| globalErrorFormatter
| (data) => data
| This is called for any error condition, see order here, and should return any data format you want to use in subsequent items, this is useful because often you want to format your error data from an API in a specific, and global way.
| additionalReducers
| []
| Should contain any additional reducers that you want to include in the state, you can fire actions that will be picked up by these in any connected component.
| networkTestAction
| {}
| If specified as some NetworkActions.start***
action, this will be used to test for network connectivity changes once a downtime event is triggered. I.e. networkTestAction={NetworkActions.startGET({url: 'https://some-test-url'})}
.All network connections that fail, with this specified, will be queued up if they have autoRetryOnNetworkReconnection
set to true on their individual config and then will be actioned when this network request succeeds
| networkTestDelay
| 10000
| This is the delay between uptime checks on the networkTestAction
| persistorStorageOverride
| null
| Provide an alternative storage method for the persistor, see more here
| defaultContentTypes
| defaultContentTypesObject
| For each type of request, this object specifies the default value for the Content-Type
header. This value can and will be overwritten by other headers, see header order of priority here. See below this table for the default values, null set for any method will not apply any value to Content-Type
defaultContentTypesObject
default value
{
get: null,
post: 'application/json',
patch: 'application/json',
delete: 'application/json',
head: null,
options: null,
put: 'application/json',
}
You can access methods on the ReduxWrapper like this:
<ReduxWrapper
ref={(el) => { this.wrapper = el; }}
>
{/* Your app children here */}
</ReduxWrapper>
...
this.wrapper.METHOD()
| Function | Description
| --- | ---
| persistorPurge()
| Will clear the persistor storage completely.
The Data API
Actions (DataActions)
Modifying the data
DataActions.setData(location, data, keys = null)
Overrides any element at the current location with the data specified. See keys note below for description.
DataActions.mergeData(location, data, keys = null)
Uses Object.assign({}, currentStateAtLocation, data)
to mutate the state
DataActions.concatFirstData(location, data)
Uses locationState = data.concat(currentStateAtLocation);
to add the existing state (assuming it is an array) to the end of the data you are inserting, if the data
value is not an array it will be converted to an array prior to concatenation.
DataActions.concatLastData(location, data)
Uses locationState = currentStateAtLocation.concat(data);
to add your data to the end of the array at the location, if the data at the location is not an array, it will be overwritten with a blank array.
The keys
parameter is useful for putting array data with some unique ID into the global state so it can be referenced - the keyExtractor parameter in network requests is used to generate the keys
array.
If the keys
parameter is specified, it must be an array, and the data
must be an array with the same length as keys.
The data in data
is 'spread' onto the location
with the keys as values - an example:
const data = [{one: true}, {two: true}, {three: true}];
const keys = ['a', 'b', 'c'];
const location = 'some.location';
setData(location, data, keys);
// the modified state would now have this value:
{
some: {
location: {
a: {one: true},
b: {two: true},
c: {three: true},
}
}
}
in contrast, not using a keys array gives this behaviour:
const data = [{one: true}, {two: true}, {three: true}];
const location = 'some.location';
setData(location, data);
// the modified state would now have this value:
{
some: {
location: [
{one: true},
{two: true},
{three: true}
]
}
}
Removing data
DataActions.unsetData(location)
Removes data at a specific location from the state.
DataActions.unsetKeysAtLocation(location, keys)
Removes any keys in the keys array at the location.
DataActions.clearWithIgnoreList(ignores)
Removes everything from the state, while ignoring any keys in 'ignores'
This would remove all keys on the global data store except userObject
and applicationVersion
DataActions.clearWithIgnoreList(['userObject', 'applicationVersion'])
DataActions.clearAllData()
Restores the state back to how it was when initialised. Useful for logout type events.
Selectors (DataSelectors)
Remember that makeGet***
will return a function to select on and so the value passed to a makeGet***
function does NOT perform the same function as the location value that is passed to the returned function. Read more here.
Listening to one location
DataSelectors.makeGetData(cacheName = null)
returns a function with the signature
(state, location, emptyReturnValue = {})
The optional cacheName
value instructs the selector to cache the returned function, and additional calls to makeGetData with the same cacheName value will return the same function, make sure that when doing this, the resulting function call also has the same location: as a tip use the location as the cacheName.
This returns a function that can be used to select data like this:
const makeMapStateToProps = () => {
const getData = DataSelectors.makeGetData();
function mapStateToProps(state) {
return {
_alertsCounter: getData(state, 'user.alerts.counter', 0),
};
}
return mapStateToProps;
};
Which will return the data in user.alerts.counter, or 0 if it does not exist.
It will appear on the component like this:
console.log(this.props.alertsCounter); // reads 0 if not defined, something else if it is
DataSelectors.getData()
function mapStateToProps(state) {
return {
alertsCounter: DataSelectors.getData(state, 'user.alerts.counter', 0),
};
}
Which will return the data in user.alerts.counter, or 0 if it does not exist.
It will appear on the component like this:
console.log(this.props.alertsCounter); // reads 0 if not defined, something else if it is
Multiple locations
Now, there is a problem with these selectors, while they may provide you with convenience, the whole point of selectors is to select just the data that changes so you don't end up render thrashing. Because of the structure of these selectors, they can lead to bad performance if miss-used. In general, it is better to over-select data (i.e. select staticData instead of staticData.one and staticData.two, even i there is staticData.three in the state) or use multiple selectors on the same component that have good re-use and caching than to use these.
What this is useful for is assigning data to specific keys, all from the same data structure, like current, previous, next
in a news article app, for instance.
DataSelectors.makeGetDataMulti()
returns a function with the signature
(state, inputLocations)
where inputLocations is an array of object like this:
{
name: <string> // the name the connected component will reference this value by
location: <string> // the location to pull data from
emptyReturnValue: <any> // optional return value if not found, defaults to empty object {}
}
eg:
[
{ name: 'appState', location: 'app.state' },
{ name: 'userAlertsCounter', location: 'user.alerts.counter', emptyReturnValue: 0},
...
]
This returns a function that can be used to select data like this:
const makeMapStateToProps = () => {
const getDataMulti = DataSelectors.makeGetDataMullti();
function mapStateToProps(state) {
return {
globalData: getDataMulti(state, [
{ name: 'appState', location: 'app.state' },
{ name: 'userAlertsCounter', location: 'user.alerts.counter', emptyReturnValue: 0},
]),
};
}
return mapStateToProps;
};
Which will return the data both the app.state location and the user.alerts.counter location
It will appear on the component like this:
console.log(this.props.globalData.appState); // contains the value in app.state or {} if undefined
console.log(this.props.globalData.userAlertsCounter); // contains teh value in user.alerts.counter or 0 if undefined
DataSelectors.getDataMulti(state, inputLocations)
where inputLocations is an array of object like this:
{
name: <string> // the name the connected component will reference this value by
location: <string> // the location to pull data from
emptyReturnValue: <any> // optional return value if not found, defaults to empty object {}
}
eg:
[
{ name: 'appState', location: 'app.state' },
{ name: 'userAlertsCounter', location: 'user.alerts.counter', emptyReturnValue: 0},
...
]
This returns a function that can be used to select data like this:
function mapStateToProps(state) {
return {
globalData: DataSelectors.getDataMulti(state, [
{ name: 'appState', location: 'app.state' },
{ name: 'userAlertsCounter', location: 'user.alerts.counter', emptyReturnValue: 0},
]),
};
}
Which will return the data both the app.state location and the user.alerts.counter location
It will appear on the component like this:
console.log(this.props.globalData.appState); // contains the value in app.state or {} if undefined
console.log(this.props.globalData.userAlertsCounter); // contains teh value in user.alerts.counter or 0 if undefined
The Network API
Actions (NetworkActions)
Starting a Network Transaction
Network transactions have a variety of configuration options, all detailed in the NetworkActions.start*****
documentation, but fundamentally there are two different types of request, and it matters because of how you will access them with the selectors:
- Single - has an identifier, and a state - good for getting an individual users details
- Multiple - has an identifier, a sub identifier, and each identifier/sub identifier combo has its own state - good for getting paginated data
In addition, there are two types of data when it comes to network request
- The current state of the network request - you 'tune into' this with
makeGetNetworkData
orgetNetworkData
and it contains data about the current state of the transaction but does NOT (by default, although you can make it) contain the success response data - it will only contain error data by default. - The response data - usually it is expected that this be dumped onto the global state and be accessed with the
DataSelectors
- you need to see theresponseTarget
parameter for more information in theNetworkActions.start*****
The framework provides massive flexibility in how you handle response data, errors etc. but the main idea is that the networking component updates the global state.
Network request flow
| >= 200 && <= 299 | Other status codes | Exception
| --- | --- | ---
| requestConfig.successFormatHandler
| ReduxWrapper.globalErrorFormatter
| ReduxWrapper.networkExceptionCallback
| requestConfig.setGlobalHeaders
| requestConfig.errorFormatHandler
| requestConfig.errorFormatHandler
| requestConfig.keyExtractor
| requestConfig.errorCallback
| ReduxWrapper.globalResponseIntercept
| requestConfig.preDataInsertCleanupHandler
| ReduxWrapper.globalResponseIntercept
| requestConfig.successCallback
|
| ReduxWrapper.globalResponseIntercept
|
Header order priority
There are a variety of ways to modify headers for an individual request - here is the order in which they are applied to any request:
ReduxWrapper.defaultContentTypes
is applied for the specific request type, setting theContent-Type
header if not null- The global headers are then collected, these are set through either
requestConfig.setGlobalHeaders
or directly throughNetworkActions.setGlobalHeaders
. If there is aContent-Type
header, it will override the header set in the previous step. - The request specific headers are then applied from
requestConfig.additionalHeaders
, overwriting anything from the previous two steps if there are conflicts
NetworkActions.startGET(requestConfig)
NetworkActions.startPOST(requestConfig)
NetworkActions.startPATCH(requestConfig)
NetworkActions.startDELETE(requestConfig)
NetworkActions.startHEAD(requestConfig)
NetworkActions.startOPTIONS(requestConfig)
NetworkActions.startPUT(requestConfig)
The requestConfig
object has these parameters:
The request data
| Value | Required | Default | Description
| --- | --- | --- | ---
| url
| true
| null
| This is the fully qualified domain name for the request
| data
| false
| {}
| The request data, ignored for GET requests
| multi
| false
| false
| if this is true, there can be more than 1 request for the same identifier, see multiIdentifier
| additionalHeaders
| false
| []
| array of additional headers in the format of { name: 'header-name', value: 'header-value' }
to be added on, these are applied LAST so can override global headers
Where to put the response
| Value | Required | Default | Description
| --- | --- | --- | ---
| responseTarget
| false
| null
| see location documentation - a string like location.other.place
to target in the global data or null to not put it into the global data, use the successFormatHandler
and keyExtractor
to decide the format of what ends up here
| responseTargetMethod
| false
| set
| decides how to handle placement of the data onto the responseTarget
, merge
will shallow merge the object, set
will just set it, concatFirst
and concatLast
will perform an array concatenation (start of end of the existing array) - note the concat values do not work with a value set on keyExtractor
- the extractor will be ignored
How to listen to the status of the network request
| Value | Required | Default | Description
| --- | --- | --- | ---
| identifier
| false
| null
| Identifier used to monitor the status of this request if you don't need to hook into the response at all set this to null, check out the selectors section for more info
| multiIdentifier
| false
| null
| If this is not null, it will be used as a sub identifier (see selectors to understand how it is used), if null and multi is set to true, uuid will be generated, check out the selectors section for more info
| dumpSuccessResponseToNetworkState
| false
| false
| if this is set to true, the data will be put onto the network state (specified by identifier/multiIdentifier
). Setting this to true will not stop anything that happened with responseTarget and its chain of actions.
Lifecyle hooks
application order can be seen here
| Value | Required | Default | Description
| --- | --- | --- | ---
| successFormatHandler(data, statusCode, existingData, responseHeaders) => data
| false
| null
| a function that is called with any 200 <> 299 status code, can return an array of objects to be dumped into the responseTarget
- the keyExtractor
will be called to distribute them properly, if this does not return an array, it will not call the keyExtractor
at all and just dump onto the responseTarget
, if the keyExtractor
is null, the response will just be dumped onto the responseTarget
- existingData
is NULL if no responseTarget
specified, or is the current data in the store at that target
| errorFormatHandler(data, statusCode, err, responseHeaders) => { return data; }
| false
| null
| a function that is called with any other status code not captured by the success handler, whatever is returned by this function is returned as the data attribute when there is an error, note it is possible for this function to receive an exception (Error) type as well as an actual response, the statusCode will be -1 if this is the case with the error as the third argument, and the error.toString() value as the data
| successCallback(formattedData, originalData, statusCode, responseHeaders) => {}
| false
| null
| a function that is called with any 200 <> 299 status code after the sucessFormatHandler in case you need to do any side effects as a result of a success condition
| errorCallback(formattedData, originalData, statusCode, responseHeaders) => {}
| false
| null
| called when there is an error, inverse of successCallback
| preDataInsertCleanupHandler(existingData, modifiedKeys, networkState, responseHeaders) => {}
| false
| null
| this function is called right before successFormatHandler and the insert action for response target which can be used to remove / clean up old state changes
| keyExtractor(item, index) => 'key'
| false
| null
| this is called for every element returned by the successFormatHandler
and should return a key that will be used to allocate the data to the responseTarget.[key]
location. if it is null then it will not be used and the data will be just dumped onto the object, this will not be called if the data returned from successFormatHandler
is not an array
| setGlobalHeaders(formattedData, originalData, statusCode) => {}
| false
| null
| This is called before the success format handler with the same conditions as success format handler (200 <> 299 status) - any thing return by this needs to be an array of { name: 'header-name', value: 'header-value' }
and will update the global headers so every subsequent request has this set of headers, useful for authentication. If it does not return an array then nothing happens.
Retrying, timeouts and multiple similar requests
| Value | Required | Default | Description
| --- | --- | --- | ---
| autoRetryOnNetworkReconnection
| false
| false
| If autoRetryOnNetworkReconnection is true, we will retry this network request if it fails due to network connectivity once connectivity is re-established, tested with networkTestAction
set on the ReduxWrapper
. Make sure networkTestAction
is specified or these network connections will not be retried.
| timeout
| false
| -1
| Once this many seconds have gone by, the network state will be set to EXPIRED, negative numbers are ignored entirely
| cancelInFlightWithSameIdentifiers
| false
| true
| If this is set to true, then we match against some identifier/multiIdentifier combo - if they are the same, any in flight request is cancelled and replaced with the new one.
Clearing network data
NetworkActions.clearNetworkData(identifier)
This will clear all the network response data for a given identifier, regardless of if it is single or multi
NetworkActions.clearAllNetworkData()
This completely clears everything in the network state, setting the whole thing to default, including all headers and anything else.
Manipulating global headers
NetworkActions.setGlobalHeaders(headers)
You can call this to set a global header, see how global headers are applied here.
Format for headers
parameter is
[
{name: 'Content-Type', value :'application/json'}
....
]
Any headers in the state with the same name will be overwritten.
Selectors (NetworkSelectors)
Remember that makeGet***
will return a function to select on and so the value passed to a makeGet***
function does NOT perform the same function as the location value that is passed to the returned function. Read more here.
Network response object
When using a network selector there are a couple of scenarios to worry about:
- Single request selectors always return an object with the structure outlined below
- Multi request selectors return a keyed object, if a request was started for a specific key then it will have the structure outline below, if it is not started it will be undefined - this is a limitation because we do not know ahead of time what the 'multi identifiers' will be so can't get the selector to populate them with 'not started'
The object takes this form:
{
state: NetworkStates.NOT_STARTED || NetworkStates.LOADING || NetworkStates.RELOADING || || NetworkStates.ERROR || NetworkStates.SUCCESS || NetworkStates.EXPIRED,
statusCode: null || <response status code>,
data: {} || <error data> || <success data if requestConfig.dumpSuccessResponseToNetworkState set to true>,
started: false || true,
finished: false || true,
startTimestamp: null || <timestamp>,
endTimestamp: null || <timestamp>,
stateUpdatedKeys: null || <array of keys>,
_internalID: null || <internal tracking ID for this library>,
}
Some details about the values:
| Value | Description
| --- | ---
| state
| Is a value that indicates what sort of state the network request is in so you can show loading spinners etc.
| statusCode
| Will be null until the state
is either NetworkStates.ERROR
or NetworkStates.SUCCESS
or NetworkStates.EXPIRED
| data
| Will contain data if state
=
NetworkStates.ERROR
OR if state
=
NetworkStates.SUCCESS
and requestConfig.dumpSuccessResponseToNetworkState
=
true
| started
| False if the request is not started for single requests, it will never be false for a multi request as starting a multi request is the thing that populates the relevant key on the multi identifier object
| false
| False if the state is not NetworkStates.ERROR
or NetworkStates.SUCCESS
or NetworkStates.EXPIRED
| startedTimestamp
| The time the network request was started
| endTimestamp
| The time the network request ended
| stateUpdatedKeys
| Details the keys the requestConfig.keyExtractor
updated as part of this request
| _internalID
| Used by this library internally
Network requests have a set of 'states' associated with them - you can compare the current state of a network request to the NetworkStates
object
import { NetworkStates } from 'react-redux-with-networking-helper';
console.log(NetworkState);
{
NOT_STARTED: 'not-started', // not started - note this will never be seen for multi requests
LOADING: 'loading', // we are loading something (for the first time)
RELOADING: 'reloading', // we are 're' loading something
ERROR: 'error', // there was an error
SUCCESS: 'success', // there was success
EXPIRED: 'timed-out', // a request has been expired because of the requestConfig.timeout value
}
Listening to a single request
NetworkSelectors.makeGetNetworkData()
Returns a function with the signature
(state, identifier)
Where identifier matches whatever you used in the initial request config.
The usage is like this:
const makeMapStateToProps = () => {
const getNetworkData = NetworkSelectors.makeGetNetworkData();
function mapStateToProps(state) {
return {
_networkData: getNetworkData(state, 'user-settings'),
};
}
return mapStateToProps;
};
Which returns data in this format onto the _networkData
property, details of which can be found here
NetworkSelectors.getNetworkData(state, identifier)
Where identifier matches whatever you used in the initial request config.
The usage is like this:
function mapStateToProps(state) {
return {
_networkData: NetworkSelectors.getNetworkData(state, 'user-settings'),
};
}
return mapStateToProps;
Which returns data in this format onto the _networkData
property, details of which can be found here
Listening to a multi request
DataSelectors.makeGetNetworkDataMulti()
Returns a function with the signature
(state, identifier)
Where identifier matches whatever you used in the initial request config.
The usage is like this:
const makeMapStateToProps = () => {
const getNetworkDataMulti = NetworkSelectors.makeGetNetworkDataMulti();
function mapStateToProps(state) {
return {
_networkData: getNetworkDataMulti(state, 'user-settings'),
};
}
return mapStateToProps;
};
Which returns data in this format onto the _networkData
property, details of which can be found here
DataSelectors.getNetworkDataMulti(state, identifier)
Where identifier matches whatever you used in the initial request config.
The usage is like this:
function mapStateToProps(state) {
return {
_networkData: NetworkSelectors.getNetworkDataMulti(state, 'user-settings'),
};
}
return mapStateToProps;
Which returns data in this format onto the _networkData
property, details of which can be found here
Custom Actions, Reducers, Selectors and Sagas
This library provides a bunch of actions, reducers, selectors and sagas to perform the pre-canned set of tasks it was designed for, that is not to say that it is limited to just those things, you can plug your stuff into the underlying store easily
You can specify your own actions and reducers by passing in the appropriate reducers to the additionalReducers
prop on the ReduxWrapper
component. Once you have specified them, you can call dispatch whatever actions they respond to from any connected component.
You can build your own selectors anywhere, and just include them - react-redux-with-networking-helper uses reselect to build it's selectors, but you can use anything.
TODO - not supported yet
TODO
- Documentation on how we export immer for import in other projects (convenience, immerProduce)
- Update docs examples with hooks everywhere
- Make examples and finish documentation
- Put hook examples into documentation and include imports in the in-line API ddlcs
- Add doNotPersistKeys to docs
- Provide additional / custom sagas to the initialisation of the library
- Test on dom react (non react-native) project / environment
- TODO convenience method for selector creation so you don't have to reference immer directly
- Removal / invalidation of global headers?
- Some kind of debugging switch for selectors etc. to see performance
- a way to select between shallow and deep equity checking
- Cleanup the locationStore variable in the referenceData selector
- A flag to toggle the redux logging middleware
- Update the dependencies - don't need strict version numbers on stuff like prop-types etc.
- When cancelling active network request SAGA forks, the axios transactions are not interrupted and will still complete and return, this can lead to performance problems as the underlying device will probably limit how many connections are allowed, meaning its possible to take ages for things to complete even though the sagas are dead and don't care about the response
- We don't properly cleanup the timeouts when calling LOCAL_NETWORK_CLEAR_NETWORK_DATA
- need to do something to support state migrations as is supported by react-redux
- test what will happen is networkTestAction is not specified - we need to not store up network requests if we have no way of unblocking them etc.
- toggle if you want to even use the persistor
- one setDebugWithCurl is set on the wrapper, it can't be unset
- document the immer import/export on the wrapper