Is is possible/safe to use withHandlers with promises?
Ex.:
withHandlers({
onChange: props => event => {
props.callAPI(props.data)
.then(data => props.updateData(data))
},
...
Thanks!
After some tests I realized that it's working pretty well. Recompose rocks for building with pure components.
This is perfectly valid and working pretty well.
const enhWithHandlers = withHandlers({
loginUserMutation: props => args => {
props.updateMutationState(loading: true, error: null });
props.loginUser(args)
.then(() =>
props.updateMutationState({loading: false, error: null }))
.catch(err =>
props.updateMutationState({ loading: false, error: err }));
}
},
...
// then compose like
export default compose(
reduxConnect,
gqlConnectLogin,
gqlConnectRegister,
enhWithState,
enhWithHandlers
)(UserLoginRegister);
It helps me to overcome lack of ability to reflect results of graphQl mutation with Apollo client to the wrapped component. This handles it perfectly and without the need of side effects in the component itself.
But there are some problems when we use it like this:
compose(
withState('loginStatus', 'setLoginStatus', {loading: false, error:null}),
withHandlers({
loginUserMutation: props => async args => {
try {
props.setLoginStatus({loading: true, error: null});
await props.loginUser(args);
} catch(error) {
props.setLoginStatus({...props.loginStatus, error});
} finally {
props.setLoginStatus({...props.loginStatus, loading: false});
}
}
})
)
Because the props reference is indeed lost after we await props.loginUser(args). Then we use it after that is wrong.
We should notice not to use it like above.
Related
I am trying to include two Apollo-Client useLazyQuery hooks in my function component. Either works fine alone with the other one commented out, but as soon as I include both, the second one does nothing. Any ideas?
export default function MainScreen(props) {
useEffect(() => {
validateWhenMounting();
}, []);
const [validateWhenMounting, { loading, error, data }] = useLazyQuery(
validateSessionToken,
{
onCompleted: (data) => console.log('data', data),
},
);
const [validate, { loading: loading2, error: error2, data: data2 }] =
useLazyQuery(validateSessionTokenWhenSending, {
onCompleted: (data2) => console.log('data2', data2),
});
const handleSendFirstMessage = (selectedCategory, title, messageText) => {
console.log(selectedCategory, title, messageText);
validate();
};
Figured it out: Adding the key-value pair fetchPolicy: 'network-only', after onCompleted does the trick. It seems that otherwise, no query is being conducted due to caching...
This is the pattern that I was talking about and mentioned in the comments:
const dummyComponent = () => {
const [lazyQuery] = useLazyQuery(DUMMY_QUERY, {variables: dummyVariable,
onCompleted: data => // -> some code here, you can also accept an state dispatch function here for manipulating some state outside
onError: error => // -> you can accept state dispatch function here to manipulate state from outside
});
return null;
}
this is also a pattern that you are going to need sometimes
I am implementing a mock function for firebase authentication, I have mocked the firebase module like this:
jest.mock("../../lib/api/firebase", () => {
return {
auth: {
createUserWithEmailAndPassword: jest.fn(() => {
return {
user: {
uid: "fakeuid",
},
};
}),
signOut: jest.fn(),
},
firestore: {
collection: jest.fn(() => ({
doc: jest.fn(() => ({
collection: jest.fn(() => ({
add: jest.fn(),
})),
set: jest.fn(),
})),
})),
},
};
});
right now createUserWithEmailAndPassword returns an user uid to fake the firebase response on signup. I use it in my test like this:
.
.
.
await wait(() => {
expect(auth.createUserWithEmailAndPassword).toHaveBeenCalled();
expect(history.location.pathname).toBe("/dashboard");
expect(getByText("You succesfully signed up!")).toBeInTheDocument();
});
it works perfectly fine, but how if I want the returned value to be something different for one test?
I saw there is mockImplementationOnce that seems to be the right path but I am struggling implementing it, any help?
Thanks,
F.
You can use mockReturnValueOnce like this:
auth.createUserWithEmailAndPassword
.mockReturnValueOnce(/* your desired return value here */)
Just make sure you are mocking the return value before calling auth.createUserWithEmailAndPassword in your test.
For example:
auth.createUserWithEmailAndPassword
.mockReturnValueOnce({user: { uid: 'mocked uid'}})
auth.createUserWithEmailAndPassword()
// Insert your expect statement(s) here
...
I just found a finite state module called Robot. it's very lightweight and simple. I got one case I couldn't solve, which is to create a dynamic request for API inside Robot. I tried this
robot.js
const context = () => ({
data: [],
});
export const authRobot = (request) =>
createMachine(
{
ready: state(transition(CLICK, 'loading')),
loading: invoke(
request,
transition(
'done',
'success',
reduce((ctx, evt) => ({ ...ctx, data: evt }))
),
transition(
'error',
'error',
reduce((ctx, ev) => ({ ...ctx, error: ev }))
)
),
success: state(immediate('ready')),
error: state(immediate('ready')),
},
context
);
and I use it in my react component like this
// ...
export default function Login() {
const [current, send] = useMachine(authRobot(UserAPI.getData));
const { data } = current.context;
function handleSubmit(e) {
e.preventDefault();
send(CLICK);
}
useEffect(() => {
console.log(data);
console.log(current);
console.log(current.name);
}, [data]);
// ...
the problem happened when I click the button, my web console logs many data. it seems the event called multiple times. what can I do here?
I think the problem here is that useMachine() will trigger a rerender when passed a different machine. Since you are creating a new machine inside your render function useMachine sees this as a different machine every time.
I'd create your machine outside of this closure.
I'm doing a mutation using Apollo-client and redux-observable and so far this is my code:
export const languageTimeZoneEpic = (action$) => {
return action$.ofType('PING')
.flatMap(action => client.mutate({
mutation: languageTimeZoneIdMutation,
variables: { id: action.id, defaultLanguage: action.selected_language, defaultTimeZoneId: action.selected_timeZone }
})
.then(store.dispatch(setLocale(action.selected_language)))
)
.map(result => ({
type: 'PONG',
payload: result
}))
.catch(error => ({
type: 'PONG_ERROR'
}));
};
My mutation works correctly but I can't seem to make my catch(error) work. In the small amount of documentation I've found on this, it suggests I put Observable of after error => but then it gives me an error saying Observable is undefined.
Thank you
UPDATE:
If the connection between the app and the server doesn't work, it just waits for the connection to come back up and then finish the epic. I would like for it to just catch and error and stop the epic.
Figured out there was nothing wrong with the catch(error) in the code above. Turns out putting the server on hold did not throw an error when I tried to establish a connection. It had to be shut down for it to throw an error.
For those who are looking for a good way to implement a mutation using Apollo-client with redux-observable (because there is almost no docs on it), here's the code I used for my mutation:
import { languageMutation } from '../mutation/languageMutation';
import { changeLanguageFulfilled, changeLanguageError } from '../actions/actions';
export const languageEpic = (action$) => {
return action$.ofType('CHANGE_LANGUAGE')
.mergeMap(action => client.mutate({
mutation: languageMutation,
variables: { id: action.id, defaultLanguage: action.selected_language, defaultTimeZoneId: action.selected_timeZone }
}).then(result => changeLanguageFulfilled(result))
.catch(error => changeLanguageError(error))
);
};
Both changeLanguageFulfilled and changeLanguageError are actions that trigger their own reducer.
Hope this helps someone.
I have the following middleware that I use to call similar async calls:
import { callApi } from '../utils/Api';
import generateUUID from '../utils/UUID';
import { assign } from 'lodash';
export const CALL_API = Symbol('Call API');
export default store => next => action => {
const callAsync = action[CALL_API];
if(typeof callAsync === 'undefined') {
return next(action);
}
const { endpoint, types, data, authentication, method, authenticated } = callAsync;
if (!types.REQUEST || !types.SUCCESS || !types.FAILURE) {
throw new Error('types must be an object with REQUEST, SUCCESS and FAILURE');
}
function actionWith(data) {
const finalAction = assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
}
next(actionWith({ type: types.REQUEST }));
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I am then making the following calls in componentWillMount of a component:
componentWillMount() {
this.props.fetchResults();
this.props.fetchTeams();
}
fetchTeams for example will dispatch an action that is handled by the middleware, that looks like this:
export function fetchTeams() {
return (dispatch, getState) => {
return dispatch({
type: 'CALL_API',
[CALL_API]: {
types: TEAMS,
endpoint: '/admin/teams',
method: 'GET',
authenticated: true
}
});
};
}
Both the success actions are dispatched and the new state is returned from the reducer. Both reducers look the same and below is the Teams reducer:
export const initialState = Map({
isFetching: false,
teams: List()
});
export default createReducer(initialState, {
[ActionTypes.TEAMS.REQUEST]: (state, action) => {
return state.merge({isFetching: true});
},
[ActionTypes.TEAMS.SUCCESS]: (state, action) => {
return state.merge({
isFetching: false,
teams: action.payload.response
});
},
[ActionTypes.TEAMS.FAILURE]: (state, action) => {
return state.merge({isFetching: false});
}
});
The component then renders another component that dispatches another action:
render() {
<div>
<Autocomplete items={teams}/>
</div>
}
Autocomplete then dispatches an action in its componentWillMount:
class Autocomplete extends Component{
componentWillMount() {
this.props.dispatch(actions.init({ props: this.exportProps() }));
}
An error happens in the autocomplete reducer that is invoked after the SUCCESS reducers have been invoked for fetchTeams and fetchResults from the original calls in componentWillUpdate of the parent component but for some reason the catch handler in the middleware from the first code snippet is invoked:
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I do not understand why the catch handler is being invoked as I would have thought the promise has resolved at this point.
Am not completely sure, it's hard to debug by reading code. The obvious answer is because it's all happening within the same stacktrace of the call to next(actionWith({ type: types.SUCCESS, payload: { response } })).
So in this case:
Middleware: Dispatch fetchTeam success inside Promise.then
Redux update props
React: render new props
React: componentWillMount
React: Dispatch new action
If an error occurs at any point, it will bubble up to the Promise.then, which then makes it execute the Promise.catch callback.
Try calling the autocomplete fetch inside a setTimeout to let current stacktrace finish and run the fetch in the next "event loop".
setTimeout(
() => this.props.dispatch(actions.init({ props: this.exportProps() }))
);
If this works, then its' the fact that the event loop hasn't finished processing when the error occurs and from the middleware success dispatch all the way to the autocomplete rendered are function calls after function calls.
NOTE: You should consider using redux-loop, or redux-saga for asynchronous tasks, if you want to keep using your custom middleware maybe you can get some inspiration from the libraries on how to make your api request async from the initial dispatch.