I'm trying to learn Ramda and how to use it on my daily work. So I have a quick question. "How can I use pipe with sync and async functions?" or best, how can I improve the following code?
const AuthService = () => ({
async signIn(credentials: Credentials): Promise<AuthSession> {
const result = await api.signIn(credentials)
return R.pipe(
signInResultToAuthSession,
saveAuthSession
)(result)
}
})
[EDITED]: A second alternative that I think be better.
const AuthService = () => ({
async signIn(credentials: Credentials): Promise<AuthSession> {
return api.signIn(credentials).then(
R.pipe(
signInResultToAuthSession,
saveAuthSession
)
)
}
})
pipeWith was added for just such cases. It wraps the functions in a common interface (here then) and passes the results along, just like pipe.
const api = {signIn: ({pwd, ...creds}) => Promise.resolve({...creds, signedIn: true})}
const signInResultToAuthSession = (creds) => Promise.resolve({...creds, auth: true})
const saveAuthSession = (creds) => Promise.resolve({...creds, saved: true})
const AuthService = {
signIn: pipeWith(then)([
api.signIn, // [1]
signInResultToAuthSession,
saveAuthSession
])
}
AuthService.signIn({name: 'fred', pwd: 'flintstone'})
.then(console.log)
// [1]: `bind` might be necessary here, depending on the design of `api.signIn`
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipeWith, then} = R </script>
Do note that pipeWith wraps the functions in an array. One day we'd like to do the same with pipe, but that's a hugely breaking change.
You could create a function like this:
const then = f => p => p.then(f)
And then your pipe would look like:
const AuthService = () => ({
async signIn(credentials: Credentials): Promise<AuthSession> {
return R.pipe(
api.signIn,
then(signInResultToAuthSession),
then(saveAuthSession),
)(credentials)
}
})
You could even catch exceptions with:
const pCatch = f => p => p.catch(f)
R.pipe(
api.signIn,
pCatch(err => console.error(err)),
then(signInResultToAuthSession),
then(saveAuthSession),
)(credentials)
Related
Suppose we have this scenario: we need to make a request to an API to get data on number of books available. If books > 0, we must trigger
some sort of popup with a function which is provided to us. The books need to be stored into the redux store for other components to use. The codebase already uses redux-thunk and redux-thunk-middleware.
What would be the best implementation using a hook and why? (displayPopUp is the function that we must use to trigger the pop-up)
1)
const useBooksNotification = () => {
const dispatch = useDispatch();
const [shouldShowNotification, setShouldShowNotification] = useState(false);
const books = useSelector(selectAvailableBooks);
useEffect(() => {
if (shouldShowNotification) {
setShouldShowNotification(false);
if (books.length > 0) {
displayPopUp();
}
}
}, [shouldShowNotification, books]);
const showNotification = async () => {
if (!shouldShowNotification) {
await dispatch(fetchBooks);
setShouldShowNotification(true);
}
};
return {
showNotification,
};
};
or 2)
const useBooksNotification2 = () => {
const dispatch = useDispatch();
const showNotification = async () => {
{
const response = await dispatch(fetchBooks);
const books = response.value;
if (books.length > 0) {
displayPopUp();
}
}
};
return {
showNotification,
};
};
Personally, I prefer 2 since to me it is much more readable but someone told me 1 is preferable i.e listening to the selector for the books instead of getting the books directly from the action/API response. I am very curious as to why this is? Or if there is an even better implementation.
I would understand using a selector if there was no displayPopUp function given to us and instead there was some implementation like so:
const BooksNotification = () => {
const dispatch = useDispatch();
const books = useSelector(selectAvailableBooks);
const showNotification = () => {
dispatch(fetchBooks);
};
return books.length > 0 ? <h1>Pretend this is a pop-up</h1> : null;
};
const SomeComponent = () => {
<div>
<h1>Component</h1>
<BooksNotification />
</div>
}
I' trying to test a custom hook but I receive this warning message
console.error node_modules/#testing-library/react-hooks/lib/core/console.js:19
Warning: An update to TestComponent inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
This is my custom hook
import { useState, useEffect } from 'react'
import io from 'socket.io-client'
import config from './../../../../config'
const useNotificationsSocket = (user) => {
const [socket, setSocket] = useState(null)
const [numUnreadMessages, setNumUnreadMessages] = useState(0)
const configureSocket = socket => {
socket.on('connect', () => {
const data = {
user: user,
}
socket.emit('user joined', data)
})
socket && socket.on('messages updated', (data) => {
//console.log(data)
setNumUnreadMessages(data.numUnreadMessages)
})
}
useEffect(() => {
const fetchSocket = async () => {
const s = await io(config.nSocket.url, {transports: ['websocket']})
configureSocket(s)
setSocket(s)
}
// Check that user is not an empty object as this causes a crash.
user && user.Id && fetchSocket()
}, [user])
return [socket, numUnreadMessages]
}
export { useNotificationsSocket }
and this is the test
import { renderHook, act } from '#testing-library/react-hooks'
import { useNotificationsSocket } from './../hooks/useNotificationsSocket'
jest.mock('socket.io-client')
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result } = renderHook(() => useNotificationsSocket(user))
expect(result).not.toBeNull()
})
})
I've tried importing act and wrapping the code in a call to act but however I try to wrap the code I still get a warning and can't figure out how I should use act in this case.
Your hook is asynchronous, so you need to await its response:
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result } = renderHook(() => useNotificationsSocket(user))
await waitFor(() => expect(result).not.toBeNull())
})
})
Additionally, if you define multiple tests, you may encounter your original error if you fail to unmount the hook. At least this appears to be the behaviour in #testing-library/react v13.3.0. You can solve this by unmounting the hook when your test completes:
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result, unmount } = renderHook(() => useNotificationsSocket(user))
await waitFor(() => expect(result).not.toBeNull())
unmount()
})
})
I have some problem with apply fetching "more" data using fromFetch from rxjs.
I have project with React and RXJS. Currently I'm using something like this:
const stream$ = fromFetch('https://pokeapi.co/api/v2/pokemon?limit=100', {
selector: response => response.json()
}).subscribe(data => console.log(data));
But! I would like to change limit dynamically, when I click button or even better - when I scroll to the very bottom of my website. How to make something like this?
So that, based on some interaction, the limit would change?
The way your observable work in your case it's a request-response. You're declaring stream$ to be an observable that when someone subscribes it will make a request with limit=100.
There are different ways of solving this... The most straightforward would be:
const getPokemon$ = limit =>
fromFetch('https://pokeapi.co/api/v2/pokemon?limit=' + limit, {
selector: response => response.json()
});
const MyComponent = () => {
// ...
useEffect(() => {
const sub = getPokemon$(limit).subscribe(res => console.log(res));
return () => sub.unsubscribe();
}, [limit])
// ...
}
Another option, probably a bit more reactive but harder to follow for others, would be to declare another stream which sets the limit:
const limit$ = new BehaviorSubject(100)
const pokemon$ = limit$.pipe(
switchMap(limit => fromFetch('https://pokeapi.co/api/v2/pokemon?limit=' + limit, {
selector: response => response.json()
}))
);
// In your component
const MyComponent = () => {
// ...
useEffect(() => {
const sub = pokemon$.subscribe(res => console.log(res));
return () => sub.unsubscribe();
}, [])
changeLimit = (newLimit) => limit$.next(newLimit)
// ...
}
In this other solution, you're declaring how pokemon$ should react to changes on limit$, and you can set limit$ from any other component you want.
I created a recordSaga function, its target is to record what actions have been dispatched during the saga.
export const recordSaga = async (saga, initialAction, state) => {
const dispatched = [];
const done = await runSaga(
{
dispatch: action => dispatched.push(action),
getState: () => state,
},
saga,
initialAction,
).done;
return {
dispatched,
done,
};
};
so let's say my saga is this one
export function* mySaga() {
const needToSave = yield select(needToSaveDocument);
if (needToSave) {
yield put(saveDocument());
yield take(SAVE_DOCUMENT_SUCCESS);
}
yield put(doSomethingElse())
}
I want to write two tests, which I expect to be the following
describe('mySaga', async () => {
it('test 1: no need to save', async () => {
const state = { needToSave: false }
const { dispatched } = await recordSaga(mySaga, {}, state);
expect(dispatched).toEqual([
doSomethingElse()
])
})
it('test 2: need to save', async () => {
const state = { needToSave: true }
const { dispatched } = await recordSaga(mySaga, {}, state);
expect(dispatched).toEqual([
saveDocument(),
doSomethingElse()
])
})
})
However, for the test 2 where there is a take in between, and of course jest (or its girlfriend jasmine) is yelling at me: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
I know it is because runSaga is waiting for the take(SAVE_DOCUMENT_SUCCESS), but how can I mock that up ?
The answer stdChannel().put({type, payload})
Why ?
Using stdChannel you can dispatch after the first run.
How ?
import stdChannel;
add to the first param in runSaga;
call stdChannel().put(SAVE_DOCUMENT_SUCCESS);
Example
what worked for me
I left the first test as it is the expected final result, but the solution comes on the last 2.
import { runSaga, stdchannel } from 'redux-saga'
let dispatchedActions = [];
let channel;
let fakeStore;
beforeEach(() => {
channel = stdChannel(); // you have to declare the channel to have access to it later
fakeStore = {
channel, // add it to the store in runSaga
getState: () => "initial",
dispatch: (action) => dispatchedActions.push(action),
};
});
afterEach(() => {
global.fetch.mockClear();
});
it("executes getData correctly", async () => {
await runSaga(fakeStore, getData, getAsyncData("test")).toPromise();
expect(global.fetch.mock.calls.length).toEqual(1);
expect(dispatchedActions[0]).toEqual(setData(set_value));
});
it("triggers takeLatest and call getData(), but unfortunately doesn't resolve promise", async () => {
await runSaga(fakeStore, rootSaga)// .toPromise() cannot be used here, as will throw Timeout error
channel.put(getAsyncData("test")); // if remove this line, the next 2 expects() will fail
expect(global.fetch.mock.calls.length).toEqual(1);
// expect(dispatchedActions[1]).toEqual(setData(set_value)); // will fail here, but pass on the next it()
});
it("takes the promised data from test above", () => {
expect(dispatchedActions[1]).toEqual(setData(set_value));
});
this answer (about true code, not tests) helped me
By looking at recordSaga:
export const recordSaga = async (saga, initialAction, state) => {
It seems that you should pass {type: SAVE_DOCUMENT_SUCCESS} as a second argument (i.e initialAction). That should trigger the take effect.
I am writing an epic using redux-observable and am trying to write an epic using multiple filters (oftype). Given below is my sample code
export const landingEpic = action$ => {
console.log('inside landing epic');
return action$.ofType('APPLY_SHOPPING_LISTS').map(() => (
{
type: 'APPLYING_SHOPPING_LISTS',
})
);
return action$.ofType('APPLIED_SHOPPING_LIST'){
//here I want to return something else
}
}
However I cannot have two return methods in one epic?
You'll want to combine them with Observable.merge() then return that--however I also highly suggest separating them into two separate epics. It will make it easier to test, but that's of course your call.
export const landingEpic = action$ => {
return Observable.merge(
action$.ofType('APPLY_SHOPPING_LISTS')
.map(() => ({
type: 'APPLYING_SHOPPING_LISTS',
}),
action$.ofType('APPLIED_SHOPPING_LIST')
.map(() => ({
type: 'SOMETHING_ELSE',
}),
);
}
It sounds like you want to use combineEpics:
import { combineEpics } from "redux-observable";
const landingEpic1 = // put epic1 definition here
const landingEpic2 = // put epic2 definition here
export default combineEpics(
landingEpic1,
landingEpic2,
// ...
);