Redux Responder
Redux without side effects
Like Redux Thunk or Redux Saga, Redux Responder enables the incorporation of asynchronous, non-deterministic code into a Redux application. Unlike thunks or sagas, this is done without introducing middleware that alters the handling of action creators or reducers, allowing Redux to remain "a predictable state container for JavaScript apps" without side effects.
There is already a place for asynchronous, non-deterministic behavior in a Redux application: the black box between a change in state and the dispatching of a new action in response to that change. That black box is typically a user mediated by a React UI, but it could just as easily be our own code.
Responder enforces the convention that only a change in state will initiate an unpredictable process that dispatches a single action, executed by either a user or a responder.
Contents
Note: There is a similar mechanism called "Reactors" included in the very interesting Redux Bundler library.
Usage
Responders use Reselect under the hood, and work in a similar way. If any of the values specified by an array of state selectors change, they are passed to a function that optionally returns a promise. Action creators can be specified for both success and failure of that promise.
;; const fetchResponder = ; ;
Conventions
Using responders should require minimal changes to your application, aside from the structure of your Redux store. Using other solutions, an action could initiate an API request directly without touching any data in the store. Using a responder, a similar action would only be able to update a value in the store, which would then in turn trigger a responder.
Using Thunk
const initialState = response: null error: null; const reducer = { }; const initiateRequest = async { ; try const response = await ; const json = await response; ; catch error ; };
Using Responder
; const initialState = requestStatus: 'UNINITIATED' response: null error: null; const reducer = { }; const initiateRequest = type: 'REQUEST_INITIATED'; const requestResponder = ;
Although the latter is slightly more verbose, notice that a snapshot of the store at any time will include information about the exact state of the request (in addition to the other benefits described above).
You are free to structure your store however you like, but some conventions have emerged during development that might be useful. Because a responder needs to receive all relevant data for a request from the store, one solution is to maintain an entry in the store for each request:
const initialState = someGetRequest: status: 'UNINITIATED' somePostRequest: status: 'UNINITIATED' body: null ;
Alternately, you can maintain a single entry in the store to be used for initiating any request:
const initialState = request: status: 'UNINITIATED' method: null url: null body: null ;
In a final example, you could use a single entry and responder to choose between predefined request services:
const initialState = request: status: 'UNINITIATED' requestId: null body: null ; const services = SOME_GET_REQUEST: async { const response = await ; return response; } SOME_POST_REQUEST: async { const response = await ; return response; }; const requestResponder = ;
Testing
Responders can be unit tested without connecting them to a Redux store.
An unconnected responder is a function that takes a state and dispatch function as its arguments, and returns a promise that resolves when the dispatch is done (or deliberately skipped). By calling it you are simulating an update to the given state.
;; // Typical action creator { return type: 'EVALUATE_HAPPINESS' payload: isHappy ;} // Always fires the action when state.value changesconst evaluateHappinessResponder = ; // Simulate a sequence of store statesconst states = value: 'sad' value: 'sad' value: 'happy' ; // Actions should only be dispatched when the// value changes in states[0] and states[2]const expectedActions = ; // Mock dispatch function stores actions in an arrayconst actualActions = ;const dispatch = actualActions; // Invoke the responder with each sequential stateconst invokeResponder = ;const promises = states; // Ensure expected actions were dispatched once all responders are done;
API
connect
returns an array of functions that correspond to the array of responders passed into it. These functions can be called to unsubscribe the responder from the Redux store.
;;; const unsubscribers = ; // Unsubscribe responderBunsubscribers1;
Just like Reselect, the internal memory of the selectors' previous values can be reset to guarantee that the responder will evaluate whether its promise should be executed on the next state update.
responderA; // 0 // On the first state update, the responder will evaluate whether its promise should be executed; responderA; // 1 // Since the state hasn't changed, nothing will happen; responderA; // 1 // After resetting, the responder will evaluate once againresponderA; responderA; // 0 ; responderA; // 1
Examples
There are two projects in the examples
directory that illustrate the integration of responders into a React/Redux project.