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.
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
Background
I'm building a React Native 0.64.1 app using Redux 4.1.0. This app fetches data from an API endpoint via POST which can take multiple "category" params. Only one value can be passed as category at a time, so in order to display data from multiple categories one would have to execute the function one time per category.
This is how the axios request is handled:
export const getData = (tk, value) =>
apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
})
.then(response => {
return response.data;
})
.catch(error => {
return Promise.reject(error.message);
});
This function is then executed via a redux action/reducer, etc.
The tricky part is that "value" is set by the user and can be changed at any point in time.
The front end meets this function in a certain screen where this happens:
useEffect(() => {
dispatch(retrieveData(tk, value));
}, [dispatch, value]);
Problem & Question
I've tried doing a for loop that would iterate through an array that contains the possible strings of text value could be, that would look something like this:
const arrayOfValues = ['A','B','C','D']
let value = null;
useEffect(() => {
for (let i = 0; i < arrayOfValues.length; i++) {
value = arrayOfValues[i];
dispatch(retrieveData(tk, value));
}
}, [dispatch, value]);
I know this is horrible and I'm just showing it because it's the only thing I could think about (and it doesn't even work).
An ideal solution would:
Execute the first request on load
Run a request once per item in an array WITHOUT deleting the previously called for data
Each time it runs it needs to update the "value" parameter.
As a note about "retrieveData()", that is just the redux action.
Any help would be very much appreciated.
Solution by #rigojr
This seems like it should work, but either I haven't expressed myself properly or there's something wrong with the answer. I'm guessing it's the former.
#rigojr proposed the following:
export const getData = (tk, values) => values.map((value) => apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
}))
Promise.all(getData(tk,values)) *****
.then(responseValues => {
// Dispatch the response, it will come an array of values response.
})
.catch(eer => {
// Error handling
})
Howeve, values in the line marked with many asterisks is inaccessible. I believe this is because previosuly I failed to mention that the whole Redux data flow happens in three separate files.
Dispatching the action: UI dispatches an action onLoad in App.js:
useEffect(() => {
dispatch(retrieveData(tk, values));
}, [dispatch, value]);
The action is ran in action.js file. It looks something like this:
Note that I have added the Promise.all() in this screen, as it seems like the place where it should actually go, instead of the other one.
export const actionTypes = keyMirror({
RETRIEVE_REQUEST: null,
RETRIEVE_SUCCESS: null,
RETRIEVE_FAILURE: null,
});
const actionCreators = {
request: createAction(actionTypes.RETRIEVE_REQUEST),
success: createAction(actionTypes.RETRIEVE_SUCCESS),
failure: createAction(actionTypes.RETRIEVE_FAILURE),
};
export const retrieveData = (tk, values) => dispatch => {
dispatch(actionCreators.request());
Promise.all(getData(tk, values))
.then(data => dispatch(actionCreators.success(data)))
.catch(error => dispatch(actionCreators.failure(error)));
};
Then there's the reducer, of course in reducer.js:
export const initialState = {
loadingData: false,
data: [],
error: null,
};
const actionsMap = {
[actionTypes.RETRIEVE_REQUEST]: state => ({
...state,
loadingData: true,
}),
[actionTypes.RETRIEVE_SUCCESS]: (state, action) => ({
...state,
loadingData: false,
data: action.payload,
}),
[actionTypes.RETRIEVE_FAILURE]: (state, action) => ({
...state,
loadingData: false,
error: action.payload,
}),
};
export default (state = initialState, action) => {
const actionHandler = actionsMap[action.type];
if (!actionHandler) {
return state;
}
return actionHandler(state, action);
};
Data is then accessed via a selector:
const data = useSelector(state => state.data.data);
When running the code above, I am greeted with the following lovely error message:
TypeError: undefined is not a function (near '...}).then(function (response)...')
And in the emulator, I get pointed in the direction of these lines of code:
export const getData = (tk, values) => values.map((value) => apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
}))
More specifically, the emulator seems to think that the error has to do with value.map, as it points a little red arrow at "values" just before the method.
Any idea on what went wrong?
Note
Upon refresh the error might change, for example just now it has shown the same error message but it points in the direction of
export const retrieveData = (tk, values) => dispatch => {
dispatch(actionCreators.request());
Promise.all(getData(tk, values))
.then(data => dispatch(actionCreators.success(data)))
.catch(error => dispatch(actionCreators.failure(error)));
};
More specifically, the little red arrow points at getData.
Refreshing again, and the error points at
useEffect(() => {
dispatch(retrieveData(tk, values));
}, [dispatch, value]);
Refrsh once more and it just loses it and goes for a module, as shown in the image:
It doesn't go further from there. Just mind that every single time, the error message is TypeError: undefined is not a function (near '...}).then(function (response)...'), it just points in a new direction.
Solved in
Unable to perform .map whithin function
Try to use a Promise.all():
export const getData = (tk, values) => values.map((value) => apiInstance
.request({
url: ENDPOINTS.CATEGORIES,
method: 'POST',
data: qs.stringify({
token: tk,
category: value,
}),
}))
Promise.all(getData(tk,values))
.then(responseValues => {
// Dispatch the response, it will come an array of values response.
})
.catch(eer => {
// Error handling
})
Read more about Promise.all() here
I don't really know how to ask clearly but, I will paste my code first and ask below.
function useToDos() {
const queryCache = useQueryCache();
const fetchTodos = useQuery(
'fetchTodos',
() => client.get(paths.todos).then(({ data }: any) => data),
{ enabled: false }
);
const createTodo = async ({ name ) =>
await client.post(paths.todos, { name }).then(({ data }) => data);
return {
fetchTodos,
createTodo: useMutation(createTodo, {
onMutate: newItem => {
queryCache.cancelQueries('fetchTodos');
const previousTodos = queryCache.getQueryData('fetchTodos');
queryCache.setQueryData('fetchTodos', old => [
...old,
newItem,
]);
return () => queryCache.setQueryData('fetchTodos', previousTodos);
},
}),
};
}
As you can see, I am trying to create my own custom hooks that wrap react-query functionality. Because of this, I need to set my fetchTodos query to be disabled so it doesn't run right away. However, does this break all background data fetching?
Specifically, when I run createTodo and the onMutate method triggers, I would ideally like to have the fetchTodos query update in the background so that my list of todos on the frontend is updated without having to make the request again. But it seems that with the query initially set to be disabled, the background updating doesn't take effect.
As I don't think wrapping react-query hooks into a library of custom hooks is a very great idea, I will probably have more questions about this same setup but for now, I will start here. Thank you. 😊
The mutation does not automatically triggers a refetch. The way to achieve this using react-query is via queryCache.invalidateQueries to invalidate the cache after the mutation. From the docs:
The invalidateQueries method can be used to invalidate and refetch single or multiple queries in the cache based on their query keys or any other functionally accessible property/state of the query. By default, all matching queries are immediately marked as stale and active queries are refetched in the background.
So you can configure the useMutation to invalidate the query when the mutation settles. Example:
function useToDos() {
const queryCache = useQueryCache();
const fetchTodos = useQuery(
'fetchTodos',
() => client.get(paths.todos).then(({ data }: any) => data),
{ enabled: false }
);
const createTodo = async ({ name ) =>
await client.post(paths.todos, { name }).then(({ data }) => data);
return {
fetchTodos,
createTodo: useMutation(createTodo, {
onMutate: newItem => {
queryCache.cancelQueries('fetchTodos');
const previousTodos = queryCache.getQueryData('fetchTodos');
queryCache.setQueryData('fetchTodos', old => [
...old,
newItem,
]);
return () => queryCache.setQueryData('fetchTodos', previousTodos);
},
onSettled: () => {
cache.invalidateQueries('fetchTodos');
}
}),
};
}
What about splitting the logic into two different hooks? Instead of a monolith like useToDos?
That way you could have a hook for fetching:
const fetchData = _ => client.get(paths.todos).then(({ data }: any) => data)
export default function useFetchTodo(
config = {
refetchOnWindowFocus: false,
enabled: false
}
) {
return useQuery('fetchData', fetchData, config)
}
And in your mutation hook you can refetch manually, before createTodo
import useFetchTodo from './useFetchTodo'
//
const { refetch } = useFetchTodo()
// before createTodo
refetch()
I am trying to test a Redux Observable epic which dispatches an action to invoke an other epic. Somehow the invoked epic is not called.
Lets say my epics looks like this;
const getJwtEpic = (action$, store, { api }) =>
action$.ofType('GET_JWT_REQUEST')
.switchMap(() => api.getJWT()
.map(response => {
if (response.errorCode > 0) {
return {
type: 'GET_JWT_FAILURE',
error: { code: response.errorCode, message: response.errorMessage },
};
}
return {
type: 'GET_JWT_SUCCESS',
idToken: response.id_token,
};
})
);
const bootstrapEpic = (action$, store, { api }) =>
action$.ofType('BOOTSTRAP')
.switchMap(() =>
action$.filter(({ type }) => ['GET_JWT_SUCCESS', 'GET_JWT_FAILURE'].indexOf(type) !== -1)
.take(1)
.mergeMap(action => {
if (action.type === 'GET_JWT_FAILURE') {
return Observable.of({ type: 'BOOTSTRAP_FAILURE' });
}
return api.getProfileInfo()
.map(({ profile }) => ({
type: 'BOOTSTRAP_SUCCESS', profile,
}));
})
.startWith({ type: 'GET_JWT_REQUEST' })
);
When I try to test the bootstrapEpic in Jest with the following code;
const response = {};
const api = { getJWT: jest.fn() };
api.getJWT.mockReturnValue(Promise.resolve(response));
const action$ = ActionsObservable.of(actions.bootstrap());
const epic$ = epics.bootstrapEpic(action$, null, { api });
const result = await epic$.toArray().toPromise();
console.log(result);
The console.log call gives me the following output;
[ { type: 'GET_JWT_REQUEST' } ]
Somehow the getJwtEpic isn't called at all. I guess it has something to do with the action$ observable not dispatching the GET_JWT_REQUEST action but I can't figure out why. All help is so welcome!
Assuming actions.rehydrate() returns an action of type BOOTSTRAP and the gigya stuff is a typo,
getJwtEpic isn't called because you didn't call it yourself 🤡 When you test epics by manually calling them, then it's just a function which returns an Observable, without any knowledge of the middleware or anything else. The plumbing that connects getJwtEpic as part of the root epic, and provides it with (action$, store) is part of the middleware, which you're not using in your test.
This is the right approach, testing them in isolation, without redux/middleware. 👍 So you test each epic individually, by providing it actions and dependencies, then asserting on the actions it emits and the dependencies it calls.
You'll test the success path something like this:
const api = {
getProfileInfo: () => Observable.of({ profile: 'mock profile' })
};
const action$ = ActionsObservable.of(
actions.rehydrate(),
{ type: 'GET_JWT_SUCCESS', idToken: 'mock token' }
);
const epic$ = epics.bootstrapEpic(action$, null, { api });
const result = await epic$.toArray().toPromise();
expect(result).toEqual([
{ type: 'GET_JWT_REQUEST' },
{ type: 'BOOTSTRAP_SUCCESS', profile: 'mock profile' }
]);
Then you'll test the failure path in another test by doing the same thing except giving it GET_JWT_FAILURE instead of GET_JWT_SUCCESS. You can then test getJwtEpic separately as well.
btw, ofType accepts any number of types, so you can just do action$.ofType('GET_JWT_SUCCESS', 'GET_JWT_FAILURE')
I try to build a generic confirm component with redux and native promise. I read Dan Abramovs solution here: How can I display a modal dialog in Redux that performs asynchronous actions? but i am looking for a more generic appoach.
Basically i want to do this:
confirm({
type: 'warning',
title: 'Are you sure?',
description: 'Would you like to do this action?',
confirmLabel: 'Yes',
abortLabel: 'Abort'
})
.then(() => {
// do something after promise is resolved
})
The confirm method basically opens the modal and returns a promise. Inside the promise i subscribe my redux store, listen for state changes and resolve or reject the promise:
export const confirm = function(settings) {
// first dispatch openConfirmModal with given props
store.dispatch(
openConfirmModal({
...settings
})
);
// return a promise that subscribes to redux store
// see: http://redux.js.org/docs/api/Store.html#subscribe
// on stateChanges check for resolved/rejected
// if resolved or rejected:
// - dispatch closeConfirmModal
// - resolve or reject the promise
// - unsubscribe to store
return new Promise((resolve, reject) => {
function handleStateChange() {
let newState = store.getState();
if (newState.confirmModal.resolved) {
store.dispatch(closeConfirmModal());
resolve();
unsubscribe();
}
if (newState.confirmModal.rejected) {
store.dispatch(closeConfirmModal());
reject();
unsubscribe();
}
}
let unsubscribe = store.subscribe(handleStateChange);
})
}
My confirm component is connected to redux store and is included once in some kind of layout component - so it is useable on all routes in the app:
class ConfirmModal extends Component {
constructor(props) {
super(props)
}
confirm() {
this.props.dispatch(resolveConfirmModal());
}
abort() {
this.props.dispatch(rejectConfirmModal());
}
render() {
// my modal window
}
}
export default connect(
state => ({
confirmModal: state.confirmModal
})
)(ConfirmModal);
Reducer/Action looks like this:
export const openConfirmModal = (settings) => {
return {
type: 'OPEN_CONFIRM_MODAL',
settings
};
};
export const resolveConfirmModal = () => {
return {
type: 'RESOLVE_CONFIRM_MODAL'
};
};
export const rejectConfirmModal = () => {
return {
type: 'REJECT_CONFIRM_MODAL'
};
};
export const closeConfirmModal = () => {
return {
type: 'CLOSE_CONFIRM_MODAL'
};
};
const initialState = {
open: false,
type: 'info',
title: 'Are you sure?',
description: 'Are you sure you want to do this action?',
confirmLabel: 'Yes',
abortLabel: 'Abort',
};
export const ConfirmModalReducer = (state = initialState, action) => {
switch (action.type) {
case 'OPEN_CONFIRM_MODAL':
return { ...action.settings, open: true };
case 'RESOLVE_CONFIRM_MODAL':
return { ...state, resolved: true };
case 'REJECT_CONFIRM_MODAL':
return { ...state, rejected: true };
case 'CLOSE_CONFIRM_MODAL':
return initialState;
default:
return state;
}
};
The redux part is working. My confirm window can be open/closed and renders depending on my options. But how i can define a promise in my confirm method that can be resolved in my component? How i get everything connected?
Found a working Solution!
Found a solution that is pretty much what i was looking for:
The modal properties are driven by my Redux state
The modal component is included once AND it lives inside my
applicition not as a different rendered app like
here:http://blog.arkency.com/2015/04/beautiful-confirm-window-with-react/
The confirm method returns a native promise that is
resolved/rejected driven by Redux state
What do you think?
Well, you can do it, but it won't be pretty. Basically, you need a map of outstanding promises next to your confirm():
var outstandingModals = {}
const confirm = function(settings) {
return new Promise((resolve, reject) => {
let id = uuid.v4();
outstandingModals = resolve;
store.dispatch(
openConfirmModal({
...settings,
confirmationId: id,
})
);
}
and then later:
case 'CLOSE_CONFIRM_MODAL':
let resolve = outstandingModals[state.confirmationId];
if (resolve) {
resolve();
delete outstandingModals[state.confirmationId];
}
return initialState;
Like I said - ugly. I don't think you can do much better than that using promises.
But you can do better by NOT using promises. What I would do is simply render a Confirm component whenever necessary, say:
render() {
return <div>
... My stuff ...
{confirmationNecessary && <Confirm text='Are you sure?' onAction={this.thenConfirmed}/>}
</div>
}
confirmationNecessary can come from this.state or from the store.
I wrote a blog post that discusses one possible approach for managing "generic" modals like this: Posts on PacktPub: "Generic Redux Modals" and "Building Better Bundles".
The basic idea is that the code that requested the modal can include a pre-written action as a prop, and the modal can dispatch that action once it's closed.
There's also an interesting-looking library at redux-promising-modals that appears to implement modal result promises through middleware.
possible so :)
export const MsgBox = {
okCancel : s =>
new Promise((ok, cancel) =>
confirm(s) ? ok() : cancel() ),
......etc