redux-thunk and in app architecture - want to render only views in views and dispatch GET actions in separate component - redux

I am using react-redux and redux-thunk in my application and there are two things I am trying to do:
I want to be able to share the results of a GET request in two components. I know you can do this by connecting the two components to the store, but I want to make it so if the user lands on X page, then Y page cannot make the same GET request again (these two components are Thumbnail and Carousel). In other words, the GET request should be made once (not 100% sure what best practice is here for redux-thunk), and each component should be able to access the store and render the results in the component (this is easy and I can do)
currently the GET request is the parent of the two children view components, which (I think) doesn't make sense. I only want to render a child view component in the parent view, not a GET request. If unclear it will make more sense if you read my code below
This is parent view (Gallery), which has a child component which dispatches an action to redux (using redux-thunk) that makes an API (FetchImages):
import ...
export default function Gallery() {
return(
<>
<GalleryTabs />
<GalleryText />
<div className="gallery-images-container">
<FetchImages /> ----> this is making an API request and rendering two child view components
</div>
</>
)
}
This is FetchImages, which is dispatching the action (fetchImages) which makes the API call
import ...
function FetchImages({ fetchImages, imageData }) {
useEffect(() => {
fetchImages()
}, [])
return imageData.loading ? (
<h2>Loading</h2>
) : imageData.error ? (
<h2>Something went wrong {imageData.error}</h2>
) : (
<>
<Thumbnail /> -----> these two are views that are rendered if GET request is successful
<Carousel />
</>
)
}
const mapStateToProps = state => {
return {
imageData: state.images
}
}
const mapDispatchToProps = dispatch => {
return {
fetchImages: () => dispatch(fetchImages())
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(FetchImages)
I think it makes more sense to have something like this:
import ...
export default function Gallery() {
return(
<>
<GalleryTabs />
<GalleryText />
<div className="gallery-images-container">
<Thumbnail /> -----> Thumbnail should be rendered here but not Carousel ( FetchImages here adds unnecessary complexity )
</div>
</>
)
}
tldr
What are some best practices to follow if two components can dispatch an action which makes a GET request but the dispatch should only be made once per time the user is on the website?
Using redux-thunk, what are some best practices for separating concerns so that children view components are within parent view components and the smarter components which are shared between children view components (such as dispatching actions that make GET requests) are dispatched when the user lands on the page without the views and smarter components being directly together?
I'm a noob so thank you for any help

Your first question: your component container should just dispatch the action that it needs data. How you should store async result in state and later handle result from state is something not covered in this answer but the later example uses a component named List that just dispatches getting a data page, selects the data page and dumps the data page in UI. The tunk action does an early return if the data is already in state.
In production application you probably want to store async api result with loading, error, requested and a bunch of extra info instead of assuming it is there or not there.
Your second question is partly answered by the first answer. Component containers should just dispatch an action indicating they need data and not have to know about the data already being there, already being requested or any of that stuff.
You can group functions that return a promise with the following code:
//resolves a promise later
const later = (time, result) =>
new Promise((resolve) =>
setTimeout(() => resolve(result), time)
);
//group promise returning function
const createGroup = (cache) => (
fn,
getKey = (...x) => JSON.stringify(x)
) => (...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//permanent memory cache store creator
const createPermanentMemoryCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
resolved: (x) => x,//will not remove cache entry after promise resolves
};
};
//temporary memory cache store creator when the promise is done
// the cache key is removed
const createTmpMemCache = () => {
const map = new Map();
const cache = createPermanentMemoryCache(map);
cache.resolved = (key) => map.delete(key);
return cache;
};
//tesgting function that returns a promise
const testPromise = (m) => {
console.log(`test promise was called with ${m}`);
return later(500, m);
};
const permanentCache = createPermanentMemoryCache();
const groupTestPromise = createGroup(permanentCache)(
testPromise,
//note that this causes all calls to the grouped function to
// be stored under the key 'p' no matter what the arguments
// passed are. In the later List example I leave this out
// and calls with different arguments are saved differently
() => 'p'
);
Promise.all([
//this uses a permanent cache where all calls to the function
// are saved under the same key so the testPromise function
// is only called once
groupTestPromise('p1'),//this creates one promise that's used
// in all other calls
groupTestPromise('p2'),
])
.then((result) => {
console.log('first result:', result);
return Promise.all([
//testPromise function is not called again after first calls
// resolve because cache key is not removed after resolving
// these calls just return the same promises that
// groupTestPromise('p1') returned
groupTestPromise('p3'),
groupTestPromise('p4'),
]);
})
.then((result) => console.log('second result', result));
const tmpCache = createTmpMemCache();
const tmpGroupTestPromise = createGroup(tmpCache)(
testPromise,
//all calls to testPromise are saved with the same key
// no matter what arguments are passed
() => 'p'
);
Promise.all([
//this uses a temporary cache where all calls to the function
// are saved under the same key so the testPromise function
// is called twice, the t2 call returns the promise that was
// created with the t1 call because arguments are not used
// to save results
tmpGroupTestPromise('t1'),//called once here
tmpGroupTestPromise('t2'),//not called here using result of t1
])
.then((result) => {
console.log('tmp first result:', result);
return Promise.all([
//called once here with t3 becuase cache key is removed
// when promise resolves
tmpGroupTestPromise('t3'),
tmpGroupTestPromise('t4'),//result of t3 is returned
]);
})
.then((result) =>
console.log('tmp second result', result)
);
const tmpUniqueKeyForArg = createGroup(createTmpMemCache())(
testPromise
//no key function passed, this means cache key is created
// based on passed arguments
);
Promise.all([
//this uses a temporary cache where all calls to the function
// are saved under key based on arguments
tmpUniqueKeyForArg('u1'), //called here
tmpUniqueKeyForArg('u2'), //called here (u2 is different argument)
tmpUniqueKeyForArg('u1'), //not called here (already called with u1)
tmpUniqueKeyForArg('u2'), //not called here (already called with u2)
])
.then((result) => {
console.log('unique first result:', result);
return Promise.all([
tmpUniqueKeyForArg('u1'), //called with u1 tmp cache removes key
// after promise is done
tmpUniqueKeyForArg('u3'), //called with u3
tmpUniqueKeyForArg('u3'), //not called, same argument
]);
})
.then((result) =>
console.log('unique second result', result)
);
Now that we have code to group functions that return promises (function is not called when called again with same argument) we can try to apply this to thunk action creators.
Because a trunk action creator is not (...args)=>result but (...args)=>(dispatch,getState)=>result we can't pass the action creator directly to createGroup I created createGroupedThunkAction that adopts the function to group from (...args)=>(dispatch,getState)=>result to ([args],dispatch,getState)=>result while still returning a function with the right signature: (...args)=>(dispatch,getState)=>result.
Here is the example snippet:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
//resolves a promise later
const later = (time, result) =>
new Promise((resolve) =>
setTimeout(() => resolve(result), time)
);
//group promise returning function
const createGroup = (cache) => (
fn,
getKey = (...x) => JSON.stringify(x)
) => (...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//thunk action creators are not (...args)=>result but
// (...args)=>(dispatch,getState)=>result
// so here is how we group thunk actions
const createGroupedThunkAction = (thunkAction, cache) => {
const group = createGroup(
cache
)((args, dispatch, getState) =>
thunkAction.apply(null, args)(dispatch, getState)
);
return (...args) => (dispatch, getState) => {
return group(args, dispatch, getState);
};
};
//permanent memory cache store creator
const createPermanentMemoryCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
resolved: (x) => x,//will not remove cache entry after promise is done
};
};
const initialState = {
data: {},
};
//action types
const MAKE_REQUEST = 'MAKE_REQUEST';
const SET_DATA = 'SET_DATA';
//action creators
const setData = (data, page) => ({
type: SET_DATA,
payload: { data, page },
});
const makeRequest = (page) => ({
type: MAKE_REQUEST,
payload: page,
});
//standard thunk action returning a promise
const getData = (page) => (dispatch, getState) => {
console.log('get data called with page:',page);
if (createSelectDataPage(page)(getState())) {
return; //do nothing if data is there
}
//return a promise before dispatching anything
return Promise.resolve()
.then(
() => dispatch(makeRequest(page)) //only once
)
.then(() =>
later(
500,
[1, 2, 3, 4, 5, 6].slice(
(page - 1) * 3,
(page - 1) * 3 + 3
)
)
)
.then((data) => dispatch(setData(data, page)));
};
//getData thunk action as a grouped function
const groupedGetData = createGroupedThunkAction(
getData,//no getKey function so arguments are used as cache key
createPermanentMemoryCache()
);
const reducer = (state, { type, payload }) => {
console.log('action:', JSON.stringify({ type, payload }));
if (type === SET_DATA) {
const { data, page } = payload;
return {
...state,
data: { ...state.data, [page]: data },
};
}
return state;
};
//selectors
const selectData = (state) => state.data;
const createSelectDataPage = (page) =>
createSelector([selectData], (data) => data[page]);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
//improvided thunk middlere
({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
)
)
);
//List is a pure component using React.memo
const List = React.memo(function ListComponent({ page }) {
const selectDataPage = React.useMemo(
() => createSelectDataPage(page),
[page]
);
const data = useSelector(selectDataPage);
const dispatch = useDispatch();
React.useEffect(() => {
if (!data) {
dispatch(groupedGetData(page));
}
}, [data, dispatch, page]);
return (
<div>
<pre>{data}</pre>
</div>
);
});
const App = () => (
<div>
<List page={1} />
<List page={1} />
<List page={2} />
<List page={2} />
</div>
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
In that example there are 4 List components rendered, two for page 1 and two for page 2. All 4 will dispatch groupedGetData(page) but if you check the redux dev tools (or the console) you see MAKE_REQUEST and resulting SET_DATA is only dispatched twice (once for page 1 and once for page 2)
Relevant grouping functions with permanent memory cache is less than 50 lines and can be found here

Related

dispatch is not a function Next.js +thunk load data user after login

When I'm try to getUserProfile() I receive that typeError that dispatch is not a function
Unhandled Runtime Error
Error: Actions must be plain objects. Use custom middleware for async actions.
export const fetchUserProfile = (userData) => ({
type: types.GET_USER_PROFILE,
userData,
});
//thunk
export const loginUser = (credentials) => async (dispatch) => {
dispatch(loginRequest(credentials));
try {
const userToken = await userService.login(credentials);
await dispatch(loginSuccess(userToken));
getUserProfile();
} catch (error) {
const message = await errorMessage(
error,
"There was a problem processing your request"
);
dispatch(loginFailure(message));
}
};
export const getUserProfile = async (dispatch) => {
try {
const profileData = await userService.getProfileData();
dispatch(fetchUserProfile(profileData));
} catch (e) {
console.log(e);
return [];
}
};
You need to dispatch all thunks, replace
getUserProfile();
with
dispatch(getUserProfile())
Your getUserProfile should be a function that accepts its own arguments when you dispatch it, and then it can either be a callback function that accepts dispatch as an argument (this comes from the Redux Thunk middleware) and then that function has functions that return action objects, OR it can just be a function that returns an action object directly (confusing, I know, but you actually did it correctly for your loginUser action):
export const getUserProfile = () => async (dispatch) => {
try {
const profileData = await userService.getProfileData();
dispatch(fetchUserProfile(profileData));
} catch (e) {
console.log(e);
return []; // this shouldn’t be returning an empty array, if anything it should be dispatching an action for errors that can be displayed to the user
}
};
This overly simple example kind of gives you an idea of what's happening (click Run code snippet):
// the "dispatch" function that would come from
// the Redux Thunk middleware
const dispatch = (action) => action((args) => console.log("dispatch:", JSON.stringify(args, null, 2)));
// a "setUserProfile" action that returns an object
const setUserProfile = (payload) => ({
type: "SET_PROFILE",
payload
});
// a "fetchUserProfile" action that returns an object
const fetchUserProfile = () => ({ type: "FETCH_USER" });
// a "showError" action that returns an object
const showError = error => ({ type: "FETCH_USER/ERROR", payload: error });
// while the "getUserProfile" action doesn't have any arguments of
// its own, it accepts a "dispatch" callback function as the SECOND
// set of arguments, then other actions are dispatched (which return
// their own objects)
const getUserProfile = () => async(dispatch) => {
try {
// dispatches the "fetchUserProfile" action
// which just returns: { type: "FETCH_USER" }
dispatch(fetchUserProfile());
// fetching data from API
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const data = await res.json();
// dispatches the "setUserProfile" with data from API
// which returns: { type: "SET_PROFILE", payload: data }
dispatch(setUserProfile(data));
} catch (e) {
console.log(e);
dispatch(showError(e.message));
}
};
// dispatching the "getUserProfile" function above
// optionally, you can add arguments here, but then these would be
// a part of the FIRST set of arguments to the function
dispatch(getUserProfile());
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>

A clean way for an action to fire multiple asynchronous actions with createAsyncThunk

We're delaying the rendering of our React-Redux web app until several asynchronous app initialization tasks in the Redux store have been completed.
Here's the code that sets up the store and then fires off the initialization action:
export const setupStoreAsync = () => {
return new Promise((resolve, reject) => {
const store = setupStore()
store
.dispatch(fetchAppInitialization())
.then(unwrapResult)
.then(_ => resolve(store))
.catch(e => reject(e.message))
})
}
The promise rejection is very important since it's used to render an error message for the user in case the app cannot be properly set up. This code is very nice to read and works wonderfully.
The issue is with the action creator:
export const fetchAppInitialization = createAsyncThunk(
'app/initialization',
(_, thunkApi) =>
new Promise((resolve, reject) =>
Promise.all([thunkApi.dispatch(fetchVersionInfo())]).then(results => {
results.map(result => result.action.error && reject(result.error))
})
)
)
This code works beautifully. If any of these actions fail, the promise is rejected and the user sees an error message. But it's ugly - It's not as pretty as our normal action creators:
export const fetchVersionInfo = createAction('system/versionInfo', _ => ({
payload: {
request: { url: `/system/versionInfo` },
},
}))
We will at some point fire more than one fetch request in fetchAppInitialization, so the Promise.all function is definitely required. We'd love to be able to use Redux-Toolkit's createAction syntax to fire multiple promisified actions in order to shorten this action creator, but I have no idea if that's even possible.
Note: I'm using redux-requests to handle my axios requests.
Is createAsyncThunk even required?
Since I wasn't using the fetchAppInitialization action for anything but this single use case, I've simply removed it and moved the logic straight into the setupStoreAsync function. This is a bit more compact. It's not optimal, since the results.map logic is still included, but at least we don't use createAsyncThunk any more.
export const setupStoreAsync = () => {
return new Promise((resolve, reject) => {
const store = setupStore()
new Promise((resolve, reject) =>
Promise.all([store.dispatch(fetchVersionInfo())]).then(results => {
results.map(result => result.action.error && reject(result.error))
resolve()
})
)
.then(_ => resolve(store))
.catch(e => reject(e.message))
})
}
Update: I was able to make the code even prettier by using async/await.
export const setupStoreAsync = async () => {
const store = setupStore()
const results = await Promise.all([store.dispatch(fetchVersionInfo())])
results.forEach(result => {
if (result.action.error) throw result.error
})
return store
}

Redux - Update store with same function from different files

being rather new to react.js + redux, I'm facing the following conundrum:
I have multiple files, which need to update the store in exactly the same way, based on the stores current state. Currently I simply copy-paste the same code (along with the needed mapStateToProps), which goes again DRY.
Similar to something like the below, where getData is an Ajax call living in the actions file and props.timeAttribute is coming from mapStateToProps:
props.getData(props.timeAttribute).then((newState) => {
console.log(newState)
})
Would a function like that go in the actions file? Can the current state be read from within that actions file? Or does one normally create some sort of helperFile.js in which a function like that lives and is being called from other files?
Thanks!
If your file is executing the same action, then yes, you would put the action creator in a separate file and export it. In theory, you can put state in an action by passing the state as a parameter, but the philosophy behind an action is that it announces to your application that SOMETHING HAPPENED (as denoted by the type property on the return value of the action function). The reducer function responsible for handling that type subsequently updates the state.
You can access the current state of the store inside of an action creator like this:
export const testAction = (someParam) => {
return (dispatch, getState) => {
const {
someState,
} = getState(); //getState gets the entire state of your application
//do something with someState and then run the dispatch function like this:
dispatch(() => {type: ACTION_TYPE, payload: updatedState})
}
I like this approach because it encapsulates all the logic for accessing state inside of the one function that will need to access it.
DO NOT modify the state inside of the action creator though! This should be read only. The state of your application should only be updated through your reducer functions.
Yes, it is recommended to maintain a separate file for your actions.
Below is an example of how i use an action to fetch information and dispatch an action.
export const fetchComments = () => (dispatch) => {
console.log("Fetch Comment invoked");
/*you can use your Ajax getData call instead of fetch.
Can also add parameters if you need */
return fetch(baseUrl + 'comments')
.then(response => {
if (response.ok){
return response;
}
else {
var error = new Error('Error ' + response.status + ': ' + response.statusText);
error.response = response;
throw error;
}
},
error => {
var errmess = new Error(error.message);
throw errmess;
})
.then(response => response.json())
.then(comments => dispatch(addComments(comments)))
.catch(error => dispatch(commentsFailed(error.message)));
}
/* Maintain a separate file called ActionTypes.js where you can store all the ActionTypes as Strings. */
export const addComments = (comments) => ({
type : ActionTypes.ADD_COMMENTS,
payload : comments
});
export const comments = (errMess) => ({
type : ActionTypes.COMMENTS_FAILED,
payload : errMess
});
Once, you receive dispatch an action, you need an reducer to capture the action and make changes to your store.
Note that this reducer must be a pure function.
export const comments = (state = { errMess: null, comments:[]}, action) => {
console.log("inside comments");
switch (action.type) {
case ActionTypes.ADD_COMMENTS:
return {...state, errMess: null, comments: action.payload};
case ActionTypes.COMMENTS_FAILED:
return {...state, errMess: action.payload};
default:
return state;
}
};
Don't forget to combine the reducers in the configureStore().
const store = createStore(
combineReducers({
comments
}),
applyMiddleware(thunk,logger)
);
In your components where you use the Actions, use
const mapDispatchToProps = dispatch => ({
fetchComments : () => dispatch(fetchComments()),
})
Note to export the component as
export default connect(mapStateToProps,mapDispatchToProps)(Component);

Alternative to direct calling of store.dispatch()

Since in the latest redux-observable (0.17) is direct calling of store.dispatch() deprecated, I wonder what is an alternative if I need to dispatch actions from the outside of my redux app.
Example:
Let's say I have this function which initialize native module and set up native handler.
const configure = (dispatch) => {
const printingModule = NativeModules.PrintingManager
const eventEmitter = new NativeEventEmitter(printingModule)
eventEmitter.addListener(
"PrintingManagerNewPrinterConnected",
(payload) => dispatch({
type: PRINTER_MANAGER_NEW_PRINTER_CONNECTED,
payload: {
macAddress: payload[2],
connectionType: payload[3],
},
}))
printingModule.initialize()
}
What I typically do is that I will call this function from observable after something like APP_STARTUP_FINISHED:
const appStatePrepared = (action$: Object, { dispatch }) =>
action$.ofType(APP_STATE_PREPARED)
.switchMap(() => {
configurePrinters(dispatch)
})
What is the correct solution for this?
Thanks!
When using RxJS the ideal is to compose streams. So in this case we need to some how create a stream of "PrintingManagerNewPrinterConnected" events that we can then map each to their own PRINTER_MANAGER_NEW_PRINTER_CONNECTED action.a
Let's first learn how to do this completely custom.
Custom Observables
Creating your own custom Observables is very similar to creating a Promise. So say you had the most simple Promise in the world that just immediately resolves to the number 1
const items = new Promise(resolve => {
resolve(1);
});
The equivalent Observable looks super similar
const items = new Observable(observer => {
observer.next(1);
observer.complete();
});
Visually, the main differences are that instead of being passed (resolve, reject) callbacks we're given an Observer because there is next, error, and complete.
Semantically, Observables can represent more than one value by calling observer.next as many times as they'd like until they call observer.complete() to signal the end of the stream; this is in contrast to Promises, which only represent a single value.
Observables are also lazy and synchronous by default, whereas Promises are always eager and async.
Now that we have that understanding we want to take that and wrap your NativeEventEmitter API which uses addEventListener.
const configurePrinters = () => {
return new Observable(observer => {
const printingModule = NativeModules.PrintingManager;
const eventEmitter = new NativeEventEmitter(printingModule);
eventEmitter.addListener(
'PrintingManagerNewPrinterConnected',
(payload) => observer.next(payload)
);
printingModule.initialize();
});
};
configurePrinters()
.subscribe(payload => console.log(payload));
This works and is super simple, but there's one problem with it: we should call removeListener when they unsubscribe so that we clean up after ourselves and don't leak memory.
To do that, we need to return a subscription inside our custom Observable. A subscription in this context is an object that has an unsubscribe() method on it, which will be called automatically when the subscriber unsubscribes, an error is triggered, or your observable completes. This is your chance to clean up.
const items = new Observable(observer => {
let i = 0;
const timer = setInterval(() => {
observer.next(i++);
}, 1000);
// return a subscription that has our timer cleanup logic
return {
unsubscribe: () => {
clearInterval(timer);
}
};
});
Because returning an object is a bit verbose RxJS supports a shorthand where you just return a function which itself will be treated as the unsubscribe method.
const items = new Observable(observer => {
let i = 0;
const timer = setInterval(() => {
observer.next(i++);
}, 1000);
// return an "unsubscribe" function that has our timer cleanup logic
return () => {
clearInterval(timer);
};
});
Now we can apply this to our example, where we want to remove our listener when our unsubscribe teardown function is called.
const configurePrinters = () => {
return new Observable(observer => {
const printingModule = NativeModules.PrintingManager;
const eventEmitter = new NativeEventEmitter(printingModule);
const listener = (payload) => observer.next(payload);
eventEmitter.addListener(
'PrintingManagerNewPrinterConnected',
listener
);
printingModule.initialize();
return () => eventEmitter.removeListener(
'PrintingManagerNewPrinterConnected',
listener
);
});
};
Now let's turn this into a reusable utility function
const fromPrinterEvent = (eventName) => {
return new Observable(observer => {
const printingModule = NativeModules.PrintingManager;
const eventEmitter = new NativeEventEmitter(printingModule);
const listener = (payload) => observer.next(payload);
eventEmitter.addListener(eventName, listener);
printingModule.initialize();
return () => eventEmitter.removeListener(eventName, listener);
});
};
fromPrinterEvent('PrintingManagerNewPrinterConnected')
.subscribe(payload => console.log(payload));
Observable.fromEvent
While NativeEventEmitter is a react-native thing, it follows the node-style EventEmitter interface and RxJS already comes with a utility helper to create an Observable from them to save you the effort. It's called fromEvent, found at Observable.fromEvent or import { fromEvent } from 'rxjs/observables/fromEvent'.
const fromPrinterEvent = (eventName) => {
return Observable.defer(() => {
const printingModule = NativeModules.PrintingManager;
const eventEmitter = new NativeEventEmitter(printingModule);
printingModule.initialize();
return Observable.fromEvent(eventEmitter, eventName);
});
};
Here I also wrapped it in Observable.defer so that we don't create the NativeEventEmitter or printingModule.initialize() until someone actually subscribes (maintain laziness). This may or may not be neccesary for you, I don't know what PrintingManager does or how it behaves. e.g. it might be desirable to only create a single emitter and to initialize the module upfront.
const printingModule = NativeModules.PrintingManager;
const printerEmitter = new NativeEventEmitter(printingModule);
printingModule.initialize();
const fromPrinterEvent = (eventName) =>
Observable.fromEvent(printerEmitter, eventName);
So keep in mind that I'm just showing patterns without knowing what PrintingManager, etc does.
Use it within redux-observable
To use this within redux-observable and your epic is now the same as you'd use any other Observable. So we'll want to map the values from it to actions and
mergeMap, switchMap, concatMap, or exhaustMap that into our top-level stream.
Something like this:
const appStatePrepared = action$ =>
action$.ofType(APP_STATE_PREPARED)
.switchMap(() =>
fromPrinterEvent('PrintingManagerNewPrinterConnected')
.map(payload => ({
type: PRINTER_MANAGER_NEW_PRINTER_CONNECTED,
payload: {
macAddress: payload[2],
connectionType: payload[3],
}
}))
);
Remember that many streams, including our custom fromPrinterEvent('PrintingManagerNewPrinterConnected'), go forever until you unsubscribe from them. So if you only want one you'd use .take(1). If you want to unsubscribe when you receive another action you'd use .takeUntil(action$.ofType(WHATEVER)), etc. Normal RxJS patterns.

fetch will not dispatch if I getState first

I changed the signature of an action creator to make a call to getState before trying to dispatch fetch, but now fetch is not getting called.
StartingPoint: I have an async action that makes an api call using fetch and then dispatches a success or error action once it's done, as below. I call this action from a container like so and it works fine:
dispatch(actions.getData()); //from a container
export function getData(){
return (dispatch : any) => {
return fetch(
'http://localhost:8000/api',{}
).then(
response => response.json()
).then(
json => dispatch(successAction(json)),
err => dispatch(notify("SERVER_ERROR"))
);
}
}
The problem is that I need to call getState in the action, so that I can have an option about which port to query. Therefore, I changed the getData action to what you see below. However, when I call the action creator like this dispatch(actions.getData());, it's not making a network call, although the console.log statement is running.
Question: how can the getData function be written to allow for making a call to getState before running the fetch? (and, related, what is the purpose of wrapping it in the dispatch return)?
export const getData = () => (dispatch: any, getState: any) => {
let state = getState();
let url = //code omitted - getting port from state object
console.log("this log statement runs");
return (dispatch : any) => {
return fetch(
url,{}
).then(
response => response.json()
).then(
json => dispatch(successAction(json)),
err => dispatch(notify("SERVER_ERROR"))
);
}
}
added Promise support
const addPromiseSupportToDispatch = (store: any) => {
const rawDispatch = store.dispatch;
return (action: any) => {
if (typeof action.then === 'function') {
return action.then(rawDispatch);
}
return rawDispatch(action);
};
};
store.dispatch = addPromiseSupportToDispatch(store);
You inserted an additional return I think. This should be the right code block
export const getData = () => (dispatch: any, getState: any) => {
let state = getState();
let url = //code omitted - getting port from state object
console.log("this log statement runs");
return fetch(
url,{}
).then(
response => response.json()
).then(
json => dispatch(successAction(json)),
err => dispatch(notify("SERVER_ERROR"))
);
}
EDIT
If I had to use your original code:
export function getData(){
return (dispatch : any, getState: any) => { // <= second parameter provided by redux-thunk
let url = getState().url; //can call getState here
return fetch(
'http://localhost:8000/api',{}
).then(
response => response.json()
).then(
json => dispatch(successAction(json)),
err => dispatch(notify("SERVER_ERROR"))
);
}
}

Resources